[
  {
    "path": ".circleci/config.yml",
    "content": "env: &env\n  environment:\n    GRUNTWORK_INSTALLER_VERSION: v0.0.36\n    MODULE_CI_VERSION: v0.46.0\n    MODULE_GCP_CI_VERSION: v0.1.1\n    MODULE_CI_CIRCLECI_HELPER_VERSION: v0.56.0\n    TERRAFORM_VERSION: 1.5.7\n    TOFU_VERSION: 1.8.0\n    PACKER_VERSION: 1.10.0\n    TERRAGRUNT_VERSION: v0.80.4\n    TERRAGRUNT_TEST_VERSION: v0.93.10  # Version used for terragrunt module tests\n    OPA_VERSION: v1.1.0\n    GO_VERSION: 1.26.0\n    GO111MODULE: auto\n    K8S_VERSION: v1.28.0  # Same as EKS\n    MINIKUBE_VERSION: v1.35.0\n    CRI_DOCKERD_VERSION: v0.3.16\n    CNI_PLUGINS_VERSION: v1.6.2\n    HELM_VERSION: v3.13.1\n    KUBECONFIG: /home/circleci/.kube/config\n    BIN_BUILD_PARALLELISM: 3\n    MISE_VERSION: v2024.4.0\n    # Mise ASDF defaults to using main.tf to determine the terraform version to use, so we need to\n    # override this to use the .terraform-version file instead.\n    ASDF_HASHICORP_TERRAFORM_VERSION_FILE: .terraform-version\n\ndefaults: &defaults\n  machine:\n    enabled: true\n    image: ubuntu-2004:2022.10.1\n  <<: *env\n\nsetup_minikube: &setup_minikube\n  command: |\n    sudo apt update -y\n    sudo apt install -y conntrack\n\n    # Install cri-dockerd (required for minikube none driver with K8s v1.24+)\n    CRI_DOCKERD_VERSION_NUM=\"${CRI_DOCKERD_VERSION#v}\"\n    curl -sLO \"https://github.com/Mirantis/cri-dockerd/releases/download/${CRI_DOCKERD_VERSION}/cri-dockerd-${CRI_DOCKERD_VERSION_NUM}.amd64.tgz\"\n    tar xzf \"cri-dockerd-${CRI_DOCKERD_VERSION_NUM}.amd64.tgz\"\n    sudo install -m 0755 cri-dockerd/cri-dockerd /usr/local/bin/cri-dockerd\n\n    # Set up cri-dockerd systemd service\n    curl -sLo /tmp/cri-docker.service \"https://raw.githubusercontent.com/Mirantis/cri-dockerd/${CRI_DOCKERD_VERSION}/packaging/systemd/cri-docker.service\"\n    curl -sLo /tmp/cri-docker.socket \"https://raw.githubusercontent.com/Mirantis/cri-dockerd/${CRI_DOCKERD_VERSION}/packaging/systemd/cri-docker.socket\"\n    sudo mv /tmp/cri-docker.service /tmp/cri-docker.socket /etc/systemd/system/\n    sudo sed -i -e 's,/usr/bin/cri-dockerd,/usr/local/bin/cri-dockerd,' /etc/systemd/system/cri-docker.service\n    sudo systemctl daemon-reload\n    sudo systemctl enable --now cri-docker.socket\n\n    # Install CNI plugins (required for minikube none driver with K8s v1.24+)\n    curl -sLO \"https://github.com/containernetworking/plugins/releases/download/${CNI_PLUGINS_VERSION}/cni-plugins-linux-amd64-${CNI_PLUGINS_VERSION}.tgz\"\n    sudo mkdir -p /opt/cni/bin\n    sudo tar -C /opt/cni/bin -xzf \"cni-plugins-linux-amd64-${CNI_PLUGINS_VERSION}.tgz\"\n\n    # Retry setup-minikube to reduce flakes from transient GPG key retrieval timeouts\n    for i in 1 2 3; do\n      echo \"Attempt $i: setting up minikube...\"\n\n      if setup-minikube --k8s-version \"$K8S_VERSION\" --minikube-version \"$MINIKUBE_VERSION\"; then\n        break\n      fi\n\n      echo \"setup-minikube failed on attempt $i\"\n\n      if [ \"$i\" -eq 3 ]; then\n        echo \"setup-minikube failed after 3 attempts\"\n        exit 1\n      fi\n\n      sleep 10\n    done\n\ninstall_helm: &install_helm\n  name: install helm\n  command: |\n    # install helm\n    curl -Lo helm.tar.gz https://get.helm.sh/helm-${HELM_VERSION}-linux-amd64.tar.gz\n    tar -xvf helm.tar.gz\n    chmod +x linux-amd64/helm\n    sudo mv linux-amd64/helm /usr/local/bin/\n\ninstall_gruntwork_utils: &install_gruntwork_utils\n  name: install gruntwork utils\n  command: |\n    curl -Ls https://raw.githubusercontent.com/gruntwork-io/gruntwork-installer/master/bootstrap-gruntwork-installer.sh | bash /dev/stdin --version \"${GRUNTWORK_INSTALLER_VERSION}\"\n    gruntwork-install --module-name \"gruntwork-module-circleci-helpers\" --repo \"https://github.com/gruntwork-io/terraform-aws-ci\" --tag \"${MODULE_CI_CIRCLECI_HELPER_VERSION}\"\n    gruntwork-install --module-name \"kubernetes-circleci-helpers\" --repo \"https://github.com/gruntwork-io/terraform-aws-ci\" --tag \"${MODULE_CI_VERSION}\"\n    gruntwork-install --module-name \"gcp-helpers\" --repo \"https://github.com/gruntwork-io/terraform-google-ci\" --tag \"${MODULE_GCP_CI_VERSION}\"\n    configure-environment-for-gruntwork-module \\\n      --mise-version ${MISE_VERSION} \\\n      --terraform-version ${TERRAFORM_VERSION} \\\n      --terragrunt-version ${TERRAGRUNT_VERSION} \\\n      --packer-version ${PACKER_VERSION} \\\n      --go-version NONE\n\n    # Install OPA\n    echo \"Installing OPA version ${OPA_VERSION}\"\n    curl -sLO \"https://github.com/open-policy-agent/opa/releases/download/${OPA_VERSION}/opa_linux_amd64_static\"\n    chmod +x ./opa_linux_amd64_static\n    sudo mv ./opa_linux_amd64_static /usr/local/bin/opa\n\n    # Temporary fix for installing go - remove when we can update gruntwork-module-circleci-helpers to version with fix\n    GO_VERSION=\"${GO_VERSION:-1.26.0}\"\n    echo \"Installing Go version ${GO_VERSION}\"\n    curl -O --silent --location --fail --show-error --retry 3 \"https://golang.org/dl/go${GO_VERSION}.linux-amd64.tar.gz\"\n    sudo rm -rf /usr/local/go\n    sudo tar -C /usr/local -xzf \"go${GO_VERSION}.linux-amd64.tar.gz\"\n    sudo rm -f /usr/bin/go\n    sudo ln -sf /usr/local/go/bin/go /usr/bin/go\n    echo \"The installed version of Go is now $(go version)\"\n\ninstall_tofu: &install_tofu\n  name: Install OpenTofu\n  command: |\n    curl -L \"https://github.com/opentofu/opentofu/releases/download/v${TOFU_VERSION}/tofu_${TOFU_VERSION}_linux_amd64.zip\" -o tofu.zip\n    unzip -o tofu.zip\n    sudo install -m 0755 tofu /usr/local/bin/tofu\n    rm -rf tofu\n    rm -rf tofu.zip\n    tofu --version\n\ninstall_terragrunt_latest: &install_terragrunt_latest\n  name: Install Terragrunt (latest test version)\n  command: |\n    echo \"Installing Terragrunt ${TERRAGRUNT_TEST_VERSION}...\"\n    curl -sL \"https://github.com/gruntwork-io/terragrunt/releases/download/${TERRAGRUNT_TEST_VERSION}/terragrunt_linux_amd64\" -o /tmp/terragrunt\n    chmod +x /tmp/terragrunt\n    sudo mv /tmp/terragrunt /usr/local/bin/terragrunt\n    terragrunt --version\n\ninstall_docker_buildx: &install_docker_buildx\n  name: install docker buildx\n  command: |\n    curl -sLO https://github.com/docker/buildx/releases/download/v0.6.1/buildx-v0.6.1.linux-amd64\n    mkdir -p ~/.docker/cli-plugins\n    mv buildx-v0.6.1.linux-amd64 ~/.docker/cli-plugins/docker-buildx\n    chmod a+x ~/.docker/cli-plugins/docker-buildx\n\n    # Verify buildx is available\n    docker buildx create --use\n\nconfigure_environment_for_gcp: &configure_environment_for_gcp\n  name: configure environment for gcp\n  command: |\n    # install the Google Cloud SDK CLI\n    install-gcloud\n\n    # Make GCP Service Account credentials available as a file\n    echo $GCLOUD_SERVICE_KEY > ${HOME}/gcloud-service-key.json\n    echo 'export GOOGLE_APPLICATION_CREDENTIALS=${HOME}/gcloud-service-key.json' >> $BASH_ENV\n\n    # Tell gcloud to use the credentials and set defaults\n    echo $GCLOUD_SERVICE_KEY | gcloud auth activate-service-account --key-file=-\n    gcloud --quiet config set project ${GOOGLE_PROJECT_ID}\n    gcloud --quiet config set compute/zone ${GOOGLE_COMPUTE_ZONE}\n\nversion: 2\njobs:\n  setup:\n    <<: *env\n    resource_class: xlarge\n    docker:\n      - image: cimg/python:3.10.2\n\n    steps:\n      - checkout\n      - restore_cache:\n          keys:\n            - gomod-{{ checksum \"go.sum\" }}\n\n      # Install gruntwork utilities\n      - run:\n          <<: *install_gruntwork_utils\n\n      - save_cache:\n          key: gomod-{{ checksum \"go.sum\" }}\n          paths:\n            - $HOME/go/src/\n\n      # The weird way you have to set PATH in Circle 2.0\n      - run: |\n          echo 'export PATH=$HOME/.local/bin:$HOME/go/bin:$HOME/terraform:$HOME/packer:$PATH' >> $BASH_ENV\n\n      # Run pre-commit hooks and fail the build if any hook finds required changes.\n      - run:\n          name: run precommit\n          command: |\n            go install golang.org/x/tools/cmd/goimports@latest\n            # Install the latest minor version for v2\n            pip install pre-commit~=2.9\n            pre-commit install\n            pre-commit run --all-files\n\n      # Build any binaries that need to be built\n      # We always want to build the binaries to test that there are no compile failures. Also, we will use the\n      # terratest_log_parser to parse out the test output during a failure. Finally, on releases, we'll push these\n      # binaries to GitHub as release assets.\n      - run:\n          command: |\n            # For some reason, the circleci environment requires additional module dependencies that are not captured by\n            # our Linux or Mac OSX dev environments. We workaround this by running `go mod tidy` in the CircleCI\n            # environment so it pulls in what it needs.\n            go mod tidy\n\n            go install github.com/mitchellh/gox@latest\n\n            GO_ENABLED=0 build-go-binaries \\\n              --parallel \"$BIN_BUILD_PARALLELISM\" \\\n              --app-name terratest_log_parser \\\n              --src-path ./cmd/terratest_log_parser \\\n              --dest-path ./cmd/bin \\\n              --ld-flags \"-X main.VERSION=$CIRCLE_TAG -extldflags '-static'\"\n\n            GO_ENABLED=0 build-go-binaries \\\n              --parallel \"$BIN_BUILD_PARALLELISM\" \\\n              --app-name pick-instance-type \\\n              --src-path ./cmd/pick-instance-type \\\n              --dest-path ./cmd/bin \\\n              --ld-flags \"-X main.VERSION=$CIRCLE_TAG -extldflags '-static'\"\n          when: always\n\n      - persist_to_workspace:\n          root: /home/circleci\n          paths:\n            - project\n\n  # run tests with terraform binary\n  terraform_test:\n    <<: *defaults\n    resource_class: xlarge\n    steps:\n      - attach_workspace:\n          at: /home/circleci\n\n      - run:\n          <<: *install_gruntwork_utils\n      - run:\n          <<: *install_docker_buildx\n\n      # The weird way you have to set PATH in Circle 2.0\n      - run: |\n          echo 'export PATH=$HOME/.local/bin:$HOME/terraform:$HOME/packer:$PATH' >> $BASH_ENV\n\n      # Run the tests. Note that we set the \"-p 1\" flag to tell Go to run tests in each package sequentially. Without\n      # this, Go buffers all log output until all packages are done, which with slower running tests can cause CircleCI\n      # to kill the build after more than 10 minutes without log output.\n      # NOTE: because this doesn't build with the kubernetes tag, it will not run the kubernetes tests. See\n      # kubernetes_test build steps.\n      # NOTE: terragrunt tests are excluded here and run in a separate terragrunt_test job.\n      - run: mkdir -p /tmp/logs\n      # check we can compile the azure code, but don't actually run the tests\n      - run: run-go-tests --packages \"-p 1 -tags=azure -run IDontExist ./modules/azure\"\n      - run: |\n          # Run only terraform module tests\n          run-go-tests --packages \"-p 1 ./modules/terraform\" | tee /tmp/logs/test_output.log\n\n      - run:\n          command: |\n            ./cmd/bin/terratest_log_parser_linux_amd64 --testlog /tmp/logs/test_output.log --outputdir /tmp/logs\n          when: always\n\n      # Store test result and log artifacts for browsing purposes\n      - store_artifacts:\n          path: /tmp/logs\n      - store_test_results:\n          path: /tmp/logs\n\n  # run tests with tofu binary\n  terraform_test_tofu:\n    <<: *defaults\n    resource_class: large\n    steps:\n      - attach_workspace:\n          at: /home/circleci\n\n      - run:\n          <<: *install_gruntwork_utils\n      - run:\n          <<: *install_docker_buildx\n      - run:\n          <<: *install_tofu\n\n      # The weird way you have to set PATH in Circle 2.0\n      - run: |\n          echo 'export PATH=$HOME/.local/bin:$HOME/terraform:$HOME/packer:$PATH' >> $BASH_ENV\n          # remove terraform binary so tofu will be used\n          sudo rm -f $(which terraform)\n\n      # Run the tests. Note that we set the \"-p 1\" flag to tell Go to run tests in each package sequentially. Without\n      # this, Go buffers all log output until all packages are done, which with slower running tests can cause CircleCI\n      # to kill the build after more than 10 minutes without log output.\n      # NOTE: because this doesn't build with the kubernetes tag, it will not run the kubernetes tests. See\n      # kubernetes_test build steps.\n      # NOTE: terragrunt tests are excluded here and run in a separate terragrunt_test job.\n      - run: mkdir -p /tmp/logs\n      # check we can compile the azure code, but don't actually run the tests\n      - run: run-go-tests --packages \"-p 1 -tags=azure -run IDontExist ./modules/azure\"\n      - run: |\n          # Run only terraform module tests\n          run-go-tests --packages \"-p 1 ./modules/terraform\" | tee /tmp/logs/test_output.log\n\n      - run:\n          command: |\n            ./cmd/bin/terratest_log_parser_linux_amd64 --testlog /tmp/logs/test_output.log --outputdir /tmp/logs\n          when: always\n\n      # Store test result and log artifacts for browsing purposes\n      - store_artifacts:\n          path: /tmp/logs\n      - store_test_results:\n          path: /tmp/logs\n\n  # We run the GCP tests in a separate build step using the Docker executor for better isolation and resiliency. Using\n  # The Docker executor ensures GCP tests do not erroneously make metadata network calls within CircleCI's private\n  # environment. For more information see: https://github.com/gruntwork-io/terratest/pull/765.\n  gcp_test:\n    <<: *env\n    docker:\n      - image: cimg/base:2022.03\n\n    steps:\n      - attach_workspace:\n          at: /home/circleci\n\n      - run:\n          <<: *install_gruntwork_utils\n\n      # The weird way you have to set PATH in Circle 2.0\n      - run: |\n          echo 'export PATH=$HOME/.local/bin:$HOME/terraform:$HOME/packer:$PATH' >> $BASH_ENV\n\n      - run:\n          <<: *configure_environment_for_gcp\n\n      # Run the GCP tests. These tests are run because the gcp build tag is included, and we explicitly\n      # select the GCP tests\n      - run:\n          command: |\n            mkdir -p /tmp/logs\n            # Run the unit tests first, then the integration tests. They are separate because the integration tests\n            # require additional filtering.\n            run-go-tests --packages \"-tags gcp ./modules/gcp\" | tee /tmp/logs/test_output.log\n            run-go-tests --packages \"-tags=gcp ./test/gcp\" | tee /tmp/logs/test_output.log\n\n      - run:\n          command: |\n            ./cmd/bin/terratest_log_parser_linux_amd64 --testlog /tmp/logs/test_output.log --outputdir /tmp/logs\n          when: always\n\n      # Store test result and log artifacts for browsing purposes\n      - store_artifacts:\n          path: /tmp/logs\n      - store_test_results:\n          path: /tmp/logs\n\n  kubernetes_test:\n    <<: *defaults\n    steps:\n      - attach_workspace:\n          at: /home/circleci\n\n      - run:\n          <<: *install_gruntwork_utils\n\n      # The weird way you have to set PATH in Circle 2.0\n      - run: |\n          echo 'export PATH=$HOME/.local/bin:$HOME/terraform:$HOME/packer:$PATH' >> $BASH_ENV\n\n      - run:\n          <<: *setup_minikube\n\n      # Run the Kubernetes tests. These tests are run because the kubernetes build tag is included, and we explicitly\n      # select the kubernetes tests\n      - run:\n          command: |\n            mkdir -p /tmp/logs\n            # Run the unit tests first, then the integration tests. They are separate because the integration tests\n            # require additional filtering.\n            run-go-tests --packages \"-tags kubernetes ./modules/k8s\" | tee /tmp/logs/test_output.log\n            run-go-tests --packages \"-tags kubernetes -run TestKubernetes ./test\" | tee -a /tmp/logs/test_output.log\n\n      - run:\n          command: |\n            ./cmd/bin/terratest_log_parser_linux_amd64 --testlog /tmp/logs/test_output.log --outputdir /tmp/logs\n          when: always\n\n      # Store test result and log artifacts for browsing purposes\n      - store_artifacts:\n          path: /tmp/logs\n      - store_test_results:\n          path: /tmp/logs\n\n\n  helm_test:\n    <<: *defaults\n    resource_class: large\n    steps:\n      - attach_workspace:\n          at: /home/circleci\n\n      - run:\n          <<: *install_gruntwork_utils\n\n      # The weird way you have to set PATH in Circle 2.0\n      - run: |\n          echo 'export PATH=$HOME/.local/bin:$HOME/terraform:$HOME/packer:$PATH' >> $BASH_ENV\n\n      - run:\n          <<: *setup_minikube\n\n      - run:\n          <<: *install_helm\n\n      # Run the Helm tests. These tests are run because the helm build tag is included, and we explicitly\n      # select the helm tests\n      - run:\n          command: |\n            mkdir -p /tmp/logs\n            # Run the unit tests first, then the integration tests. They are separate because the integration tests\n            # require additional filtering.\n            run-go-tests --packages \"-tags helm ./modules/helm\" | tee /tmp/logs/test_output.log\n            run-go-tests --packages \"-tags helm -run TestHelm ./test\" | tee -a /tmp/logs/test_output.log\n\n      - run:\n          command: |\n            ./cmd/bin/terratest_log_parser_linux_amd64 --testlog /tmp/logs/test_output.log --outputdir /tmp/logs\n          when: always\n\n      # Store test result and log artifacts for browsing purposes\n      - store_artifacts:\n          path: /tmp/logs\n      - store_test_results:\n          path: /tmp/logs\n\n  # Dedicated terragrunt tests with terraform as the underlying IaC binary\n  terragrunt_test:\n    <<: *defaults\n    resource_class: large\n    steps:\n      - attach_workspace:\n          at: /home/circleci\n\n      - run:\n          <<: *install_gruntwork_utils\n      - run:\n          <<: *install_terragrunt_latest\n      - run:\n          <<: *install_docker_buildx\n\n      # The weird way you have to set PATH in Circle 2.0\n      - run: |\n          echo 'export PATH=$HOME/.local/bin:$HOME/terraform:$HOME/packer:$PATH' >> $BASH_ENV\n\n      # Run the terragrunt-specific tests. These tests specifically target the terragrunt module\n      # and require terragrunt binary to be available (which is installed via install_gruntwork_utils)\n      - run:\n          command: |\n            mkdir -p /tmp/logs\n            # Run only the terragrunt module tests\n            run-go-tests --packages \"-p 1 ./modules/terragrunt\" | tee /tmp/logs/test_output.log\n\n      - run:\n          command: |\n            ./cmd/bin/terratest_log_parser_linux_amd64 --testlog /tmp/logs/test_output.log --outputdir /tmp/logs\n          when: always\n\n      # Store test result and log artifacts for browsing purposes\n      - store_artifacts:\n          path: /tmp/logs\n      - store_test_results:\n          path: /tmp/logs\n\n  # Dedicated terragrunt tests with tofu as the underlying IaC binary\n  terragrunt_test_tofu:\n    <<: *defaults\n    resource_class: large\n    steps:\n      - attach_workspace:\n          at: /home/circleci\n\n      - run:\n          <<: *install_gruntwork_utils\n      - run:\n          <<: *install_tofu\n      - run:\n          <<: *install_terragrunt_latest\n      - run:\n          <<: *install_docker_buildx\n\n      # The weird way you have to set PATH in Circle 2.0\n      - run: |\n          echo 'export PATH=$HOME/.local/bin:$HOME/terraform:$HOME/packer:$PATH' >> $BASH_ENV\n          # Remove terraform binary so tofu will be used by terragrunt\n          sudo rm -f $(which terraform)\n          # Verify tofu is available\n          which tofu\n          tofu --version\n\n      # Run the terragrunt-specific tests with tofu as the backend\n      - run:\n          command: |\n            mkdir -p /tmp/logs\n            # Run only the terragrunt module tests\n            run-go-tests --packages \"-p 1 ./modules/terragrunt\" | tee /tmp/logs/test_output.log\n\n      - run:\n          command: |\n            ./cmd/bin/terratest_log_parser_linux_amd64 --testlog /tmp/logs/test_output.log --outputdir /tmp/logs\n          when: always\n\n      # Store test result and log artifacts for browsing purposes\n      - store_artifacts:\n          path: /tmp/logs\n      - store_test_results:\n          path: /tmp/logs\n\n  deploy:\n    <<: *defaults\n    steps:\n      - checkout\n      - attach_workspace:\n          at: /home/circleci\n      - run: |\n          curl -Ls https://raw.githubusercontent.com/gruntwork-io/gruntwork-installer/master/bootstrap-gruntwork-installer.sh | bash /dev/stdin --version \"$GRUNTWORK_INSTALLER_VERSION\"\n      - run: |\n          gruntwork-install --module-name \"gruntwork-module-circleci-helpers\" --repo \"https://github.com/gruntwork-io/terraform-aws-ci\" --tag \"$MODULE_CI_VERSION\"\n      - run: cd cmd/bin && sha256sum * > SHA256SUMS\n      - run: upload-github-release-assets cmd/bin/*\n\n\nworkflows:\n  version: 2\n  build-and-test:\n    jobs:\n      - setup:\n          context:\n            - AWS__PHXDEVOPS__circle-ci-test\n            - GITHUB__PAT__gruntwork-ci\n          filters:\n            tags:\n              only: /^v.*/\n\n      - kubernetes_test:\n          context:\n            - AWS__PHXDEVOPS__circle-ci-test\n            - GITHUB__PAT__gruntwork-ci\n          requires:\n            - setup\n          filters:\n            tags:\n              only: /^v.*/\n\n      - helm_test:\n          context:\n            - AWS__PHXDEVOPS__circle-ci-test\n            - GITHUB__PAT__gruntwork-ci\n          requires:\n            - setup\n          filters:\n            tags:\n              only: /^v.*/\n\n      - terragrunt_test:\n          context:\n            - AWS__PHXDEVOPS__circle-ci-test\n            - GITHUB__PAT__gruntwork-ci\n          requires:\n            - setup\n          filters:\n            tags:\n              only: /^v.*/\n\n      - terragrunt_test_tofu:\n          context:\n            - AWS__PHXDEVOPS__circle-ci-test\n            - GITHUB__PAT__gruntwork-ci\n          requires:\n            - setup\n          filters:\n            tags:\n              only: /^v.*/\n\n      - terraform_test:\n          context:\n            - AWS__PHXDEVOPS__circle-ci-test\n            - GITHUB__PAT__gruntwork-ci\n            - SLACK__TOKEN__refarch-deployer-test\n            - SLACK__WEBHOOK__refarch-deployer-test\n            - SLACK__CHANNEL__test-workflow-approvals\n          requires:\n            - setup\n          filters:\n            tags:\n              only: /^v.*/\n\n      - terraform_test_tofu:\n          context:\n            - AWS__PHXDEVOPS__circle-ci-test\n            - GITHUB__PAT__gruntwork-ci\n            - SLACK__TOKEN__refarch-deployer-test\n            - SLACK__WEBHOOK__refarch-deployer-test\n            - SLACK__CHANNEL__test-workflow-approvals\n          requires:\n            - setup\n          filters:\n            tags:\n              only: /^v.*/\n\n      - gcp_test:\n          context:\n            - GCP__automated-tests\n            - GITHUB__PAT__gruntwork-ci\n          requires:\n            - setup\n          filters:\n            tags:\n              only: /^v.*/\n\n      - deploy:\n          context:\n            - AWS__PHXDEVOPS__circle-ci-test\n            - GITHUB__PAT__gruntwork-ci\n          requires:\n            - setup\n          filters:\n            tags:\n              only: /^v.*/\n            branches:\n              ignore: /.*/\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\ngithub: gruntwork-io\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a bug report to help us improve Terratest.\ntitle: ''\nlabels: bug\nassignees: ''\n\n---\n\n**Describe the bug**\nA clear and concise description of what the bug is.\n\n**To Reproduce**\nSteps to reproduce the behavior, code snippets and examples which can be used to reproduce the issue.\n\n```go\n// paste code snippets here\n```\n\n**Expected behavior**\nA clear and concise description of what you expected to happen.\n\n**Nice to have**\n- [ ] Terminal output\n- [ ] Screenshots\n\n**Versions**\n- Terratest version:\n- Environment details (Ubuntu 20.04, Windows 10, etc.):\n\n**Additional context**\nAdd any other context about the problem here."
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Submit a feature request to improve Terratest.\ntitle: ''\nlabels: enhancement\nassignees: ''\n\n---\n\n**Describe the solution you'd like**\nA clear and concise description of what you want to happen.\n\n**Describe alternatives you've considered**\nA clear and concise description of any alternative solutions or features you've considered.\n\n**Additional context**\nAdd any other context or screenshots about the feature request here.\n"
  },
  {
    "path": ".github/no-response.yml",
    "content": "# Configuration for probot-no-response - https://github.com/probot/no-response\n\n# Number of days of inactivity before an Issue is closed for lack of response\ndaysUntilClose: 30\n# Label requiring a response\nresponseRequiredLabel: more-information-needed\n# Comment to post when closing an Issue for lack of response. Set to `false` to disable\ncloseComment: >\n  This issue has been automatically closed because there has been no response\n  to our request for more information from the original author. With only the\n  information that is currently in the issue, we don't have enough information\n  to take action. Please feel free to reach out if you have or find the answers we need so\n  that we can investigate further. Thank you!"
  },
  {
    "path": ".github/pull_request_template.md",
    "content": "<!-- Prepend '[WIP]' to the title if this PR is still a work-in-progress. Remove it when it is ready for review! -->\n\n## Description\n\nFixes #000.\n\n<!-- Description of the changes introduced by this PR. -->\n\n## TODOs\n\nRead the [Gruntwork contribution guidelines](https://gruntwork.notion.site/Gruntwork-Coding-Methodology-02fdcd6e4b004e818553684760bf691e).\n\n- [ ] Update the docs.\n- [ ] Run the relevant tests successfully, including pre-commit checks.\n- [ ] Ensure any 3rd party code adheres with our [license policy](https://www.notion.so/gruntwork/Gruntwork-licenses-and-open-source-usage-policy-f7dece1f780341c7b69c1763f22b1378) or delete this line if its not applicable.\n- [ ] Include release notes. If this PR is backward incompatible, include a migration guide.\n- [ ] Make a plan for release of the functionality in this PR. If it delivers value to an end user, you are responsible for ensuring it is released promptly, and correctly. If you are not a maintainer, you are responsible for finding a maintainer to do this for you.\n\n## Release Notes (draft)\n\n<!-- One-line description of the PR that can be included in the final release notes. -->\nAdded / Removed / Updated [X].\n\n### Migration Guide\n\n<!-- Important: If you made any backward incompatible changes, then you must write a migration guide! -->\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: ci-workflow\n\n# actors\n  # source repo: official terratest repo (gruntwork-io/terratest)\n  # forked repo: (e.g., xyz/terratest, abc/terratest)\n  # pr: created from forked repo, against source repo (gruntwork-io/terratest/pull/{PR NUMBER})\n# flow\n  # developer fork the source repo\n  # developer creates a branch in the forked repo\n  # developer does the development 😎\n  # developer creates a pr\n  # webhook on source repo calls a webapp when the pr has been created\n  # this pipeline will be triggered automatically by the webapp, with the following input values\n    # repo: name of the forked repo (e.g. xyz/terratest)\n    # branch: branch name on the forked repo (e.g. feature/adding-some-important-module)\n    # target_repository: home of the target_pr, which is the source repo (gruntwork-io/terratest)\n    # target_pr: pr number on the source repo (e.g. 14, 25, etc.)\n\non:\n  push:\n    branches: master\n  workflow_dispatch:\n    inputs:\n      repo:\n        description: 'Repository info'\n        required: true\n      branch:\n        description: 'Name of the branch'\n        required: true\n      target_repository:\n        description: 'Name of the official terratest repo'\n        required: false\n        default: 'gruntwork-io/terratest'\n      target_pr:\n        description: 'PR number on the official terratest repo'\n        required: false\n      skip_provider_registration:\n        description: 'When set to true, terraform will skip provider registration (see: https://www.terraform.io/docs/providers/azurerm/index.html#skip_provider_registration for more information)'\n        required: true\n        default: 'false'\n\npermissions:\n  contents: read\n\njobs:\n  ci-job:\n    runs-on: [ubuntu-latest]\n    steps:\n      - uses: hashicorp/setup-terraform@v1\n        with:\n          terraform_version: 0.15.1\n          terraform_wrapper: false  \n      - name: checkout to repo\n        uses: actions/checkout@v2\n        with:\n          repository: ${{ github.event.inputs.repo }}\n          ref: ${{ github.event.inputs.branch }}\n      - name: install golangci-lint binary\n        run: |\n          curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b ./bin v1.53.2\n      - name: lint modules/azure folder\n        id: azure_module_lint\n        run: |\n          # list files to be linted\n          [ -d \"modules/azure\" ] && ls -li \"modules/azure\"\n\n          # run the linter\n          ./bin/golangci-lint run ./modules/azure/ --build-tags=azure --timeout 5m0s\n      - name: lint test/azure folder\n        id: azure_test_lint\n        run: |\n          # list files to be linted\n          [ -d \"test/azure\" ] && ls -li \"test/azure\"\n\n          # run the linter\n          ./bin/golangci-lint run ./test/azure/ --build-tags=azure --timeout 5m0s\n      - name: run terraform format\n        id: azure_terraform_format\n        run: terraform fmt -check -recursive ./examples/azure\n      - name: login to azure cli\n        uses: azure/login@v1.1\n        with:\n          creds: ${{ secrets.AZURE_CREDENTIALS }}\n      - name: run go unit test for azure\n        id: azure_unit_test\n        env:\n          AZURE_CREDENTIALS: ${{ secrets.AZURE_CREDENTIALS }}\n        run: |\n          cd modules\n\n          APP_ID=`echo $AZURE_CREDENTIALS | jq -r -c \".clientId\"`\n          APP_PASSWORD=`echo $AZURE_CREDENTIALS | jq -r -c \".clientSecret\"`\n          TENANT_ID=`echo $AZURE_CREDENTIALS | jq -r -c \".tenantId\"`\n\n          # if clientId, subscriptionId, tenantId doesn't provide to the go tests\n          # by default, terratest reads them from below environment variables\n          export ARM_CLIENT_ID=\"$APP_ID\"\n          export ARM_CLIENT_SECRET=\"$APP_PASSWORD\"\n          export ARM_SUBSCRIPTION_ID=`az account show --query \"id\" --output tsv`\n          export ARM_TENANT_ID=\"$TENANT_ID\"\n          export ARM_SKIP_PROVIDER_REGISTRATION=${{ github.event.inputs.skip_provider_registration }}\n\n          # run the unit tests under the `azure` subfolder\n          go test ./azure/* -v -timeout 90m\n      - name: run go test for azure\n        id: azure_test\n        env:\n          AZURE_CREDENTIALS: ${{ secrets.AZURE_CREDENTIALS }}\n        run: |\n          cd test/azure\n\n          APP_ID=`echo $AZURE_CREDENTIALS | jq -r -c \".clientId\"`\n          APP_PASSWORD=`echo $AZURE_CREDENTIALS | jq -r -c \".clientSecret\"`\n          TENANT_ID=`echo $AZURE_CREDENTIALS | jq -r -c \".tenantId\"`\n\n          # if clientId, subscriptionId, tenantId doesn't provide to the go tests\n          # by default, terratest reads them from below environment variables\n          export ARM_CLIENT_ID=\"$APP_ID\"\n          export ARM_CLIENT_SECRET=\"$APP_PASSWORD\"\n          export ARM_SUBSCRIPTION_ID=`az account show --query \"id\" --output tsv`\n          export ARM_TENANT_ID=\"$TENANT_ID\"\n          export ARM_SKIP_PROVIDER_REGISTRATION=${{ github.event.inputs.skip_provider_registration }}\n\n          # some resources may require ssh keys (e.g. Kubernetes, VMs, etc.)\n          # terraform will read below environment variables\n          # if those values didn't provide to the terraform explicitly\n          rm -rf ssh_key*\n          ssh-keygen -m PEM -t rsa -b 4096 -f ./ssh_key -q -N \"\"\n          export TF_VAR_ssh_public_key=\"$PWD/ssh_key.pub\"\n          export TF_VAR_client_id=\"$APP_ID\"\n          export TF_VAR_client_secret=\"$APP_PASSWORD\"\n\n          # run the actual tests under the `azure` subfolder\n          go test --tags=azure -v -timeout 90m\n      - name: report back the result\n        if: always()\n        env:\n          CURRENT_REPOSITORY: ${{ github.repository }}\n          RUN_ID: ${{ github.run_id }}\n          GITHUB_TOKEN: ${{ secrets.PAT }}\n          TARGET_REPOSITORY: ${{ github.event.inputs.target_repository }}\n          TARGET_PR: ${{ github.event.inputs.target_pr }}\n          TEST_RESULT: ${{ steps.azure_test.conclusion }}\n          TEST_LINT_RESULT: ${{ steps.azure_test_lint.conclusion }}\n          MODULE_LINT_RESULT: ${{ steps.azure_module_lint.conclusion }}\n        run: |\n          # if no PR number provided, simply exit...\n          [[ -z \"$TARGET_PR\" ]] && { echo \"No PR Number provided, exiting...\" ; exit 0; }\n\n          # if no PAT provided, simply exit...\n          [[ -z \"$GITHUB_TOKEN\" ]] && { echo \"No PAT provided, exiting...\" ; exit 0; }\n\n          echo \"PR Number provided... ${TARGET_REPOSITORY}:#${TARGET_PR}\"\n\n          BODY_PAYLOAD=\"\"\n          # if all the previous steps finished successfully, create a comment on the PR with the \"success\" information\n          if [ \"$TEST_RESULT\" == \"success\" ] && [ \"$TEST_LINT_RESULT\" == \"success\" ] && [ \"$MODULE_LINT_RESULT\" == \"success\" ]; then\n            BODY_PAYLOAD=\"[Microsoft CI Bot] TL;DR; success :thumbsup:\\n\\nYou can check the status of the CI Pipeline logs here ; https://github.com/${CURRENT_REPOSITORY}/actions/runs/$RUN_ID\"\n          # if at least one of the previous steps failed, create a comment on the PR with the \"failure\" information\n          elif [ \"$TEST_RESULT\" == \"failure\" ] || [ \"$TEST_LINT_RESULT\" == \"failure\" ] || [ \"$MODULE_LINT_RESULT\" == \"failure\" ]; then\n            BODY_PAYLOAD=\"[Microsoft CI Bot] TL;DR; failure :facepalm:\\n\\nYou can check the status of the CI Pipeline logs here ; https://github.com/${CURRENT_REPOSITORY}/actions/runs/$RUN_ID\"\n          fi\n\n          echo \"Comment message is ready...\"\n          echo \"${BODY_PAYLOAD}\"\n\n          # if pipeline has something to report back to the PR\n\n          if [[ -z \"$BODY_PAYLOAD\" ]]\n          then\n            echo \"BODY_PAYLOAD is empty\"\n          else\n            echo \"Here is the target repository: ${TARGET_REPOSITORY}\"\n            # TARGET_REPOSITORY is in {owner}/{repo format}\n            [[ $TARGET_REPOSITORY =~ (.*)/(.*)$ ]]\n            # take the {owner} piece from the TARGET_REPOSITORY variable\n            TARGET_REPO_OWNER=${BASH_REMATCH[1]}\n            # take the {repo} piece from the TARGET_REPOSITORY variable\n            TARGET_REPO_NAME=${BASH_REMATCH[2]}\n\n            echo \"Target repository is parsed: ${TARGET_REPO_OWNER} <-> ${TARGET_REPO_NAME}\"\n\n            # create the query string to get the pr id\n            QUERY_PR_ID=\"query findPRID { repository(owner: \\\\\\\"$TARGET_REPO_OWNER\\\\\\\", name: \\\\\\\"$TARGET_REPO_NAME\\\\\\\") { pullRequest(number: $TARGET_PR) { id } } }\"\n\n            # get the pr id from github api\n            PR_ID=$(curl --silent --request POST --header \"Authorization: Bearer ${GITHUB_TOKEN}\" --data-raw \"{\\\"query\\\":\\\"${QUERY_PR_ID}\\\"}\" \"https://api.github.com/graphql\" | jq -r '.data.repository.pullRequest.id')\n\n            echo \"Target PR ID is ${PR_ID}\"\n\n            # create the mutation string to create the comment on the pr\n            MUTATION_ADD_COMMENT=\"mutation addComment { addComment(input: {subjectId: \\\\\\\"${PR_ID}\\\\\\\", body: \\\\\\\"${BODY_PAYLOAD}\\\\\\\"}) { commentEdge { node { createdAt body } } subject { id } } }\"\n\n            # call the github api to create the comment\n            curl --request POST --header \"Authorization: Bearer ${GITHUB_TOKEN}\" --data-raw \"{\\\"query\\\":\\\"${MUTATION_ADD_COMMENT}\\\"}\" \"https://api.github.com/graphql\"\n\n            echo \"Comment is created...\"\n          fi\n"
  },
  {
    "path": ".github/workflows/lint.yml",
    "content": "name: Lint\n\non:\n  push:\n\npermissions:\n  contents: read\n\njobs:\n  lint:\n    name: Lint\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6\n\n    - name: Set up mise\n      uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3\n      env:\n        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n\n    - id: go-cache-paths\n      shell: bash\n      run: |\n        echo \"go-build=$(go env GOCACHE)\" >> \"$GITHUB_OUTPUT\"\n        echo \"go-mod=$(go env GOMODCACHE)\" >> \"$GITHUB_OUTPUT\"\n        echo \"golangci-lint-cache=$HOME/.cache/golangci-lint\" >> \"$GITHUB_OUTPUT\"\n\n    - name: Go Build Cache\n      uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5\n      with:\n        path: ${{ steps.go-cache-paths.outputs.go-build }}\n        key: ${{ runner.os }}-go-build-lint-${{ hashFiles('**/go.sum') }}\n        restore-keys: |\n          ${{ runner.os }}-go-build-lint-\n\n    - name: Go Mod Cache\n      uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5\n      with:\n        path: ${{ steps.go-cache-paths.outputs.go-mod }}\n        key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }}\n        restore-keys: |\n          ${{ runner.os }}-go-mod-\n\n    - name: golangci-lint Cache\n      uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5\n      with:\n        path: ${{ steps.go-cache-paths.outputs.golangci-lint-cache }}\n        key: ${{ runner.os }}-golangci-lint-${{ hashFiles('**/go.sum') }}\n        restore-keys: |\n          ${{ runner.os }}-golangci-lint-\n\n    - name: Lint\n      run: make lint-allow-list\n"
  },
  {
    "path": ".github/workflows/update-lint-config.yml",
    "content": "name: Update Lint Config\n\non:\n  schedule:\n    # Run every Monday at 00:00 UTC\n    - cron: '0 0 * * 1'\n  workflow_dispatch:\n\njobs:\n  update-lint-config:\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/checkout@v5\n\n    - name: Update lint config from upstream\n      run: make update-lint-config\n\n    - name: Check for changes\n      id: check\n      run: |\n        if git diff --quiet .golangci.yml; then\n          echo \"changed=false\" >> \"$GITHUB_OUTPUT\"\n        else\n          echo \"changed=true\" >> \"$GITHUB_OUTPUT\"\n        fi\n\n    - name: Create Pull Request\n      if: steps.check.outputs.changed == 'true'\n      uses: peter-evans/create-pull-request@v7\n      with:\n        token: ${{ secrets.GITHUB_TOKEN }}\n        commit-message: \"chore: update golangci-lint config from upstream\"\n        title: \"chore: update golangci-lint config from upstream\"\n        body: |\n          This PR updates the golangci-lint configuration from the upstream Terragrunt repository.\n\n          Source: https://github.com/gruntwork-io/terragrunt/blob/main/.golangci.yml\n\n          This is an automated PR created by the update-lint-config workflow.\n        branch: update-lint-config\n        delete-branch: true\n"
  },
  {
    "path": ".gitignore",
    "content": "# Terraform files\n.terraform\nterraform.tfstate\nterraform.tfvars\nterraform.tfvars.json\n*.tfstate*\n.terragrunt\n.terragrunt-cache\n.terraform.lock.hcl\n# IDE files\n.idea\n.vscode\n*.iml\nvendor\n\n# Folder used to store temporary test data by Terratest\n.test-data\n\n# rbenv\n.ruby-version\n\n# OS X\n.DS_Store\n# Intermediate file for testing\nkubeconfig\n\n# environment files\n.env\n"
  },
  {
    "path": ".golangci.yml",
    "content": "# This file is generated from https://github.com/gruntwork-io/terragrunt/blob/main/.golangci.yml\n# It is automatically updated weekly via the update-lint-config workflow. Do not edit manually.\nversion: \"2\"\nrun:\n  go: \"1.26\"\n  issues-exit-code: 1\n  tests: true\noutput:\n  formats:\n    text:\n      path: stdout\n      print-linter-name: true\n      print-issued-lines: true\nlinters:\n  enable:\n    - asasalint\n    - asciicheck\n    - bidichk\n    - bodyclose\n    - contextcheck\n    - dupl\n    - durationcheck\n    - errchkjson\n    - errorlint\n    - exhaustive\n    - fatcontext\n    - gocheckcompilerdirectives\n    - gochecksumtype\n    - goconst\n    - gocritic\n    - gosmopolitan\n    - lll\n    - loggercheck\n    - makezero\n    - misspell\n    - mnd\n    - musttag\n    - nilerr\n    - nilnesserr\n    - noctx\n    - paralleltest\n    - perfsprint\n    - prealloc\n    - protogetter\n    - reassign\n    - rowserrcheck\n    - spancheck\n    - sqlclosecheck\n    - staticcheck\n    - testableexamples\n    - testifylint\n    - testpackage\n    - thelper\n    - tparallel\n    - unconvert\n    - unparam\n    - usetesting\n    - wastedassign\n    - wsl_v5\n    - zerologlint\n  settings:\n    dupl:\n      threshold: 120\n    errcheck:\n      check-type-assertions: false\n      check-blank: false\n      exclude-functions:\n        - (*os.File).Close\n    errorlint:\n      errorf: true\n      asserts: true\n      comparison: true\n    goconst:\n      min-len: 3\n      min-occurrences: 5\n    gocritic:\n      enabled-tags:\n        - performance\n      disabled-tags:\n        - experimental\n    govet:\n      enable:\n        - fieldalignment\n        - printf\n        - unusedwrite\n    nakedret:\n      max-func-lines: 20\n    staticcheck:\n      checks:\n        - all\n        - -SA9005\n        - -QF1008\n        - -ST1001\n    unparam:\n      check-exported: false\n    wsl_v5:\n      allow-whole-block: false\n      branch-max-lines: 2\n  exclusions:\n    generated: lax\n    rules:\n      - linters:\n          - dupl\n          - errcheck\n          - gocyclo\n          - mnd\n          - unparam\n          - wsl\n        path: _test\\.go\n      # We end up with duplicated content in this package to save us from duplicating code in other packages.\n      - linters:\n          - dupl\n        path: cli/flags/shared\n      # Incrementally linting lines that are too long to ensure that\n      # we don't have conflicts on every file in the codebase while\n      # trying to get this merged in.\n      - linters:\n          - lll\n        path-except: '^(internal/awshelper/|internal/cas/)'\n    paths:\n      - docs\n      - _ci\n      - .github\n      - .circleci\n      - third_party$\n      - builtin$\n      - examples$\nissues:\n  max-issues-per-linter: 0\n  max-same-issues: 0\nformatters:\n  enable:\n    - goimports\n  settings:\n    gofmt:\n      simplify: true\n  exclusions:\n    generated: lax\n    paths:\n      - docs\n      - _ci\n      - .github\n      - .circleci\n      - third_party$\n      - builtin$\n      - examples$\n"
  },
  {
    "path": ".pre-commit-config.yaml",
    "content": "repos:\n- repo: https://github.com/gruntwork-io/pre-commit\n  rev: v0.1.10\n  hooks:\n    - id: goimports\n    - id: terraform-fmt\n- repo: local\n  hooks:\n    - id: test-interfaces-used\n      name: test-interfaces-used\n      entry: bash -c 'grep -Rw \"*testing.T\" modules | grep -v _test.go | wc -l'\n      language: system\n      types: [go]\n      pass_filenames: false\n"
  },
  {
    "path": "CODEOWNERS",
    "content": "* @denis256 @yhakbar @thisguycodes @james00012\n"
  },
  {
    "path": "LICENSE",
    "content": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License."
  },
  {
    "path": "Makefile",
    "content": "update-lint-config: SHELL:=/bin/bash\nupdate-lint-config:\n\tcurl -s https://raw.githubusercontent.com/gruntwork-io/terragrunt/main/.golangci.yml --output .golangci.yml\n\ttmpfile=$$(mktemp) ;\\\n\t{ echo '# This file is generated from https://github.com/gruntwork-io/terragrunt/blob/main/.golangci.yml' ;\\\n\t  echo '# It is automatically updated weekly via the update-lint-config workflow. Do not edit manually.' ;\\\n\t  cat .golangci.yml; } > $${tmpfile} && mv $${tmpfile} .golangci.yml\n\nlint:\n\tgolangci-lint run ./...\n\nlint-allow-list:\n\tgolangci-lint run ./modules/random/... ./modules/testing/... ./modules/slack/... ./modules/collections/... ./modules/environment/... ./modules/retry/... ./modules/shell/... ./modules/git/... ./modules/files/...\n\n.PHONY: lint update-lint-config\n"
  },
  {
    "path": "NOTICE",
    "content": "terratest\nCopyright 2018 Gruntwork, Inc.\n\nThis product includes software developed at Gruntwork (https://www.gruntwork.io/)."
  },
  {
    "path": "README.md",
    "content": "# Terratest\n\n[![Maintained by Gruntwork.io](https://img.shields.io/badge/maintained%20by-gruntwork.io-%235849a6.svg)](https://gruntwork.io/?ref=repo_terratest)\n[![CircleCI](https://dl.circleci.com/status-badge/img/gh/gruntwork-io/terratest/tree/main.svg?style=svg&circle-token=8abd167739d60e4c1b6c1502d2092339a6c6a133)](https://dl.circleci.com/status-badge/redirect/gh/gruntwork-io/terratest/tree/main)\n[![Go Report Card](https://goreportcard.com/badge/github.com/gruntwork-io/terratest)](https://goreportcard.com/report/github.com/gruntwork-io/terratest)\n[![go.dev reference](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white&style=flat-square)](https://pkg.go.dev/mod/github.com/gruntwork-io/terratest?tab=overview)\n![go.mod version](https://img.shields.io/github/go-mod/go-version/gruntwork-io/terratest)\n\nTerratest is a Go library that makes it easier to write automated tests for your infrastructure code. It provides a\nvariety of helper functions and patterns for common infrastructure testing tasks, including:\n\n- Testing Terraform code\n- Testing Packer templates\n- Testing Docker images\n- Executing commands on servers over SSH\n- Working with AWS APIs\n- Working with Azure APIs\n- Working with GCP APIs\n- Working with Kubernetes APIs\n- Testing Helm Charts\n- Making HTTP requests\n- Running shell commands\n- And much more\n\nPlease see the following for more info:\n\n- [Terratest Website](https://terratest.gruntwork.io)\n- [Getting started with Terratest](https://terratest.gruntwork.io/docs/getting-started/quick-start/)\n- [Terratest Documentation](https://terratest.gruntwork.io/docs/)\n- [Contributing to Terratest](https://terratest.gruntwork.io/docs/community/contributing/)\n- [Commercial Support](https://gruntwork.io/support/)\n\n## License\n\nThis code is released under the Apache 2.0 License. Please see [LICENSE](LICENSE) and [NOTICE](NOTICE) for more details.\n\nCopyright &copy; 2025 Gruntwork, Inc.\n"
  },
  {
    "path": "REFACTOR.md",
    "content": "# Terratest refactor\n\nTerratest started out as a set of Bash scripts we were using at Gruntwork to test some of our Terraform code. As the\namount of Terraform code grew, it was getting trickier and trickier to test it with Bash, so we rewrote those scripts\nin Go. Over time, this Go code grew into a library called Terratest, which contains collection of utilities that we\nuse to test all aspects of our [Infrastructure as Code Library](https://gruntwork.io/infrastructure-as-code-library/).\n\nWe developed patterns to test Terraform configurations, Packer templates, Docker images, SSH access, AWS APIs, shell\ncommands, and much more. We built this library because we couldn't find any existing tools out there that could do\nthe type of real-world testing we needed. It turns out many other companies want to do this type of testing too, so\nnow it's time to open source Terratest.\n\nThis library grew organically, so it needs lots of refactoring, cleanup, and documentation to be useful to people\noutside of Gruntwork. This document lays out the refactoring we are planning to (a) get feedback and (b) document what\nchanged so that when we update our code, we know how to deal with the backwards incompatibilities.\n\n\n\n\n## Name change\n\n\"Terratest\" made sense as the name for this library when it was all about testing Terraform code, but now this library\nalso can help you test Packer templates, Docker images, and much more. I propose that we rename it. Some ideas:\n\n- grunt-test\n- test-grunt\n- gruntUnit\n- iac-test\n- infratest\n\n\n\n\n## Build updates\n\n1. Move from Glide to Dep\n1. Move from CircleCI 1.0 to 2.0\n1. Add support for Google Cloud Platform\n1. Move from Dep to Go Modules.\n\n\n## Folder structure\n\nChange to the same folder structure we use for just about all other Gruntwork repos:\n\n- `examples`: This will contain a number of real-world examples of code you might want to test with Terratest, such as\n  Terraform modules, Packer templates, and Docker images. The `test` folder (described below) shows how to use\n  Terratest to test these examples.\n\n- `modules`: The Terratest source code. Move all the `.go` files and packages into this folder so it's easier to browse\n  the repo. That does mean all Terratest imports will have to be updated to\n  `github.com/gruntwork-io/terratest/modules/xxx`. Unit tests for the Go source code will be in this folder too (e.g., the\n  unit test for `foo.go` will be in `foo_test.go`).\n\n- `test`: This will contain the automated tests for the examples in the `examples` folder. These will act both as an\n  example of how to use Terratest, as well as integration tests for the library.\n\n\n\n\n## Documentation\n\nUpdate the root `README.md` with documentation that shows how to use Terratest:\n\n- Overview of what Terratest is.\n- Link to blog post we'll write about Terratest (this blog post is a TODO for after this refactor).\n- Discuss some of the challenges of testing infrastructure code: i.e., lack of \"localhost,\" lock of \"unit tests,\"\n  slowness, brittleness.\n- Discuss the value of doing this testing despite the challenges: i.e., there is no way to maintain lots of\n  infrastructure code without tests, building reusable, tested, versioned modules changes how you manage\n  infrastructure.\n- Discussion of test strategies: using Docker for local testing, test stages, retries, mocks, small modules, test\n  pyramid, cleanup, `cloud-nuke`.\n- Point to `examples` folder for real-world code you may want to test and `test` folder for examples of how to use\n  Terratest to test that code.\n- Overview of Terratest packages. Explain what each top-level package in Terratest does. We can't do a method-by-method\n  breakdown, as that would go out of date immediately, so instead, link to the appropriate `examples` subfolder that\n  shows real-world usage of that package.\n- In the future: links to our open source repos (Vault, Consul, Nomad, Couchbase, etc) that show how we use Terratest\n  with our own code. We can't add this until we update those open source repos to this refactored version of Terratest\n  so the code matches up.\n\n\n\n\n## Package-by-package refactor\n\nI've gone through each of the packages in Terratest and took down some notes on cleanup we need to do. This is not a\ncomprehensive list, as things will become clearer once I actually start doing the work.\n\nIn fact, my plan is to first create all the examples in the `examples` folder, then write tests for them in the `test`\nfolder using \"wishful thinking\" (in the\n[SICP](https://www.amazon.com/Structure-Interpretation-Computer-Programs-Engineering/dp/0262510871) sense), where I\ncome up with the test API I want to have for doing the testing, and then go and refactor the Terratest code to match.\n\n\n### Root package\n\nWe have a lot of stuff in the root package and I propose moving all of it out into appropriate sub-packages:\n\n- `apply.go`, `apply_and_destroy.go`, `destroy.go`, `output.go`, and `output_test.go` will all be moved into\n  `modules/terraform`, as they are all specific to testing Terraform code.\n\n- I propose deleting `rand_resources.go` and `rand_resource_test.go` and extracting its logic into other places. The\n  `RandomResourceCollection` ended up being a, well, random collection of resources, most of which don't apply to most\n  of our tests, and certainly won't apply to tests written by the open source community. Here's what\n  `RandomResourceCollection` contains and what I propose to do with it:\n\n    - `UniqueId`: We already have a separate method for generating a unique ID and we can pass it around as a `string`.\n\n    - `AwsRegion`: This is only needed for AWS tests. We want to expand Terratest to support other clouds, so it needs\n      to be separated anyway. Code that needs an AWS region should call a method in the `modules/aws` package to pick a\n      random AWS region (passing in a list of forbidden regions, if necessary) and can pass that around as a `string`.\n\n    - `KeyPair`: This is only needed for a small percentage of our AWS tests that deploy EC2 Instances and SSH to them.\n      Those tests should call a standalone method in the `modules/aws` package to generate this `KeyPair` when they need it,\n      instead of us assuming every single test needs it.\n\n    - `AmiId`: We used to look up vanilla Ubuntu or Amazon Linux AMI IDs and put them in this field, but now that\n      Terraform has `data` sources and Packer has `source_ami_filter`, this is no longer necessary. We can keep the\n      methods around to find Ubuntu or Amazon Linux AMI IDs for tests that need them, but there's no need to assume\n      every single test needs this.\n\n    - `AccountId`: Our Terraform examples used to require an account ID to be passed in. We now avoid this to make the\n      examples easier to use, and fetch it automatically using Terraform's `aws_caller_identity` data source if it's\n      absolutely necessary. Code that needs an account ID should call a method in the `modules/aws` package to fetch it,\n      but we shouldn't assume every single test needs it.\n\n    - `SnsTopicArn`: A very, very small percentage of our tests needed an SNS topic passed in. Those tests should call\n      a method in the `modules/aws` package to create this topic instead of us assuming every single test needs it.\n\n- I propose moving `terratest_options.go` to `modules/terraform/options.go` and renaming the struct within it from\n  `TerratestOptions` to `Options`, since this is solely used for testing Terraform code. We should also rename\n  `TemplatePath` to `TerraformDir`, as `.tf` files are technically called \"configurations\" and not \"templates\".\n\n- `url_checker.go` will be deleted. It's too hard-coded for one specific type of check. The reuse value is limited and\n  it's not obvious the code exists, so it's best for the test cases to reimplement this themselves, with their specific\n  needs, even if it's a tiny bit less DRY.\n\n\n### _docker-images package\n\n- Rename to `test-docker-images` to make it clearer these are only used for testing.\n- Use these Docker images in the `examples` folder to show how to do \"unit tests\" for Packer templates.\n- Follow-up PR: build and push a new version of these Docker images on each release?\n- Follow-up PR: tag each new Docker image with a unique version number (e.g., sha1 of commit).\n\n\n### `aws` package\n\n- Right now, much of this code has no unit tests, since it relies on resources in AWS. By adding an `examples` folder\n  that deploys real resources in AWS, we will be able to test this code better, _and_ show users how to use this code!\n\n- `ami.go`: Update these methods to use the AWS APIs to find the latest Ubuntu / Amazon Linux AMI IDs instead of\n  hard-coding them.\n\n- `kms.go`: What to do about `GetDedicatedTestKeyArn`? For tests that use KMS, we don't want to create a new CMK each\n  time the test runs, as AWS charges $1/month for CMKs, even if you delete them immediately after use. This method\n  currently assumes we have a key called `alias/dedicated-test-key` in every AWS region. Should we leave it as-is and\n  document it for Terratest users that want to follow a similar pattern? Or perhaps read the key name from an env var?\n\n- `region.go`: What should we do about `GetGloballyForbiddenRegions`? Right now, it's hard-coded to include `us-west-2`\n  as a globally forbidden region, as Josh is running his personal blog there. Obviously, we don't want that in the open\n  source version. Josh, can you finally migrate your blog out of there so we don't have to have this exception?\n\n\n### `log` package\n\n- Rename to `logger`. That way, we don't have to alias it as `terralog` all over our test code.\n- Change what the package does. Instead of creating a custom `*log.Logger` and passing it around, we are going to have\n  a `Log` and `Logf` method you can call from anywhere. To use those methods, you have to pass them a `*testing.T`,\n  which they will use to read out the test name. We already pass `*testing.T` to almost all of our test methods, so\n  this reduces the number of arguments by one.\n\n\n### `parallel` package\n\n- I propose removing this package entirely. Now that go has [subtests](https://blog.golang.org/subtests) that you can\n  easily run with `t.Run()` and parallelize with `t.Parallel()`, I think that's a cleaner way of handling parallelism\n  than this custom package.\n\n\n### `packer` package\n\n- Rename `PackerOptions` to `Options` (the package name is already `packer`).\n\n\n### `resources` package\n\n- `base_resources.go` is no longer necessary if we remove `RandomResourceCollection`.\n- `exclusions.go` is not used much and very out of date.\n- `terraform_options.go` is hard-coded to how we do things at Gruntwork, but won't apply to many other users.\n\n\n### `terraform` package\n\n- `apply.go`: Remove `terraformDebugEnv` and instead make it easy to pass a map of env vars to the `Apply` method.\n  Refactor `ApplyAndGetOutputWithRetry` to accept a list of errors on which to retry and how many retries to do.\n\n\n### `test-util` package\n\n- `dummy_server.go`: Move into the `modules/http` package.\n- Remove `test-util` since that would leave it empty!\n\n\n### `util` package\n\n- `collections.go`: Move into its own `modules/collections` package.\n- `keygen.go`: Move into `modules/ssh` package.\n- `network.go`: Move into `modules/aws` package.\n- `sleep.go`: Remove. Didn't even know we had this and doubt it gets much use!\n- `random.go`: Move into its own `modules/random` package.\n- `retry.go`: Move into its own `modules/retry` package.\n\n\n\n\n## Error handling\n\nI am updating most of the methods to support handling errors in one of two ways:\n\n1. Each method `foo` will take in a `*testing.T` and upon hitting an error, call `t.Fatal`.\n1. Each method `fooE` will explicitly return any errors it hits and NOT call `t.Fatal`.\n\nExample:\n\n```go\nfunc GetCurrentBranchName(t *testing.T) string {\n\tout, err := GetCurrentBranchNameE(t)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn out\n}\n\nfunc GetCurrentBranchNameE(t *testing.T) (string, error) {\n\tcmd := exec.Command(\"git\", \"rev-parse\", \"--abbrev-ref\", \"HEAD\")\n\tbytes, err := cmd.Output()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn strings.TrimSpace(string(bytes)), nil\n}\n```\n\nIn most places in our code, we will use `GetCurrentBranchName`, which will call `t.Fatal` if it hits any errors. This\nis typically the behavior we want anyway, and not having to deal with a returned error will keep our code smaller and\neasier to read. However, in those cases where we may want to get the original error back and not fail the test\nimmediately, we can use `GetCurrentBranchNameE`.\n\n\n\n\n## Other thoughts on the refactor\n\n- Updating to the refactored version of Terratest will be a pain that requires lots of search & replace. But in the\n  long term, it seems like worthwhile cleanup.\n\n- There are a bunch of patterns we often end up using throughout our tests that would be good to copy into Terratest.\n  Anyone remember what those are off the top of our head?\n\n- Having two copies of each method (`foo` and `fooE`) is a bit tedious, but the `foo` variety is essentially the same\n  boilerplate everywhere, so it only increases the maintenance burden on Terratest library maintainers a little, but\n  it improves code readability for all Terratest users enormously.\n"
  },
  {
    "path": "SECURITY.md",
    "content": "# Reporting Security Issues\n\nGruntwork takes security seriously, and we value the input of independent security researchers. If you're reading this because you're looking to engage in responsible disclosure of a security vulnerability, we want to start with thanking you for your efforts. We appreciate your work and will make every effort to acknowledge your contributions.\n\nTo report a security issue, please use the GitHub Security Advisory [\"Report a vulnerability\"](https://github.com/gruntwork-io/terratest/security/advisories/new) button in the [\"Security\"](https://github.com/gruntwork-io/terratest/security) tab.\n\nAfter receiving the report, we will investigate the issue and inform you of next steps. After the initial reply, we may ask for additional information, and will endeavor to keep you informed of our progress.\n\nIf you are reporting a bug related to an associated tool that Terratest integrates with, we ask that you report the issue directly to the maintainers of that tool.\n\nPlease do not disclose the issue publicly until we have had a chance to address it.\n\n## Expectations on timelines\n\nYou can expect that Gruntwork will take any report of a security vulnerability seriously, but we ask that you also respect that it can take time to investigate and address issues given the size of the team maintaining Terratest. We will do our best to keep you informed of our progress, and provide insight into the timeline for addressing the issue.\n\n## Thank you\n\nWe appreciate your help in making Terratest more secure. Thank you for your efforts in responsibly disclosing security issues, and for your patience as we work to address them.\n\n"
  },
  {
    "path": "cmd/pick-instance-type/main.go",
    "content": "package main\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/gruntwork-io/go-commons/entrypoint\"\n\t\"github.com/gruntwork-io/terratest/modules/aws\"\n\t\"github.com/urfave/cli\"\n)\n\nconst CustomUsageText = `Usage: pick-instance-type [OPTIONS] <REGION> <INSTANCE_TYPE> <INSTANCE_TYPE...> \n\nThis tool takes in an AWS region and a list of EC2 instance types and returns the first instance type in the list that is available in all Availability Zones (AZs) in the given region, or exits with an error if no instance type is available in all AZs. This is useful because certain instance types, such as t2.micro, are not available in some of the newer AZs, while t3.micro is not available in some of the older AZs. If you have code that needs to run on a \"small\" instance across all AZs in many different regions, you can use this CLI tool to automatically figure out which instance type you should use.\n\nArguments:\n   \n  REGION           The AWS region in which to look up instance availability. E.g.: us-east-1. \n  INSTANCE_TYPE    One more more EC2 instance types. E.g.: t2.micro.\n\n\nOptions:\n\n  --help            Show this help text and exit.\n\nExample:\n\n  pick-instance-type ap-northeast-2 t2.micro t3.micro \n`\n\nfunc run(cliContext *cli.Context) error {\n\tregion := cliContext.Args().First()\n\tif region == \"\" {\n\t\treturn fmt.Errorf(\"You must specify an AWS region as the first argument\")\n\t}\n\n\tinstanceTypes := cliContext.Args().Tail()\n\tif len(instanceTypes) == 0 {\n\t\treturn fmt.Errorf(\"You must specify at least one instance type\")\n\t}\n\n\t// Create mock testing.T implementation so we can re-use Terratest methods\n\tt := MockTestingT{MockName: \"pick-instance-type\"}\n\n\trecommendedInstanceType, err := aws.GetRecommendedInstanceTypeE(t, region, instanceTypes)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Print the recommended instance type to stdout\n\tfmt.Print(recommendedInstanceType)\n\n\treturn nil\n}\n\nfunc main() {\n\tapp := entrypoint.NewApp()\n\tcli.AppHelpTemplate = CustomUsageText\n\tentrypoint.HelpTextLineWidth = 120\n\n\tapp.Name = \"pick-instance-type\"\n\tapp.Author = \"Gruntwork <www.gruntwork.io>\"\n\tapp.Description = `This tool takes in a list of EC2 instance types (e.g., \"t2.micro\", \"t3.micro\") and returns the first instance type in the list that is available in all Availability Zones (AZs) in the given AWS region, or exits with an error if no instance type is available in all AZs.`\n\tapp.Action = run\n\n\tentrypoint.RunApp(app)\n}\n\n// MockTestingT is a mock implementation of testing.TestingT. All the functions are essentially no-ops. This allows us\n// to use Terratest methods outside of a testing context (e.g., in a CLI tool).\ntype MockTestingT struct {\n\tMockName string\n}\n\nfunc (t MockTestingT) Fail()                                     {}\nfunc (t MockTestingT) FailNow()                                  {}\nfunc (t MockTestingT) Fatal(args ...interface{})                 {}\nfunc (t MockTestingT) Fatalf(format string, args ...interface{}) {}\nfunc (t MockTestingT) Error(args ...interface{})                 {}\nfunc (t MockTestingT) Errorf(format string, args ...interface{}) {}\nfunc (t MockTestingT) Name() string {\n\treturn t.MockName\n}\n"
  },
  {
    "path": "cmd/terratest_log_parser/main.go",
    "content": "// A CLI command to parse parallel terratest output to produce test summaries and break out interleaved test output.\n//\n// This command will take as input a terratest log output from either stdin (through a pipe) or from a file, and output\n// to a directory the following files:\n// outputDir\n//   |-> TEST_NAME.log\n//   |-> summary.log\n//   |-> report.xml\n// where:\n// - `TEST_NAME.log` is a log for each test run that only includes the relevant logs for that test.\n// - `summary.log` is a summary of all the tests in the suite, including PASS/FAIL information.\n// - `report.xml` is the test summary in junit XML format to be consumed by a CI engine.\n//\n// Certain tradeoffs were made in the decision to implement this functionality as a separate parsing command, as opposed\n// to being built into the logger module as part of `Logf`. Specifically, this implementation avoids the difficulties of\n// hooking into go's testing framework to be able to extract the summary logs, at the expense of a more complicated\n// implementation in handling various corner cases due to logging flexibility. Here are the list of pros and cons of\n// this approach that were considered:\n//\n// Pros:\n// - Robust to unexpected failures in testing code, like `ctrl+c`, panics, OOM kills and the like since the parser is\n//   not tied to the testing process. This approach is less likely to miss these entries, and can be surfaced to the\n//   summary view for easy viewing in CI engines (no need to scroll), like the panic example.\n// - Can combine `go test` output (e.g `--- PASS` entries) with the log entries for the test in a single log file.\n// - Can extract out the summary view (those are all `go test` log entries).\n// - Can build `junit.xml` report that CI engines can use for test insights.\n//\n// Cons:\n// - Complicated implementation that is potentially brittle. E.g if someone decides to change the logging format then\n//   this will all break. If we hook during the test, then the implementation is easier because those logs are all emitted\n//   at certain points in code, the information of which is lost in the final log and have to parse out.\n// - Have to store all the logs twice (the full interleaved version, and the broken out version) because the parsing\n//   depends on logs being available. (NOTE: this is avoidable with a pipe).\n\npackage main\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/gruntwork-io/go-commons/entrypoint\"\n\t\"github.com/gruntwork-io/go-commons/errors\"\n\t\"github.com/gruntwork-io/go-commons/logging\"\n\t\"github.com/gruntwork-io/terratest/modules/logger/parser\"\n\t\"github.com/sirupsen/logrus\"\n\t\"github.com/urfave/cli\"\n)\n\nvar logger = logging.GetLogger(\"terratest_log_parser\")\n\nconst CUSTOM_USAGE_TEXT = `Usage: terratest_log_parser [--help] [--log-level=info] [--testlog=LOG_INPUT] [--outputdir=OUTPUT_DIR]\n\nA tool for parsing parallel terratest output to produce a test summary and to break out the interleaved logs by test for better debuggability.\n\nOptions:\n   --log-level LEVEL  Set the log level to LEVEL. Must be one of: [panic fatal error warning info debug]\n                      (default: \"info\")\n   --testlog value    Path to file containing test log. If unset will use stdin.\n   --outputdir value  Path to directory to output test output to. If unset will use the current directory.\n   --help, -h         show help\n`\n\nfunc run(cliContext *cli.Context) error {\n\tfilename := cliContext.String(\"testlog\")\n\toutputDir := cliContext.String(\"outputdir\")\n\tlogLevel := cliContext.String(\"log-level\")\n\tlevel, err := logrus.ParseLevel(logLevel)\n\tif err != nil {\n\t\treturn errors.WithStackTrace(err)\n\t}\n\tlogger.SetLevel(level)\n\n\tvar file *os.File\n\tif filename != \"\" {\n\t\tlogger.Infof(\"reading from file\")\n\t\tfile, err = os.Open(filename)\n\t\tif err != nil {\n\t\t\tlogger.Fatalf(\"Error opening file: %s\", err)\n\t\t}\n\t} else {\n\t\tlogger.Infof(\"reading from stdin\")\n\t\tfile = os.Stdin\n\t}\n\tdefer file.Close()\n\n\toutputDir, err = filepath.Abs(outputDir)\n\tif err != nil {\n\t\tlogger.Fatalf(\"Error extracting absolute path of output directory: %s\", err)\n\t}\n\n\tparser.SpawnParsers(logger, file, outputDir)\n\treturn nil\n}\n\nfunc main() {\n\tapp := entrypoint.NewApp()\n\tcli.AppHelpTemplate = CUSTOM_USAGE_TEXT\n\tentrypoint.HelpTextLineWidth = 120\n\n\tapp.Name = \"terratest_log_parser\"\n\tapp.Author = \"Gruntwork <www.gruntwork.io>\"\n\tapp.Description = `A tool for parsing parallel terratest output to produce a test summary and to break out the interleaved logs by test for better debuggability.`\n\tapp.Action = run\n\n\tcurrentDir, err := os.Getwd()\n\tif err != nil {\n\t\tlogger.Fatalf(\"Error finding current directory: %s\", err)\n\t}\n\tdefaultOutputDir := filepath.Join(currentDir, \"out\")\n\n\tlogInputFlag := cli.StringFlag{\n\t\tName:  \"testlog, l\",\n\t\tValue: \"\",\n\t\tUsage: \"Path to file containing test log. If unset will use stdin.\",\n\t}\n\toutputDirFlag := cli.StringFlag{\n\t\tName:  \"outputdir, o\",\n\t\tValue: defaultOutputDir,\n\t\tUsage: \"Path to directory to output test output to. If unset will use the current directory.\",\n\t}\n\tlogLevelFlag := cli.StringFlag{\n\t\tName:  \"log-level\",\n\t\tValue: logrus.InfoLevel.String(),\n\t\tUsage: fmt.Sprintf(\"Set the log level to `LEVEL`. Must be one of: %v\", logrus.AllLevels),\n\t}\n\tapp.Flags = []cli.Flag{\n\t\tlogLevelFlag,\n\t\tlogInputFlag,\n\t\toutputDirFlag,\n\t}\n\n\tentrypoint.RunApp(app)\n}\n"
  },
  {
    "path": "docs/.gitignore",
    "content": ".jekyll-cache\n_site\n"
  },
  {
    "path": "docs/CNAME",
    "content": "terratest.gruntwork.io"
  },
  {
    "path": "docs/Dockerfile",
    "content": "FROM ruby:2.6.2-stretch\nMAINTAINER Gruntwork <info@gruntwork.io>\n\n# This project requires bundler 2, but the docker image comes with bundler 1 so we need to upgrade\nRUN gem install bundler\n\n# Copy the Gemfile and Gemfile.lock into the image and run bundle install in a way that will be cached\nWORKDIR /tmp\nADD Gemfile Gemfile\nADD Gemfile.lock Gemfile.lock\nRUN bundle install\n\nRUN mkdir -p /src\nVOLUME [\"/src\"]\nWORKDIR /src\nCOPY . /src\n\n# Jekyll runs on port 4000 by default\nEXPOSE 4000\n\n# Run jekyll serve - jekyll will build first to create a plain html file for TOS update\nCMD [\"./jekyll-serve.sh\"]\n"
  },
  {
    "path": "docs/Gemfile",
    "content": "source \"https://rubygems.org\"\n# Hello! This is where you manage which Jekyll version is used to run.\n# When you want to use a different version, change it below, save the\n# file and run `bundle install`. Run Jekyll with `bundle exec`, like so:\n#\n#     bundle exec jekyll serve\n#\n# This will help ensure the proper Jekyll version is running.\n# Happy Jekylling!\n# gem \"jekyll\", \"~> 4.0.0\"\n# This is the default theme for new Jekyll sites. You may change this to anything you like.\n# gem \"minima\", \"~> 2.5\"\n# If you want to use GitHub Pages, remove the \"gem \"jekyll\"\" above and\n# uncomment the line below. To upgrade, run `bundle update github-pages`.\ngem \"github-pages\", group: :jekyll_plugins\n# If you have any plugins, put them here!\ngroup :jekyll_plugins do\n  gem 'jekyll-sitemap', '1.4.0'\n  gem 'therubyracer', '0.12.3'\n  gem 'less', '2.6.0'\n  gem 'jekyll-toc'\n  gem 'jekyll-redirect-from'\nend\n\n# Performance-booster for watching directories on Windows\ngem \"wdm\", \"~> 0.1.1\", :install_if => Gem.win_platform?\n"
  },
  {
    "path": "docs/README.md",
    "content": "# Terratest website\n\nThis is the code for the [Terratest website](https://terratest.gruntwork.io).\n\nTerratest website is built with Jekyll and published on Github Pages from `docs` folder on `main` branch.\n\n# Quick Start\n\n## Download project\n\nClone or fork Terratest [repository](https://github.com/gruntwork-io/terratest).\n\n## Run\n\n1. Install [Ruby](https://www.ruby-lang.org/en/documentation/installation/). Version 2.4 or above is recommended.\n   Consider using [rbenv](https://github.com/rbenv/rbenv) to manage Ruby versions.\n\n2. Install `bundler`:\n\n    ```bash\n    gem install bundler\n    ```\n\n3. Go to the `docs` folder:\n\n    ```bash\n    cd docs\n    ```\n\n4. Install gems:\n\n    ```bash\n    bundle install\n    ```\n\n5. Run the docs site locally:\n\n    ```bash\n    bundle exec jekyll serve\n    ```\n\n6. Open [http://localhost:4000/](http://localhost:4000/) in your web browser.\n\n# Deployment\n\nGitHub Pages automatically rebuilds the website from the `/docs` folder whenever you commit and push changes to the `main`\nbranch.\n\n# Working with the documentation\n\nWe recommend updating the documentation *before* updating any code (see [Readme Driven\nDevelopment](http://tom.preston-werner.com/2010/08/23/readme-driven-development.html)). This ensures the documentation\nstays up to date and allows you to think through the problem at a high level before you get lost in the weeds of\ncoding.\n\nThe Terratest website contains *Docs* collection stored in `/docs/_docs`.\n\nWhen you work with the documentation, it's good to preview the changes. To do that, run project as it is described in [Run section](#run).\n\n1. [Change content on the existing page](#change-content-on-the-existing-page)\n2. [Add a new page](#add-a-new-page)\n3. [Remove or rename page](#remove-or-rename-page)\n4. [Add custom redirection](#add-custom-redirection)\n\n## Change content on the existing page\n\n1. Find page in `_docs` by file name (it's the same as page's title).\n2. Edit content and save file.\n\n## Add a new page\n\n1. Create a new file in `_docs`. The file's name and title have to be the same.\n2. At the beginning of the file, add:\n\n```\n---\nlayout: collection-browser-doc                  # X Cannot be changed\ntitle: Quick start                              # <--- Change this\ncategories:\n  - getting-started                             # <--- Change this if needed\nexcerpt: Learn how to work with Terratest.      # <--- Change page description\ntags: [\"Quick Start\", \"DRY\", \"backend\", \"CLI\"]  # <--- Set tags\norder: 100                                      # <--- It sorts the docs on the list\nnav_title: Documentation                        # X Cannot be changed\nnav_title_link: /docs/                          # X Cannot be changed\n---\n\n```\n\n* `layout` - do not change! (Layout sets components like a navigation sidebar, page header, footer, etc.)\n* `title` - document title\n* `categories` - the document's category. Four categories are in use for now: \"getting-started\", \"testing-best-practices\", \"alternative-testing-tools\", and \"community\".\n* `excerpt` - description. Try to keep it short.\n* `tags` - check other posts to see common tags, but you can set a new as well.\n* `order` - it is used to sort the documents within collection.\n* `nav_title` - the title displayed above navigation. It's optional and it's recommended to use the same as other files in the collection.\n* `nav_title_link` - it is URL. If it is set, the `nav_title` becomes a link with a given URL.\n\n3. Add content at the end of the file.\n\n## Remove or rename page\n\n1. Find page in `_docs` by file name (it's the same as page's title).\n2. Delete page or rename.\n\n## Add custom redirection\n\nTo add link to any page, including subpages outside of any collection, you can create a new file in specific collection (e.g. `_docs`), and set following content in the file:\n\n```\n---\ntitle: Support\ncategories: Community\nexcerpt: Need help?\ntags: [\"support\"]\nredirect_to:\n  - /support\norder: 301\n---\n```\n\n\n## Navigation\n\nThe navigation sidebar is built in `_includes/collection_browser/navigation/_collection_toc.html`.\n\nFirst, the script groups documents of the given collection by categories. Categories make the uppermost level in the navigation sidebar.\nThen, within each category, the script adds documents titles to the navigation under specific categories. Documents are sorted by `order` field set in frontmatter section.\nNext, headings from each document are being extracted and added to the navigation.\n\n# Development\n\n## Project structure\n```\n|-- _docs                     # docs *collection*\n|-- _includes                 # partials\n|-- _layouts                  # layouts\n|-- _pages                    # static pages\n| |-- 404                     # \"404: Not found\" page\n| |-- cookie-policy           # \"Cookie Policy\" page\n| |-- docs                    # index page for *_docs* collection\n| |-- index                   # home page\n|\n|-- _posts                    # Posts collection - empty and not used\n|-- _site                     # website generated by Jekyll\n|-- assets                    # Javascript, Stylesheets, and images\n|-- scripts                   # useful scripts to use in development\n| |-- convert_md_to_adoc      # contains the command to convert MD files to ADOC\n|\n|-- Gemfile\n|-- _config.yml               # Jekyll configuration file\n```\n\n## Documentation and Examples collections\n\nThe [*documentation*](https://terratest.gruntwork.io/docs) is implemented as a Jekyll collection and built with [*Collection Browser*](#collection-browser).\n\n### Documentation collection\n\nThe index page of the *Docs* collection is in: `_pages/docs/index.html` and is available under `/docs` URL. It uses *Collection browser* from `_includes/collection_browser/browser` which makes the list of docs, adds search input with tag filter and puts navigation sidebar containing collection's categories.\n\nCollection is stored in `_docs` folder.\n\n## Adding new pages to collections\n\nThe *Docs* collection uses *collection browser* which requires to setup proper meta tags in the doc file.\n\n1. Create a new file in collection folder. *Docs* add to the `_docs`.\n```\n---\nlayout: collection-browser-doc\ntitle: CLI options  # CHANGE THIS\ncategories:\n  - getting-started # CHANGE THIS\nexcerpt: >- # CHANGE THIS\n  Terratest example description\ntags: [\"CLI\"] # CHANGE THIS\norder: 102 # CHANGE THIS\nnav_title: Documentation # OPTIONAL\nnav_title_link: /docs/ # OPTIONAL\n---\n```\n\n* layout - always has to be: `collection-browser-doc` [DO NOT CHANGE]\n* title - the doc title.\n* categories - set one category. Use downcase with dashes, e.g. `getting-started`.\n* excerpt - the doc description.\n* tags - doc tags.\n* order - it is use to list documents in the right order. \"Getting Started\" starts from 100, \"Features\" starts from 200, and \"Community\" starts from 300.\n* nav_title - the title above navigation. It's optional. It's a link if `nav_title_link` is set.\n* nav_title_link - it is a URL. If it is set, `nav_link` is transformed to the link.\n\n## Adding new collections\n\nTo add a new collection based on *Collection browser*, like *Docs* collection:\n\n1. Add collection to the `_config.yml`:\n```\ncollections:\n  my-collection:   # --> Change to your collection's name\n    output: true\n    sort_by: order\n    permalink: /:collection/:categories/:title/  # --> You can adjust this to your needs. You can remove \":categories\" if your collection doesn't use it.\n```\n2. Create a folder for collection in root directory, e.g: `_my-collection` (change name)\n3. Add documents to the `_my-collection` folder and set proper meta tags (see: [Adding new docs to collections](#adding-new-docs-to-collections)).\n4. Create folder for collection's index page in `_pages`. Use collection name, e.g: `_pages/my-collection`.\n5. Add `index.html` file to newly create folder:\n```\n---\nlayout: collection-browser # DO NOT CAHNGE THIS\ntitle: Use cases\nsubtitle: Learn how to work with Terratest.\nexcerpt: Learn how to work with Terratest.\npermalink: /examples/\nslug: examples\nnav_title: Documentation # OPTIONAL\nnav_title_link: /docs/ # OPTIONAL\n---\n\n{% include collection_browser/browser.html collection=site.examples collection_name='examples' %}\n```\n6. Change `title`, `subtitle`, `excerpt`, `permalink`, and `slug` in meta tags.\n7. In `include` statement, set `collection` to your collection set in `_config.yml` and set `collection_name`.\n\n\n## Collection Browser\n\n_The Collection Browser is strongly inspired by implementation of `guides` on *gruntwork.io* website._\n\nThe Collection Browser's purpose is to wrap Jekyll collection into:\n* _index_ page containing ordered list of docs with search form,\n* _show_ pages presenting docs' contents, and containing navigation sidebar,\n* and build navigation sidebar.\n\n### Usage\n\n1. Add collection to `_config.yml`\n```\ncollections:\n  my-collection:   # --> Change to your collection's name\n    output: true\n    sort_by: order\n    permalink: /:collection/:categories/:title/  # --> You can adjust this to your needs. You can remove \":categories\" if your collection doesn't use it.\n```\n2. Create a folder for collection in root directory: `_my-collection`\n3. Add documents (`.md` format is recommended) to the `_my-collection` folder.\n4. In each document add:\n```\n---\nlayout: collection-browser-doc  # <-- It has to be \"collection-browser-doc\"\ntitle: CLI options              # <-- [CHANGE THIS] doc's title\ncategories:                     # <-- [CHANGE THIS] use single category. (Downcase and dashes instead of spaces)\n  - getting-started\nexcerpt: >-                     # <-- [CHANGE THIS] doc's description\n  Some description.\ntags: [\"CLI\", \"Another tag\"]    # <-- [CHANGE THIS] doc's tags\norder: 102                      # <-- [CHANGE THIS] set different number to each doc to set right order\n---\n```\n5. Create `index` page for collection. Create folder with collection name in `_pages`: `my-collection`\n6. Add `index.html` in `_pages/my-collection`:\n```\n---\nlayout: collection-browser         # <-- It has to be \"collection-browser\"\ntitle: Use cases\nsubtitle: Learn how to work with Terratest.\nexcerpt: Learn how to work with Terratest.\npermalink: /use-cases/\nslug: use-cases\n---\n\n{% include collection_browser/browser.html collection=site.my-collection collection_name='my-collection' %}\n```\nAdjust meta tags and replace `my-collection` with your collection name in `{% include ... %}`\n\n### How it works\n\nThe Collection Browser needs the _index_ page in `_pages` folder. It basically imports `browser.html` from `_includes/collection_browser`.\nMeta tags, like `title`, `subtitle`, `excerpt`, are used by Collection Browser on _index_ page. The _index_ page is then published under URL assigned to `permalink`.\n\nThe _index_ page displays the list of collection's docs. Clicking on any of them, redirects user to the collection's doc page.\n\n#### config.yml\n\nCollections are registered in the `_config.yml` file like other typical Jekyll collections.\nAdditional field used in the configuration is: `sort_by: order`. It ensures that collection's documents are displayed in the right order.\nThe `order` is set then in every collection document. For large collections it's recommended to split files into several folders, and then to use 3-digit numbers. So each folder would have reserved range of numbers, like: `100 - 199`, `200-299`, etc. It makes easy to add new documents without overwriting `order` fields in other docs.\n\n#### Layouts\n\nThe Collection Browser uses two layouts:\n* `_layouts/collection-browser.html` - for the _index_ page containing the list of documents\n* `_layouts/collection-browser-doc.html` - for the \"_show_\" page of collection's doc\n\n#### Includes\n\nAll Collection Browser partials are stored in `_includes/collection_browser`.\n* `browser.html` - it is the collection's _index_ page\n* `_doc-page.html` - is a starting point for collection's document pages (_show_ pages)\n\nSome includes may use partials from `_includes` directory. For example, `_includes/collection_browser/_cta-section.html` uses `_includes/links-n-built-by.html`.\n\n#### Assets\n\nNames of assets used by `collection-browser` in `assets` folder start with `collection-browser`.\n\nCollection Browser's classes in stylesheets starts mostly with `cb`, `collection-browser` and `collection-browser-doc`.\n\nJavascript files used by  Collection Browser:\n* `collection-browser_scroll.js` - responsible for the scroll spying in navigation sidebar and sticking this bar at the top of the screen when page is being scrolled.\n* `collection-browser_search.js` - responsible for handling text search and tag filters.\n* `collection-browser_toc.js` - responsible for opening and closing navigation sidebar on the document page.\n\n#### Navigation Sidebar\n\nThe navigation sidebar is built in `_includes/collection_browser/navigation/_collection_toc.html`. Read more: [Navigation](#navigation)\n\n## Markdown (md) > AsciiDoc (adoc) converter\n\nRecommended format of documents is Github Markdown (`.md`). If you use the Markdown format (`.md`), and you want to convert to AsciiDoc format, you can do this with *Pandoc*:\n\n1. Create `input.md` file and paste there Markdown content\n2. Run:\n```\n$ pandoc --from=gfm --to=asciidoc --wrap=none --atx-headers  input.md > output.adoc\n```\n3. The converted content in `.adoc` format is printed in `output.adoc`\n\nYou can use also `scripts/convert_md_to_adoc.sh`.\n\n## AsciiDoc (adoc) > Markdown (md) converter\n\nTo convert from `.adoc` (AsciiDoc) format to Markdown, you need *AsciiDoctor* and *Pandoc*.\n\nFirst, install Asciidoctor:\n```\n$ sudo apt-get install asciidoctor\n```\n\nNext, install Pandoc:\nhttps://pandoc.org/installing.html\n\nThen you can use script: `scripts/convert_adoc_to_md.sh` or use the command:\n\n```\n$ asciidoctor -b docbook input.adoc && pandoc -f docbook -t gfm input.xml -o output.md --wrap=none --atx-headers\n```\n\nIn both cases:\n1. create a file: `input.adoc`,\n2. add content to the file,\n3. run script or command,\n4. The output can be found in `output.md`.\n"
  },
  {
    "path": "docs/_config.yml",
    "content": "# Welcome to Jekyll!\n#\n# This config file is meant for settings that affect your whole blog, values\n# which you are expected to set up once and rarely edit after that. If you find\n# yourself editing this file very often, consider using Jekyll's data files\n# feature for the data you need to update frequently.\n#\n# For technical reasons, this file is *NOT* reloaded automatically when you use\n# 'bundle exec jekyll serve'. If you change this file, please restart the server process.\n#\n# If you need help with YAML syntax, here are some quick references for you:\n# https://learn-the-web.algonquindesign.ca/topics/markdown-yaml-cheat-sheet/#yaml\n# https://learnxinyminutes.com/docs/yaml/\n#\n# Site settings\n# These are used to personalize your new site. If you look in the HTML files,\n# you will see them accessed via {{ site.title }}, {{ site.email }}, and so on.\n# You can create any custom variable you would like, and they will be accessible\n# in the templates via {{ site.myvariable }}.\n\ntitle: Terratest\nurl: \"https://terratest.gruntwork.io\"\nemail: info@gruntwork.io\nname: \"Terratest | Automated tests for your infrastructure code.\"\ndescription: >- # this means to ignore newlines until \"baseurl:\"\n  Terratest is a Go library that provides patterns and helper functions for\n  testing infrastructure, with 1st-class support for Terraform, Packer, Docker,\n  Kubernetes, AWS, GCP, and more.\nbaseurl: \"\" # the subpath of your site, e.g. /blog\nfull_company_name: \"Gruntwork, Inc\"\nthumbnail_path: \"/assets/img/terratest-thumbnail.png\"\nrepository: \"github.com/gruntwork-io/terratest\"\ntwitter_username: https://twitter.com/gruntwork_io\ngithub_username:  https://github.com/gruntwork-io\n\ngithub_api_url: https://raw.githubusercontent.com/gruntwork-io/terratest/main\n\n# Build settings\n# theme: minima\nassets_base_url: '/assets/'\n\ngtm_tracker: GTM-5TTJJGTL\n\ntheme: null\n\nplugins:\n  - jekyll-toc\n  - jekyll-redirect-from\n  - jekyll-sitemap\n\nsass:\n  sass_dir: assets/css\n  style: compressed\n\nwhitelist:\n  - jekyll-redirect-from\n\ninclude: ['_pages']\n\ncollections:\n  docs:\n    output: true\n    sort_by: order\n    permalink: /:collection/:categories/:title/\n\n# Exclude from processing.\n# The following items will not be processed, by default.\n# Any item listed under the `exclude:` key here will be automatically added to\n# the internal \"default list\".\n#\n# Excluded items can be processed by explicitly listing the directories or\n# their entries' file path in the `include:` list.\n#\nexclude:\n  - .sass-cache/\n  - .jekyll-cache/\n  - gemfiles/\n  - Gemfile\n  - Gemfile.lock\n  - node_modules/\n  - vendor/bundle/\n  - vendor/cache/\n  - vendor/gems/\n  - vendor/ruby/\n  - scripts/\n"
  },
  {
    "path": "docs/_data/examples.yml",
    "content": "- id: terraform-hello-world\r\n  name: Terraform Hello, World Example\r\n  image: /assets/img/logos/terraform-logo.png\r\n  files:\r\n    - url: /examples/terraform-hello-world-example/main.tf\r\n      id: terraform_code\r\n    - url: /test/terraform_hello_world_example_test.go\r\n      id: test_code\r\n      default: true\r\n  learn_more:\r\n    - name: Terraform Hello, World\r\n      url: https://github.com/gruntwork-io/terratest/tree/main/examples/terraform-hello-world-example\r\n\r\n- id: packer-hello-world\r\n  name: Packer Hello, World Example\r\n  image: /assets/img/logos/packer-logo.png\r\n  files:\r\n    - url: /examples/packer-hello-world-example/build.pkr.hcl\r\n      id: packer_code\r\n    - url: /test/packer_hello_world_example_test.go\r\n      id: test_code\r\n      default: true\r\n  learn_more:\r\n    - name: Packer Hello, World\r\n      url: https://github.com/gruntwork-io/terratest/tree/main/examples/packer-hello-world-example\r\n\r\n- id: docker-hello-world\r\n  name: Docker Hello, World Example\r\n  image: /assets/img/logos/docker-logo.png\r\n  files:\r\n    - url: /examples/docker-hello-world-example/Dockerfile\r\n      id: docker_code\r\n      prism_lang: docker\r\n    - url: /test/docker_hello_world_example_test.go\r\n      id: test_code\r\n      default: true\r\n  learn_more:\r\n    - name: Docker Hello, World\r\n      url: https://github.com/gruntwork-io/terratest/tree/main/examples/docker-hello-world-example\r\n\r\n- id: kubernetes-hello-world\r\n  name: Kubernetes Hello, World Example\r\n  image: /assets/img/logos/kubernetes-logo.png\r\n  files:\r\n    - url: /examples/kubernetes-hello-world-example/hello-world-deployment.yml\r\n      id: k8s_code\r\n    - url: /test/kubernetes_hello_world_example_test.go\r\n      id: test_code\r\n      default: true\r\n  learn_more:\r\n    - name: Kubernetes Hello, World\r\n      url: https://github.com/gruntwork-io/terratest/tree/main/examples/kubernetes-hello-world-example\r\n\r\n- id: terragrunt-hello-world\r\n  name: Terragrunt Example\r\n  image: /assets/img/logos/terragrunt-logo.png\r\n  files:\r\n    - url: /examples/terragrunt-example/terragrunt.hcl\r\n      id: terragrunt_code\r\n    - url: /examples/terragrunt-example/main.tf\r\n      id: terraform_code\r\n    - url: /modules/terragrunt/terragrunt_example_test.go\r\n      id: test_code\r\n      default: true\r\n  learn_more:\r\n    - name: Terragrunt Unit\r\n      url: https://github.com/gruntwork-io/terratest/tree/main/examples/terragrunt-example\r\n    - name: Terragrunt Stack\r\n      url: https://github.com/gruntwork-io/terratest/tree/main/examples/terragrunt-multi-module-example\r\n\r\n- id: aws-hello-world\r\n  name: AWS Hello, World Example\r\n  image: /assets/img/logos/aws-logo.png\r\n  files:\r\n    - url: /examples/terraform-aws-hello-world-example/main.tf\r\n      id: terraform_code\r\n    - url: /test/terraform_aws_hello_world_example_test.go\r\n      id: test_code\r\n      default: true\r\n  learn_more:\r\n    - name: AWS Hello, World\r\n      url: https://github.com/gruntwork-io/terratest/tree/main/examples/terraform-aws-hello-world-example\r\n\r\n- id: gcp-hello-world\r\n  name: GCP Hello, World Example\r\n  image: /assets/img/logos/gcp-logo.png\r\n  files:\r\n    - url: /examples/terraform-gcp-hello-world-example/main.tf\r\n      id: terraform_code\r\n    - url: /test/gcp/terraform_gcp_hello_world_example_test.go\r\n      id: test_code\r\n      default: true\r\n  learn_more:\r\n    - name: GCP Hello, World\r\n      url: https://github.com/gruntwork-io/terratest/tree/main/examples/terraform-gcp-hello-world-example\r\n\r\n- id: azure-basic\r\n  name: Azure Hello, World Example\r\n  image: /assets/img/logos/azure-logo.png\r\n  files:\r\n    - url: /examples/azure/terraform-azure-example/main.tf\r\n      id: terraform_main_code\r\n    - url: /examples/azure/terraform-azure-example/outputs.tf\r\n      id: terraform_output_code\r\n    - url: /examples/azure/terraform-azure-example/variables.tf\r\n      id: terraform_var_code\r\n    - url: /test/azure/terraform_azure_example_test.go\r\n      id: test_code\r\n      default: true\r\n  learn_more:\r\n    - name: Terraform Azure Example\r\n      url: https://github.com/gruntwork-io/terratest/tree/main/examples/azure/terraform-azure-example\r\n\r\n- id: opa-terraform\r\n  name: OPA Terraform Example\r\n  image: /assets/img/logos/opa-logo.png\r\n  files:\r\n    - url: /examples/terraform-opa-example/pass/main_pass.tf\r\n      id: pass_terraform_main_code\r\n    - url: /examples/terraform-opa-example/fail/main_fail.tf\r\n      id: fail_terraform_main_code\r\n    - url: /examples/terraform-opa-example/policy/enforce_source.rego\r\n      id: policy_main_code\r\n    - url: /test/terraform_opa_example_test.go\r\n      id: test_code\r\n      default: true\r\n  learn_more:\r\n    - name: Terraform OPA Example\r\n      url: https://github.com/gruntwork-io/terratest/tree/main/examples/terraform-opa-example\r\n\r\n- id: client-factory\r\n  name: Azure Client Factory\r\n  display_in_examples: false\r\n  files:\r\n    - url: /modules/azure/client_factory.go\r\n      id: client_factory_code\r\n    - url: /modules/azure/client_factory_test.go\r\n      id: client_factory_test\r\n    - url: /modules/azure/compute.go\r\n      id: client_factory_helper\r\n"
  },
  {
    "path": "docs/_data/prism_extends.yml",
    "content": "sh: bash\ntpl: yaml\ntf: hcl\ntfvars: hcl\nyml: yaml\n"
  },
  {
    "path": "docs/_docs/01_getting-started/examples.md",
    "content": "---\r\ntitle: Examples\r\ncategory: getting-started\r\nexcerpt: Examples are the best way to start testing Terraform, Docker, Packer, Kubernetes, AWS, GCP, and more with Terratest.\r\ntags: [\"example\"]\r\nredirect_to:\r\n  - /examples/\r\norder: 102\r\nnav_title: Documentation\r\nnav_title_link: /docs/\r\n---\r\n"
  },
  {
    "path": "docs/_docs/01_getting-started/godoc.md",
    "content": "---\r\nlayout: collection-browser-doc\r\ntitle: GoDoc\r\ncategory: getting-started\r\nexcerpt: >-\r\n  Browse Terratest methods and types in GoDoc.\r\ntags: [\"packages\"]\r\nredirect_to:\r\n  - https://godoc.org/github.com/gruntwork-io/terratest\r\ntarget_blank: true\r\norder: 104\r\nnav_title: Documentation\r\nnav_title_link: /docs/\r\n---\r\n"
  },
  {
    "path": "docs/_docs/01_getting-started/introduction.md",
    "content": "---\r\nlayout: collection-browser-doc\r\ntitle: Introduction\r\ncategory: getting-started\r\ntoc: true\r\nexcerpt: >-\r\n  Terratest provides a variety of helper functions and patterns for common infrastructure testing tasks. Learn more about Terratest basic usage.\r\ntags: [\"basic-usage\"]\r\norder: 100\r\nnav_title: Documentation\r\nnav_title_link: /docs/\r\n---\r\n\r\n## Introduction\r\n\r\nTerratest is a Go library that makes it easier to write automated tests for your infrastructure code. It provides a\r\nvariety of helper functions and patterns for common infrastructure testing tasks, including:\r\n\r\n- Testing Terraform code\r\n- Testing Packer templates\r\n- Testing Docker images\r\n- Executing commands on servers over SSH\r\n- Working with AWS APIs\r\n- Working with Azure APIs\r\n- Working with GCP APIs\r\n- Working with Kubernetes APIs\r\n- Enforcing policies with OPA\r\n- Testing Helm Charts\r\n- Making HTTP requests\r\n- Running shell commands\r\n- And much more\r\n\r\n\r\n## Watch: “How to test infrastructure code”\r\n\r\nYevgeniy Brikman talks about how to write automated tests for infrastructure code, including the code written for use with tools such as Terraform, Docker, Packer, and Kubernetes. Topics covered include: unit tests, integration tests, end-to-end tests, dependency injection, test parallelism, retries and error handling, static analysis, property testing and CI / CD for infrastructure code.\r\n\r\nThis presentation was recorded at QCon San Francisco 2019: https://qconsf.com/.\r\n\r\n<iframe width=\"100%\" height=\"450\" allowfullscreen src=\"https://www.youtube.com/embed/xhHOW0EF5u8\"></iframe>\r\n\r\n### Slides\r\n\r\nSlides to the video can be found here: [Slides: How to test infrastructure code](https://www.slideshare.net/brikis98/how-to-test-infrastructure-code-automated-testing-for-terraform-kubernetes-docker-packer-and-more){:target=\"\\_blank\"}.\r\n\r\n\r\n## Gruntwork\r\n\r\nTerratest was developed at [Gruntwork](https://gruntwork.io/) to help maintain the [Infrastructure as Code\r\nLibrary](https://gruntwork.io/infrastructure-as-code-library/), which contains over 300,000 lines of code written\r\nin Terraform, Go, Python, and Bash, and is used in production by hundreds of companies.\r\n\r\n<div class=\"cb-post-cta\">\r\n  <span class=\"title\">See how to get started with Terratest</span>\r\n  <a class=\"btn btn-primary\" href=\"{{site.baseurl}}/docs/getting-started/quick-start/\">Quick Start</a>\r\n</div>\r\n"
  },
  {
    "path": "docs/_docs/01_getting-started/packages-overview.md",
    "content": "---\r\nlayout: collection-browser-doc\r\ntitle: Package by package overview\r\ncategory: getting-started\r\nexcerpt: >-\r\n  Learn more about Terratest modules and how they can help you test different types infrastructure.\r\ntags: [\"packages\"]\r\norder: 103\r\nnav_title: Documentation\r\nnav_title_link: /docs/\r\n---\r\n\r\nNow that you've had a chance to browse the examples and their tests, here's an overview of the packages you'll find in\r\nTerratest's [modules folder](https://github.com/gruntwork-io/terratest/tree/main/modules) and how they can help you test different types infrastructure:\r\n\r\n{:.doc-styled-table}\r\n| Package            | Description                                                                                                                                                                                                                                                                                          |\r\n| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\r\n| **aws**            | Functions that make it easier to work with the AWS APIs. Examples: find an EC2 Instance by tag, get the IPs of EC2 Instances in an ASG, create an EC2 KeyPair, look up a VPC ID.                                                                                                                     |\r\n| **azure**          | Functions that make it easier to work with the Azure APIs. Examples: get the size of a virtual machine, get the tags of a virtual machine.                                                                                                                                                           |\r\n| **collections**    | Go doesn't have much of a collections library built-in, so this package has a few helper methods for working with lists and maps. Examples: subtract two lists from each other.                                                                                                                      |\r\n| **docker**         | Functions that make it easier to work with Docker and Docker Compose. Examples: run `docker compose` commands.                                                                                                                                                                                       |\r\n| **environment**    | Functions for interacting with os environment. Examples: check for first non empty environment variable in a list.                                                                                                                                                                                   |\r\n| **files**          | Functions for manipulating files and folders. Examples: check if a file exists, copy a folder and all of its contents.                                                                                                                                                                               |\r\n| **gcp**            | Functions that make it easier to work with the GCP APIs. Examples: Add labels to a Compute Instance, get the Public IPs of an Instance, Get a list of Instances in a Managed Instance Group, Work with Storage Buckets and Objects.                                                                                                                                                                                                                     |\r\n| **git**            | Functions for working with Git. Examples: get the name of the current Git branch.                                                                                                                                                                                                                                                                                                                                                                                                                                                  |\r\n| **helm**           | Functions for working with Helm. Examples: Install a Helm chart.                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |\r\n| **http-helper**    | Functions for making HTTP requests. Examples: make an HTTP request to a URL and check the status code and body contain the expected values, run a simple HTTP server locally.                                                                                                                        |\r\n| **k8s**            | Functions that make it easier to work with Kubernetes. Examples: Getting the list of nodes in a cluster, waiting until all nodes in a cluster is ready.                                                                                                                                              |\r\n| **logger**         | A replacement for Go's `t.Log` and `t.Logf` that writes the logs to `stdout` immediately, rather than buffering them until the very end of the test. This makes debugging and iterating easier.                                                                                                      |\r\n| **logger/parser**  | Includes functions for parsing out interleaved go test output and piecing out the individual test logs. Used by the [terratest_log_parser](https://github.com/gruntwork-io/terratest/tree/main/cmd/terratest_log_parser) command.                                                                                                                       |\r\n| **oci**            | Functions that make it easier to work with OCI. Examples: Getting the most recent image of a compartment + OS pair, deleting a custom image, retrieving a random subnet.                                                                                                                             |\r\n| **packer**         | Functions for working with Packer. Examples: run a Packer build and return the ID of the artifact that was created.                                                                                                                                                                                  |\r\n| **random**         | Functions for generating random data. Examples: generate a unique ID that can be used to namespace resources so multiple tests running in parallel don't clash.                                                                                                                                      |\r\n| **retry**          | Functions for retrying actions. Examples: retry a function up to a maximum number of retries, retry a function until a stop function is called, wait up to a certain timeout for a function to complete. These are especially useful when working with distributed systems and eventual consistency. |\r\n| **shell**          | Functions to run shell commands. Examples: run a shell command and return its `stdout` and `stderr`.                                                                                                                                                                                                 |\r\n| **ssh**            | Functions to SSH to servers. Examples: SSH to a server, execute a command, and return `stdout` and `stderr`.                                                                                                                                                                                         |\r\n| **terraform**      | Functions for working with Terraform. Examples: run `terraform init`, `terraform apply`, `terraform destroy`.                                                                                                                                                                                        |\r\n| **terragrunt**     | Functions for working with Terragrunt. Examples: run `terragrunt apply --all`, `terragrunt destroy --all`, test stack configurations with dependencies, and work with Terragrunt stacks.                                                                                                      |\r\n| **test_structure** | Functions for structuring your tests to speed up local iteration. Examples: break up your tests into stages so that any stage can be skipped by setting an environment variable.                                                                                                                     |\r\n"
  },
  {
    "path": "docs/_docs/01_getting-started/quick-start.md",
    "content": "---\r\nlayout: collection-browser-doc\r\ntitle: Quick start\r\ncategory: getting-started\r\nexcerpt: Learn how to start with Terratest.\r\ntags: [\"quick-start\"]\r\norder: 101\r\nnav_title: Documentation\r\nnav_title_link: /docs/\r\ncustom_js:\r\n  - examples\r\n  - prism\r\n---\r\n\r\n## Requirements\r\n\r\nTerratest uses the Go testing framework. To use Terratest, you need to install:\r\n\r\n- [Go](https://golang.org/) (requires version >=1.21.1)\r\n\r\n## Setting up your project\r\n\r\nThe easiest way to get started with Terratest is to copy one of the examples and its corresponding tests from this\r\nrepo. This quick start section uses a Terraform example, but check out the [Examples]({{site.baseurl}}/examples/) section for other\r\ntypes of infrastructure code you can test (e.g., Packer, Kubernetes, etc).\r\n\r\n1. Create an `examples` and `test` folder.\r\n\r\n1. Copy the folder including all the files from the [basic terraform example](https://github.com/gruntwork-io/terratest/tree/main/examples/terraform-basic-example/) into the `examples` folder.\r\n\r\n1. Copy the [basic terraform example test](https://github.com/gruntwork-io/terratest/blob/main/test/terraform_basic_example_test.go) into the `test` folder.\r\n\r\n1. To configure dependencies, run:\r\n\r\n    ```bash\r\n    cd test\r\n    go mod init \"<MODULE_NAME>\"\r\n    go mod tidy\r\n    ```\r\n\r\n    Where `<MODULE_NAME>` is the name of your module, typically in the format\r\n    `github.com/<YOUR_USERNAME>/<YOUR_REPO_NAME>`.\r\n\r\n1. To run the tests:\r\n\r\n    ```bash\r\n    cd test\r\n    go test -v -timeout 30m\r\n    ```\r\n\r\n    *(See [Timeouts and logging]({{ site.baseurl }}/docs/testing-best-practices/timeouts-and-logging/) for why the `-timeout` parameter is used.)*\r\n\r\n\r\n## Terratest intro\r\n\r\nThe basic usage pattern for writing automated tests with Terratest is to:\r\n\r\n1. Write tests using Go’s built-in [package testing](https://golang.org/pkg/testing/): you create a file ending in `_test.go` and run tests with the `go test` command. E.g., `go test my_test.go`.\r\n1. Use Terratest to execute your _real_ IaC tools (e.g., Terraform, Packer, etc.) to deploy _real_ infrastructure (e.g., servers) in a _real_ environment (e.g., AWS).\r\n1. Use the tools built into Terratest to validate that the infrastructure works correctly in that environment by making HTTP requests, API calls, SSH connections, etc.\r\n1. Undeploy everything at the end of the test.\r\n\r\nTo make this sort of testing easier, Terratest provides a variety of helper functions and patterns for common infrastructure testing tasks, such as testing Terraform code, testing Packer templates, testing Docker images, executing commands on servers over SSH, making HTTP requests, working with AWS APIs, and so on.\r\n\r\n\r\n## Example #1: Terraform \"Hello, World\"\r\n\r\nLet's start with the simplest possible [Terraform](https://www.terraform.io/) code, which just outputs the text, \r\n\"Hello, World\" (if you’re new to Terraform, check out our [Comprehensive Guide to \r\nTerraform](https://blog.gruntwork.io/a-comprehensive-guide-to-terraform-b3d32832baca)): \r\n \r\n{% include examples/explorer.html example_id='terraform-hello-world' file_id='terraform_code' class='wide quick-start-examples' skip_learn_more=true skip_view_on_github=true skip_tags=true %}\r\n \r\nHow can you test this code to be confident it works correctly? Well, let’s think about how you would test it manually:\r\n\r\n1. Run `terraform init` and `terraform apply` to execute the code.\r\n1. When `apply` finishes, check that the output variable says, \"Hello, World\".\r\n1. When you're done testing, run `terraform destroy` to clean everything up.\r\n \r\nUsing Terratest, you can write an automated test that performs the exact same steps! Here’s what the code looks like:\r\n \r\n{% include examples/explorer.html example_id='terraform-hello-world' file_id='test_code' class='wide quick-start-examples' skip_learn_more=true skip_view_on_github=true skip_tags=true %}\r\n \r\nThis code does all the steps we mentioned above, including running `terraform init`, `terraform apply`, reading the \r\noutput variable using `terraform output`, checking its value is what we expect, and running `terraform destroy` \r\n(using [`defer`](https://blog.golang.org/defer-panic-and-recover) to run it at the end of the test, whether the test \r\nsucceeds or fails). If you put this code in a file called `terraform_hello_world_example_test.go`, you can run it by \r\nexecuting `go test`, and you’ll see output that looks like this (truncated for readability):\r\n\r\n```\r\n$ go test -v\r\n=== RUN   TestTerraformHelloWorldExample\r\nRunning command terraform with args [init]\r\nInitializing provider plugins...\r\n[...]\r\nTerraform has been successfully initialized!\r\n[...]\r\nApply complete! Resources: 0 added, 0 changed, 0 destroyed.\r\nOutputs:\r\nhello_world = \"Hello, World!\"\r\n[...]\r\nRunning command terraform with args [destroy -force -input=false]\r\n[...]\r\nDestroy complete! Resources: 2 destroyed.\r\n--- PASS: TestTerraformHelloWorldExample (149.36s)\r\n```\r\n\r\nSuccess! \r\n\r\n## Example #2: Terraform and AWS\r\n\r\nLet's now try out a more realistic Terraform example. Here is some Terraform code that deploys a simple web server in \r\nAWS:\r\n\r\n{% include examples/explorer.html example_id='aws-hello-world' file_id='terraform_code' class='wide quick-start-examples' skip_learn_more=true skip_view_on_github=true skip_tags=true %}\r\n\r\nThe code above deploys an [EC2 Instance](https://aws.amazon.com/ec2/) that is running an Ubuntu \r\n[Amazon Machine Image (AMI)](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/AMIs.html). To keep this example \r\nsimple, we specify a [User Data](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/user-data.html#user-data-api-cli) \r\nscript that, while the server is booting, fires up a dirt-simple web server that returns “Hello, World” on port 8080.\r\n\r\nHow can you test this code to be confident it works correctly? Well, let’s again think about how you would test it \r\nmanually:\r\n\r\n1. Run `terraform init` and `terraform apply` to deploy the web server into your AWS account.\r\n1. When `apply` finishes, get the IP of the web server by reading the `public_ip` output variable.\r\n1. Open the IP in your web browser with port 8080 and make sure it says “Hello, World”. Note that it can take 1–2 \r\n   minutes for the server to boot up, so you may have to retry a few times.\r\n1. When you’re done testing, run `terraform destroy` to clean everything up.\r\n\r\nHere's how we can automate the steps above using Terratest:\r\n\r\n{% include examples/explorer.html example_id='aws-hello-world' file_id='test_code' class='wide quick-start-examples' skip_learn_more=true skip_view_on_github=true skip_tags=true %}\r\n\r\nThis test code runs `terraform init` and `terraform apply`, reads the server IP using `terraform output`, makes HTTP \r\nrequests to the web server (including plenty of retries to account for the server taking time to boot), checks the HTTP\r\nresponse is what we expect, and then runs `terraform destroy` at the end. If you put this code in a file called \r\n`terraform_aws_hello_world_example_test.go`, you can run just this test by passing the `-run` argument to `go test` as \r\nfollows:\r\n\r\n```\r\n$ go test -v -run TestTerraformAwsHelloWorldExample -timeout 30m\r\n=== RUN   TestTerraformAwsHelloWorldExample\r\nRunning command terraform with args [init]\r\nInitializing provider plugins...\r\n[...]\r\nTerraform has been successfully initialized!\r\n[...]\r\nRunning command terraform with args [apply -auto-approve]\r\naws_instance.example: Creating...\r\n  associate_public_ip_address:       \"\" => \"<computed>\"\r\n  availability_zone:                 \"\" => \"<computed>\"\r\n  ephemeral_block_device.#:          \"\" => \"<computed>\"\r\n  instance_type:                     \"\" => \"t2.micro\"\r\n  key_name:                          \"\" => \"<computed>\"\r\n[...]\r\nApply complete! Resources: 2 added, 0 changed, 0 destroyed.\r\nOutputs:\r\npublic_ip = 52.67.41.31\r\n[...]\r\nMaking an HTTP GET call to URL http://52.67.41.31:8080\r\ndial tcp 52.67.41.31:8080: getsockopt: connection refused.\r\nSleeping for 5s and will try again.\r\nMaking an HTTP GET call to URL http://52.67.41.31:8080\r\ndial tcp 52.67.41.31:8080: getsockopt: connection refused.\r\nSleeping for 5s and will try again.\r\nMaking an HTTP GET call to URL http://52.67.41.31:8080\r\nSuccess!\r\n[...]\r\nRunning command terraform with args [destroy -force -input=false]\r\n[...]\r\nDestroy complete! Resources: 2 destroyed.\r\n--- PASS: TestTerraformAwsHelloWorldExample (149.36s)\r\n```\r\n\r\nSuccess! Now, every time you make a change to this Terraform code, the test code can run and make sure your web server \r\nworks as expected.\r\n\r\nNote that in the `go test` command above, we set `-timeout 30m`. This is because Go sets a default test time out of 10\r\nminutes, and if your test take longer than that to run, Go will panic, and kill the test code part way through. This is\r\nnot only annoying, but also prevents the clean up code from running (the `terraform destroy`), leaving you with lots of\r\nresources hanging in your AWS account. To prevent this, we always recommend setting a high test timeout; the test above\r\ndoesn't actually take anywhere near 30 minutes (typical runtime is ~3 minutes), but we give lots of extra buffer to be\r\nextra sure that the test always has a chance to finish cleanly. \r\n\r\n## Example #3: Docker\r\n\r\nYou can use Terratest for testing a variety of infrastructure code, not just Terraform. For example, you can use it to\r\ntest your [Docker](https://www.docker.com/) images:\r\n\r\n{% include examples/explorer.html example_id='docker-hello-world' file_id='docker_code' class='wide quick-start-examples' skip_learn_more=true skip_view_on_github=true skip_tags=true %}\r\n\r\nThe `Dockerfile` above creates a simple Docker image that uses Ubuntu 18.04 as a base and writes the text \"Hello, World!\" \r\nto a text file. At this point, you should already know the drill. First, let's think through how you'd test this \r\n`Dockerfile` manually:\r\n\r\n1. Run `docker build` to build the Docker image.\r\n1. Run the image via `docker run`.\r\n1. Check that the running Docker container has a text file with the text \"Hello, World!\" in it.\r\n\r\nHere's how you can use Terratest to automate this process:  \r\n\r\n{% include examples/explorer.html example_id='docker-hello-world' file_id='test_code' class='wide quick-start-examples' skip_learn_more=true skip_view_on_github=true skip_tags=true %}\r\n\r\nInstead of using Terraform helpers, this test code uses Terratest's Docker helpers to run `docker build`, `docker run`,\r\nand check the contents of the text file. As before, you can run this test using `go test`!\r\n\r\n## Example #4: Kubernetes\r\n\r\nTerratest also provides helpers for testing your [Kubernetes](https://kubernetes.io/) code. For example, here's a \r\nKubernetes manifest you might want to test:\r\n\r\n{% include examples/explorer.html example_id='kubernetes-hello-world' file_id='k8s_code' class='wide quick-start-examples' skip_learn_more=true skip_view_on_github=true skip_tags=true %}\r\n\r\nThis manifest deploys the [Docker training webapp](https://hub.docker.com/r/training/webapp/), a simple app that \r\nresponds with the text \"Hello, World!\", as a Kubernetes Deployment and exposes it to the outside world on port 5000 \r\nusing a `LoadBalancer`.\r\n\r\nTo test this code manually, you would:\r\n\r\n1. Run `kubectl apply` to deploy the Docker training webapp.\r\n1. Use the Kubernetes APIs to figure out the endpoint to hit for the load balancer.\r\n1. Open the endpoint in your web browser on port 5000 and make sure it says “Hello, World”. Note that, depending on \r\n   your Kubernetes cluster, it could take a minute or two for the Docker container to come up, so you may have to retry \r\n   a few times.\r\n1. When you're done testing, run `kubectl delete` to clean everything up.\r\n\r\nHere's how you automate this process with Terratest:\r\n\r\n{% include examples/explorer.html example_id='kubernetes-hello-world' file_id='test_code' class='wide quick-start-examples' skip_learn_more=true skip_view_on_github=true skip_tags=true %}\r\n\r\nThe test code above uses Kuberenetes helpers built into Terratest to run `kubectl apply`, wait for the service to come\r\nup, get the service endpoint, make HTTP requests to the service (with plenty of retries), check the response is what\r\nwe expect, and runs `kubectl delete` at the end. You run this test with `go test` as well! \r\n\r\n\r\n## Give it a shot!\r\n\r\nThe above is just a small taste of what you can do with [Terratest](https://github.com/gruntwork-io/terratest). To \r\nlearn more:\r\n\r\n1. Check out the [examples]({{site.baseurl}}/examples/) and the corresponding automated tests for those examples for fully working (and tested!) sample code.\r\n1. Browse through the list of [Terratest packages]({{site.baseurl}}/docs/getting-started/packages-overview/) to get a sense of all the tools available in Terratest.\r\n1. Read our [Testing Best Practices Guide]({{site.baseurl}}/docs/#testing-best-practices).\r\n1. Check out real-world examples of Terratest usage in our open source infrastructure modules: [Consul](https://github.com/hashicorp/terraform-aws-consul), [Vault](https://github.com/hashicorp/terraform-aws-vault), [Nomad](https://github.com/hashicorp/terraform-aws-nomad).\r\n\r\nHappy testing!\r\n"
  },
  {
    "path": "docs/_docs/01_getting-started/testing-terragrunt.md",
    "content": "---\nlayout: collection-browser-doc\ntitle: Testing Terragrunt\ncategory: getting-started\nexcerpt: >-\n  Learn how to test Terragrunt configurations with Terratest.\ntags: [\"terragrunt\", \"testing\", \"quick-start\"]\norder: 104\nnav_title: Documentation\nnav_title_link: /docs/\n---\n\n## Overview\n\nTerratest provides two approaches for testing Terragrunt configurations:\n\n| Approach | Use Case | Package |\n|----------|----------|---------|\n| **Unit** | Testing individual units | `modules/terraform` with `TerraformBinary: \"terragrunt\"` |\n| **Stack** | Testing a stack of units with `--all` commands | `modules/terragrunt` |\n\n## Unit Testing\n\nFor testing a single Terragrunt unit, use the `terraform` package with `TerraformBinary` set to `\"terragrunt\"`:\n\n```go\nfunc TestTerragruntModule(t *testing.T) {\n    t.Parallel()\n\n    terraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{\n        TerraformDir:    \"../examples/my-module\",\n        TerraformBinary: \"terragrunt\",\n    })\n\n    defer terraform.Destroy(t, terraformOptions)\n    terraform.Apply(t, terraformOptions)\n\n    output := terraform.Output(t, terraformOptions, \"my_output\")\n    assert.Equal(t, \"expected_value\", output)\n}\n```\n\n## Stack Testing\n\nFor testing a stack of units with dependencies, use the dedicated `terragrunt` package:\n\n```go\nfunc TestStack(t *testing.T) {\n    t.Parallel()\n\n    testFolder, err := files.CopyTerragruntFolderToTemp(\"../live/prod\", t.Name())\n    require.NoError(t, err)\n\n    options := &terragrunt.Options{\n        TerragruntDir: testFolder,\n    }\n\n    defer terragrunt.DestroyAll(t, options)\n    terragrunt.ApplyAll(t, options)\n\n    exitCode := terragrunt.PlanAllExitCode(t, options)\n    require.Equal(t, 0, exitCode)\n}\n```\n\n### Available Functions\n\n| Function | Description |\n|----------|-------------|\n| `Init` | Run `terragrunt init` |\n| `ApplyAll` | Run `terragrunt apply --all` |\n| `DestroyAll` | Run `terragrunt destroy --all` |\n| `PlanAllExitCode` | Run `terragrunt plan --all`, return exit code |\n| `ValidateAll` | Run `terragrunt validate --all` |\n| `FormatAll` | Run `terragrunt fmt --all` |\n| `RunAll` | Run any command with `--all` flag |\n\n### Stack Functions\n\nFor Terragrunt [stacks](https://terragrunt.gruntwork.io/docs/features/stacks/):\n\n| Function | Description |\n|----------|-------------|\n| `StackGenerate` | Generate stack from `terragrunt.stack.hcl` |\n| `StackRun` | Run command on generated stack |\n| `StackClean` | Remove `.terragrunt-stack` directory |\n| `Output` | Get stack output value |\n| `OutputAll` | Get all stack outputs as map |\n\n## Further Reading\n\n- [Terragrunt Documentation](https://terragrunt.gruntwork.io/)\n- [Stack example](https://github.com/gruntwork-io/terratest/tree/main/examples/terragrunt-multi-module-example)\n- [terragrunt package reference](https://pkg.go.dev/github.com/gruntwork-io/terratest/modules/terragrunt)\n"
  },
  {
    "path": "docs/_docs/02_testing-best-practices/alternative-testing-tools.md",
    "content": "---\nlayout: collection-browser-doc\ntitle: Alternative testing tools\ncategory: testing-best-practices\nexcerpt: >-\n  Learn more about alternatives to Terratest, and how Terratest compares to other testing tools.\ntags: [\"testing-best-practices\", \"alternatives\"]\norder: 211\nnav_title: Documentation\nnav_title_link: /docs/\n---\n\n## A list of infrastructure testing tools\n\nBelow is a list of other infrastructure testing tools you may wish to use in addition to Terratest. Check out [How\nTerratest compares to other testing tools]({{site.baseurl}}/docs/testing-best-practices/alternative-testing-tools/#how-terratest-compares-to-other-testing-tools) to understand the trade-offs.\n\n1.  [kitchen-terraform](https://github.com/newcontext-oss/kitchen-terraform)\n1.  [rspec-terraform](https://github.com/bsnape/rspec-terraform)\n1.  [serverspec](https://serverspec.org/)\n1.  [inspec](https://www.inspec.io/)\n1.  [Goss](https://github.com/aelsabbahy/goss)\n1.  [awspec](https://github.com/k1LoW/awspec)\n1.  [Terraform's acceptance testing framework](https://github.com/hashicorp/terraform/blob/master/.github/CONTRIBUTING.md#acceptance-tests-testing-interactions-with-external-services)\n1.  [ruby_terraform](https://github.com/infrablocks/ruby_terraform)\n\n\n\n## Why Terratest?\n\nOur experience with building the [Infrastructure as Code Library](https://gruntwork.io/infrastructure-as-code-library/)\nis that the _only_ way to create reliable, maintainable infrastructure code is to have a thorough suite of real-world,\nend-to-end acceptance tests. Without these sorts of tests, you simply cannot be confident that the infrastructure code\nactually works.\n\nThis is especially important with modern DevOps, as all the tools are changing so quickly. Terratest has helped us\ncatch bugs not only in our own code, but also in AWS, Azure, Terraform, Packer, Kafka, Elasticsearch, CircleCI, and\nso on. Moreover, by running tests nightly, we're able to catch backwards incompatible changes and\nregressions in our dependencies (e.g., backwards incompatibilities in new versions of Terraform) as early as possible.\n\n\n\n## How Terratest compares to other testing tools\n\nMost of the other infrastructure testing tools we've seen are focused on making it easy to check the properties of a\nsingle server or resource. For example, the various `xxx-spec` tools offer a nice, concise language for connecting to\na server and checking if, say, `httpd` is installed and running. These tools are effectively verifying that individual\n\"properties\" of your infrastructure meet a certain spec.\n\nTerratest approaches the testing problem from a different angle. The question we're trying to answer is, \"does the\ninfrastructure actually work?\" Instead of checking individual server properties (e.g., is `httpd` installed and\nrunning), we'll actually make HTTP requests to the server and check that we get the expected response; or we'll store\ndata in a database and make sure we can read it back out; or we'll try to deploy a new version of a Docker container\nand make sure the orchestration tool can roll out the new container with no downtime.\n\nMoreover, we use Terratest not only with individual servers, but to test entire systems. For example, the automated\ntests for the [Vault module](https://github.com/hashicorp/terraform-aws-vault/tree/master/modules) do the following:\n\n1.  Use Packer to build an AMI.\n1.  Use Terraform to create self-signed TLS certificates.\n1.  Use Terraform to deploy all the infrastructure: a Vault cluster (which runs the AMI from the previous step), Consul\n    cluster, load balancers, security groups, S3 buckets, and so on.\n1.  SSH to a Vault node to initialize the cluster.\n1.  SSH to all the Vault nodes to unseal them.\n1.  Use the Vault SDK to store data in Vault.\n1.  Use the Vault SDK to make sure you can read the same data back out of Vault.\n1.  Use Terraform to undeploy and clean up all the infrastructure.\n\nThe steps above are exactly what you would've done to test the Vault module manually. Terratest helps automate this\nprocess. You can think of Terratest as a way to do end-to-end, acceptance or integration testing, whereas most other\ntools are focused on unit or functional testing.\n"
  },
  {
    "path": "docs/_docs/02_testing-best-practices/avoid-test-caching.md",
    "content": "---\r\nlayout: collection-browser-doc\r\ntitle: Avoid test caching\r\ncategory: testing-best-practices\r\nexcerpt: >-\r\n  Since Go 1.10, test results are automatically cached. See how to turn off caching test results.\r\ntags: [\"testing-best-practices\", \"cache\"]\r\norder: 207\r\nnav_title: Documentation\r\nnav_title_link: /docs/\r\n---\r\n\r\nSince Go 1.10, test results are automatically [cached](https://golang.org/doc/go1.10#test). This can lead to Go not\r\nrunning your tests again if you haven't changed any of the Go code. Since you're probably mainly manipulating Terraform\r\nfiles, you should consider turning the caching of test results off. This ensures that the tests are run every time\r\nyou run `go test` and the result is not just read from the cache.\r\n\r\nTo turn caching off, you can set the `-count` flag to `1` force the tests to run:\r\n\r\n```shell\r\n$ go test -count=1 -timeout 30m -p 1 ./...\r\n```\r\n"
  },
  {
    "path": "docs/_docs/02_testing-best-practices/cleanup.md",
    "content": "---\r\nlayout: collection-browser-doc\r\ntitle: Cleanup\r\ncategory: testing-best-practices\r\nexcerpt: >-\r\n  Since automated tests with Terratest deploy real resources into real environments, you'll want to make sure your tests\r\n  always cleanup after themselves.\r\ntags: [\"testing-best-practices\", \"clean\", \"terraform-destroy\", \"terraform-apply\"]\r\norder: 204\r\nnav_title: Documentation\r\nnav_title_link: /docs/\r\n---\r\n\r\nSince automated tests with Terratest deploy real resources into real environments, you'll want to make sure your tests\r\nalways cleanup after themselves so you don't leave a bunch of resources lying around. Typically, you should use Go's\r\n`defer` keyword to ensure that the cleanup code always runs, even if the test hits an error along the way.\r\n\r\nFor example, if your test runs `terraform apply`, you should run `terraform destroy` at the end to clean up:\r\n\r\n```go\r\n// Ensure cleanup always runs\r\ndefer terraform.Destroy(t, options)\r\n\r\n// Deploy\r\nterraform.Apply(t, options)\r\n\r\n// Validate\r\ncheckServerWorks(t, options)\r\n```\r\n\r\nOf course, despite your best efforts, occasionally cleanup will fail, perhaps due to the CI server going down, or a bug\r\nin your code, or a temporary network outage. To handle those cases, we run a tool called\r\n[cloud-nuke](https://github.com/gruntwork-io/cloud-nuke) in our test AWS account on a nightly basis to clean up any\r\nleftover resources.\r\n"
  },
  {
    "path": "docs/_docs/02_testing-best-practices/debugging-interleaved-test-output.md",
    "content": "---\r\nlayout: collection-browser-doc\r\ntitle: Debugging interleaved test output\r\ncategory: testing-best-practices\r\nexcerpt: >-\r\n  Learn more about `terratest_log_parser`.\r\ntags: [\"testing-best-practices\", \"logger\"]\r\norder: 206\r\nnav_title: Documentation\r\nnav_title_link: /docs/\r\n---\r\n\r\n## Debugging interleaved test output\r\n\r\n**Note**: The `terratest_log_parser` requires an explicit installation. See [Installing the utility\r\nbinaries](#installing-the-utility-binaries) for installation instructions.\r\n\r\nIf you log using Terratest's `logger` package, you may notice that all the test outputs are interleaved from the\r\nparallel execution. This may make it difficult to debug failures, as it can be tedious to sift through the logs to find\r\nthe relevant entries for a failing test, let alone find the test that failed.\r\n\r\nTherefore, Terratest ships with a utility binary `terratest_log_parser` that can be used to break out the logs.\r\n\r\nTo use the utility, you simply give it the log output from a `go test` run and a desired output directory:\r\n\r\n```bash\r\ngo test -timeout 30m | tee test_output.log\r\nterratest_log_parser -testlog test_output.log -outputdir test_output\r\n```\r\n\r\nThis will:\r\n\r\n- Create a file `TEST_NAME.log` for each test it finds from the test output containing the logs corresponding to that\r\n  test.\r\n- Create a `summary.log` file containing the test result lines for each test.\r\n- Create a `report.xml` file containing a Junit XML file of the test summary (so it can be integrated in your CI).\r\n\r\nThe output can be integrated in your CI engine to further enhance the debugging experience. See Terratest's own\r\n[circleci configuration](https://github.com/gruntwork-io/terratest/blob/main/.circleci/config.yml) for an example of how to integrate the utility with CircleCI. This\r\nprovides for each build:\r\n\r\n- A test summary view showing you which tests failed:\r\n\r\n![CircleCI test summary]({{site.baseurl}}/assets/img/docs/debugging-interleaved-test-output/circleci-test-summary.png)\r\n\r\n- A snapshot of all the logs broken out by test:\r\n\r\n![CircleCI logs]({{site.baseurl}}/assets/img/docs/debugging-interleaved-test-output/circleci-logs.png)\r\n\r\n## Installing the utility binaries\r\n\r\nTerratest also ships utility binaries that you can use to improve the debugging experience (see [Debugging interleaved\r\ntest output](#debugging-interleaved-test-output)). The compiled binaries are shipped separately from the library in the\r\n[Releases page](https://github.com/gruntwork-io/terratest/releases).\r\n\r\nThe following binaries are currently available with `terratest`:\r\n\r\n{:.doc-styled-table}\r\n| Command                  | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |\r\n| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\r\n| **terratest_log_parser** | Parses test output from the `go test` command and breaks out the interleaved logs into logs for each test. Integrate with your CI environment to help debug failing tests.                                                                                                                                                                                                                                                                                                                                                                                                            |\r\n| **pick-instance-type**   | Takes an AWS region and a list of EC2 instance types and returns the first instance type in the list that is available in all Availability Zones in the given region, or exits with an error if no instance type is available in all AZs. This is useful because certain instance types, such as t2.micro, are not available in some newer AZs, while t3.micro is not available in some older AZs. If you have code that needs to run on a \"small\" instance across all AZs in many regions, you can use this CLI tool to automatically figure out which instance type you should use. |\r\n\r\nYou can install any binary using one of the following methods:\r\n\r\n- [Manual installation](#manual-installation)\r\n- [go install](#go-install)\r\n- [gruntwork-installer](#gruntwork-installer)\r\n\r\n### Manual installation\r\n\r\nTo install the binary manually, download the version that matches your platform and place it somewhere on your `PATH`.\r\nFor example to install version 0.13.13 of `terratest_log_parser`:\r\n\r\n```bash\r\n# This example assumes a linux 64bit machine\r\n# Use curl to download the binary\r\ncurl --location --silent --fail --show-error -o terratest_log_parser https://github.com/gruntwork-io/terratest/releases/download/v0.13.13/terratest_log_parser_linux_amd64\r\n# Make the downloaded binary executable\r\nchmod +x terratest_log_parser\r\n# Finally, we place the downloaded binary to a place in the PATH\r\nsudo mv terratest_log_parser /usr/local/bin\r\n```\r\n\r\n### go install\r\n\r\n`go` supports building and installing packages and commands from source using the [go\r\ninstall](https://pkg.go.dev/cmd/go#hdr-Compile_and_install_packages_and_dependencies) command. To install the binaries\r\nwith `go install`, point `go install` to the repo and path where the main code for each relevant command lives. For\r\nexample, you can install the terratest log parser binary with:\r\n\r\n```\r\ngo install github.com/gruntwork-io/terratest/cmd/terratest_log_parser@latest\r\n```\r\n\r\nSimilarly, to install `pick-instance-type`, you can run:\r\n\r\n```\r\ngo install github.com/gruntwork-io/terratest/cmd/pick-instance-type@latest\r\n```\r\n\r\n### gruntwork-installer\r\n\r\nYou can also use [the gruntwork-installer utility](https://github.com/gruntwork-io/gruntwork-installer) to install the\r\nbinaries, which will do the above steps and automatically select the right binary for your platform:\r\n\r\n```bash\r\ngruntwork-install --binary-name 'terratest_log_parser' --repo 'https://github.com/gruntwork-io/terratest' --tag 'v0.13.13'\r\n```\r\n"
  },
  {
    "path": "docs/_docs/02_testing-best-practices/error-handling.md",
    "content": "---\r\nlayout: collection-browser-doc\r\ntitle: Error handling\r\ncategory: testing-best-practices\r\nexcerpt: >-\r\n  Learn how to handle errors.\r\ntags: [\"testing-best-practices\", \"terraform\", \"error\"]\r\norder: 208\r\nnav_title: Documentation\r\nnav_title_link: /docs/\r\n---\r\n\r\nJust about every method `foo` in Terratest comes in two versions: `foo` and `fooE` (e.g., `terraform.Apply` and\r\n`terraform.ApplyE`).\r\n\r\n- `foo`: The base method takes a `t *testing.T` as an argument. If the method hits any errors, it calls `t.Fatal` to\r\n  fail the test.\r\n\r\n- `fooE`: Methods that end with the capital letter `E` always return an `error` as the last argument and never call\r\n  `t.Fatal` themselves. This allows you to decide how to handle errors.\r\n\r\nYou will use the base method name most of the time, as it allows you to keep your code more concise by avoiding\r\n`if err != nil` checks all over the place:\r\n\r\n```go\r\nterraform.Init(t, terraformOptions)\r\nterraform.Apply(t, terraformOptions)\r\nurl := terraform.Output(t, terraformOptions, \"url\")\r\n```\r\n\r\nIn the code above, if `Init`, `Apply`, or `Output` hits an error, the method will call `t.Fatal` and fail the test\r\nimmediately, which is typically the behavior you want. However, if you are _expecting_ an error and don't want it to\r\ncause a test failure, use the method name that ends with a capital `E`:\r\n\r\n```go\r\nif _, err := terraform.InitE(t, terraformOptions); err != nil {\r\n  // Do something with err\r\n}\r\n\r\nif _, err := terraform.ApplyE(t, terraformOptions); err != nil {\r\n  // Do something with err\r\n}\r\n\r\nurl, err := terraform.OutputE(t, terraformOptions, \"url\")\r\nif err != nil {\r\n  // Do something with err\r\n}\r\n```\r\n\r\nAs you can see, the code above is more verbose, but gives you more flexibility with how to handle errors.\r\n"
  },
  {
    "path": "docs/_docs/02_testing-best-practices/idempotent.md",
    "content": "---\r\nlayout: collection-browser-doc\r\ntitle: Idempotent\r\ncategory: testing-best-practices\r\nexcerpt: >-\r\n  Test that your Terraform configuration results in consistent deployments.\r\ntags: [\"testing-best-practices\", \"idempotent\", \"terraform\"]\r\norder: 212\r\nnav_title: Documentation\r\nnav_title_link: /docs/\r\n---\r\n\r\nA Terraform configuration is idempotent when a second apply results in 0 changes. An idempotent configuration ensures that:\r\n\r\n1.  What you define in Terraform is exactly what is being deployed. \r\n1.  Detection of bugs in Terraform resources and providers that might affect your configuration.\r\n\r\nYou can use Terratest's `terraform.ApplyAndIdempotent()` function to both apply your Terraform configuration and test its\r\nidempotency.\r\n\r\n```go\r\nterraform.ApplyAndIdempotent(t, terraformOptions)\r\n```\r\n\r\nIf a second apply of your Terraform configuration results in changes then your test will fail.\r\n"
  },
  {
    "path": "docs/_docs/02_testing-best-practices/iterating-locally-using-docker.md",
    "content": "---\r\nlayout: collection-browser-doc\r\ntitle: Iterating locally using Docker\r\ncategory: testing-best-practices\r\nexcerpt: >-\r\n  If you're writing scripts (i.e., Bash, Python, or Go), you should be able to test them locally using Docker. Docker containers typically build 10x faster and start 100x faster than real servers.\r\ntags: [\"testing-best-practices\", \"docker\"]\r\norder: 209\r\nnav_title: Documentation\r\nnav_title_link: /docs/\r\n---\r\n\r\nFor most infrastructure code, your only option is to deploy into a real environment such as AWS. However, if you're\r\nwriting scripts (i.e., Bash, Python, or Go), you should be able to test them locally using Docker. Docker containers\r\ntypically build 10x faster and start 100x faster than real servers, so using Docker for testing can help you iterate\r\nmuch faster.\r\n\r\nHere are some techniques we use with Docker:\r\n\r\n- If your script is used in a Packer template, add a [Docker\r\n  builder](https://www.packer.io/docs/builders/docker.html) to the template so you can create a Docker image from the\r\n  same code. See the [Packer Docker Example](https://github.com/gruntwork-io/terratest/tree/main/examples/packer-docker-example) for working sample code.\r\n\r\n- We have prebuilt Docker images for major Linux distros that have many important dependencies (e.g., curl, vim,\r\n  tar, sudo) already installed. See the [test-docker-images folder](https://github.com/gruntwork-io/terratest/tree/main/test-docker-images) for more details.\r\n\r\n- Create a `docker-compose.yml` to make it easier to run your Docker image with all the ports, environment variables,\r\n  and other settings it needs. See the [Packer Docker Example](https://github.com/gruntwork-io/terratest/tree/main/examples/packer-docker-example) for working sample code.\r\n\r\n- With scripts in Docker, you can replace _some_ real-world dependencies with mocks! One way to do this is to create\r\n  some \"mock scripts\" and to bind-mount them in `docker-compose.yml` in a way that replaces the real dependency. For\r\n  example, if your script calls the `aws` CLI, you could create a mock script called `aws` that shows up earlier in the\r\n  `PATH`. Using mocks allows you to test 100% locally, without external dependencies such as AWS.\r\n"
  },
  {
    "path": "docs/_docs/02_testing-best-practices/iterating-locally-using-test-stages.md",
    "content": "---\r\nlayout: collection-browser-doc\r\ntitle: Iterating locally using test stages\r\ncategory: testing-best-practices\r\nexcerpt: >-\r\n  Learn more about Terratest's `test_structure`.\r\ntags: [\"testing-best-practices\", \"test_structure\"]\r\norder: 210\r\nnav_title: Documentation\r\nnav_title_link: /docs/\r\n---\r\n\r\nMost automated tests written with Terratest consist of multiple \"stages\", such as:\r\n\r\n1.  Build an AMI using Packer\r\n1.  Deploy the AMI using Terraform\r\n1.  Validate that the AMI works as expected\r\n1.  Undeploy the AMI using Terraform\r\n\r\nOften, while testing locally, you'll want to re-run some subset of these stages over and over again: for example, you\r\nmight want to repeatedly run the validation step while you work out the kinks. Having to run _all_ of these stages\r\neach time you change a single line of code can be very slow.\r\n\r\nThis is where Terratest's `test_structure` package comes in handy: it allows you to explicitly break up your tests into\r\nstages and to be able to disable any one of those stages by setting an environment variable. Check out the\r\n[terraform_packer_example_test.go](https://github.com/gruntwork-io/terratest/blob/main/test/terraform_packer_example_test.go)\r\nfor working sample code.\r\n\r\n## How to skip stages\r\n\r\nTo skip a stage, set `SKIP_<stage_name>` to any non-empty value. For example, to re-run deploy and validation without rebuilding the AMI:\r\n\r\n```bash\r\nSKIP_build_ami=true go test -v -run TestTerraformPackerExample\r\n```\r\n\r\nThis works because each stage saves its outputs (AMI IDs, Terraform options, etc.) to the working directory using functions like `SaveString` and `SaveAmiId`. Subsequent stages load this cached data using `LoadString`, `LoadAmiId`, etc. When any `SKIP_*` variable is set, Terratest also skips copying to a temp folder, preserving cached state between runs.\r\n"
  },
  {
    "path": "docs/_docs/02_testing-best-practices/namespacing.md",
    "content": "---\r\nlayout: collection-browser-doc\r\ntitle: Namespacing\r\ncategory: testing-best-practices\r\nexcerpt: >-\r\n  Learn how to avoid conflicts due to duplicated identifiers.\r\ntags: [\"testing-best-practices\", \"namespace\", \"id\", \"identifiers\"]\r\norder: 203\r\nnav_title: Documentation\r\nnav_title_link: /docs/\r\n---\r\n\r\nJust about all resources your tests create (e.g., servers, load balancers, machine images) should be \"namespaced\" with\r\na unique name to ensure that:\r\n\r\n1.  You don't accidentally overwrite any \"production\" resources in that environment (though as mentioned in the previous\r\n    section, your test environment should be completely isolated from prod anyway).\r\n1.  You don't accidentally clash with other tests running in parallel.\r\n\r\nFor example, when deploying AWS infrastructure with Terraform, that typically means exposing variables that allow you\r\nto configure auto scaling group names, security group names, IAM role names, and any other names that must be unique.\r\n\r\nYou can use Terratest's `random.UniqueId()` function to generate identifiers that are short enough to use in resource\r\nnames (just 6 characters) but random enough to make it unlikely that you'll have a conflict.\r\n\r\n```go\r\nuniqueId := random.UniqueId()\r\ninstanceName := fmt.Sprintf(\"terratest-http-example-%s\", uniqueId)\r\n\r\nterraformOptions := &terraform.Options {\r\n  TerraformDir: \"../examples/terraform-http-example\",\r\n  Vars: map[string]interface{} {\r\n    \"instance_name\": instanceName,\r\n  },\r\n}\r\n\r\nterraform.Apply(t, terraformOptions)\r\n```\r\n"
  },
  {
    "path": "docs/_docs/02_testing-best-practices/picking-instance-types.md",
    "content": "---\nlayout: collection-browser-doc\ntitle: Picking EC2 instance types\ncategory: testing-best-practices\nexcerpt: >-\n  Pick EC2 instance types that are available in the current AWS region.\ntags: [\"testing-best-practices\", \"aws\", \"ec2\"]\norder: 213\nnav_title: Documentation\nnav_title_link: /docs/\n---\n\nIt's common to want to test infrastructure code that deploys [EC2 instances](https://aws.amazon.com/ec2/) into AWS. \nThere are many different [instance types](https://aws.amazon.com/ec2/instance-types/), but not all instance types\nare available in all [regions or availability zones \n(AZs)](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html). For example, \n`t3.micro` is sometimes available only in newer AZs, while `t2.micro` is sometimes only available in older AZs. If you\nare testing code that needs to deploy a \"small\" instance across many regions, this can make it tricky to know which\nregion to pick.\n\nTo help work around this problem, Terratest includes:\n\n1. [`GetRecommendedInstanceType`](#getrecommendedinstancetype): A Go function that helps you pick a recommended instance type.\n1. [`pick-instance-type`](#pick-instance-type): A CLI tool that helps you pick a recommended instance type.\n\n\n\n\n## `GetRecommendedInstanceType`\n\n`GetRecommendedInstanceType` takes in an AWS region and a list of EC2 instance types and returns the first instance \ntype in the list that is available in all Availability Zones (AZs) in the given region. If there's no\ninstance available in all AZs, this function exits with an error. \n\nExample usage:\n\n```go\naws.GetRecommendedInstanceType(t, \"eu-west-1\", []string{\"t2.micro\", \"t3.micro\"})\n// As of July, 2020, returns \"t2.micro\"\n\naws.GetRecommendedInstanceType(t, \"ap-northeast-2\", []string{\"t2.micro\", \"t3.micro\"})\n// As of July, 2020, returns \"t3.micro\"\n```   \n\n\n\n## `pick-instance-type`\n\n`pick-instance-type` is a CLI tool that you can download from the [Terratest releases \npage](https://github.com/gruntwork-io/terratest/releases) (click \"Assets\" under any release). It takes in an AWS \nregion and a list of EC2 instance types and prints to `stdout` the first instance type in the list that is available in \nall Availability Zones (AZs) in the given region. If there's no instance available in all AZs, `pick-instance-type`\nexits with an error.\n\nExample usage:\n\n```bash\n# Data below is from July, 2020\n\n$ pick-instance-type eu-west-1 t2.micro t3.micro\nt2.micro\n\n$ pick-instance-type ap-northeast-2 t2.micro t3.micro\nt3.micro\n```   \n"
  },
  {
    "path": "docs/_docs/02_testing-best-practices/testing-environment.md",
    "content": "---\r\nlayout: collection-browser-doc\r\ntitle: Testing environment\r\ncategory: testing-best-practices\r\nexcerpt: >-\r\n  Learn more about testing environments.\r\ntags: [\"testing-best-practices\"]\r\norder: 202\r\nnav_title: Documentation\r\nnav_title_link: /docs/\r\n---\r\n\r\nSince most automated tests written with Terratest can make potentially destructive changes in your environment, we\r\nstrongly recommend running tests in an environment that is totally separate from production. For example, if you are\r\ntesting infrastructure code for AWS, you should run your tests in a completely separate AWS account.\r\n\r\nThis means that you will have to write your infrastructure code in such a way that you can plug in ([dependency\r\ninjection](https://en.wikipedia.org/wiki/Dependency_injection)) environment-specific details, such as account IDs,\r\ndomain names, IP addresses, etc. Adding support for this will typically make your code cleaner and more flexible.\r\n"
  },
  {
    "path": "docs/_docs/02_testing-best-practices/timeouts-and-logging.md",
    "content": "---\r\nlayout: collection-browser-doc\r\ntitle: Timeouts and logging\r\ncategory: testing-best-practices\r\nexcerpt: >-\r\n  Long-running infrastructure tests may exceed timeouts or can be killed if they do not prompt logs.\r\ntags: [\"testing-best-practices\", \"timeout\", \"error\"]\r\norder: 205\r\nnav_title: Documentation\r\nnav_title_link: /docs/\r\n---\r\n\r\nGo's package testing has a default timeout of 10 minutes, after which it forcibly kills your tests—even your cleanup\r\ncode won't run! It's not uncommon for infrastructure tests to take longer than 10 minutes, so you'll almost always\r\nwant to increase the timeout by using the `-timeout` option, which takes a `go` duration string (e.g `10m` for 10\r\nminutes or `1h` for 1 hour):\r\n\r\n```bash\r\ngo test -timeout 30m\r\n```\r\n\r\nNote that many CI systems will also kill your tests if they don't see any log output for a certain period of time\r\n(e.g., 10 minutes in CircleCI). If you use Go's `t.Log` and `t.Logf` for logging in your tests, you'll find that these\r\nfunctions buffer all log output until the very end of the test (see https://github.com/golang/go/issues/24929 for more\r\ninfo). If you have a long-running test, this might mean you get no log output for more than 10 minutes, and the CI\r\nsystem will shut down your tests. Moreover, if your test has a bug that causes it to hang, you won't see any log output\r\nat all to help you debug it.\r\n\r\nTherefore, we recommend instead using Terratest's `logger.Log` and `logger.Logf` functions, which log to `stdout`\r\nimmediately:\r\n\r\n```go\r\nfunc TestFoo(t *testing.T) {\r\n  logger.Log(t, \"This will show up in stdout immediately\")\r\n}\r\n```\r\n\r\nFinally, if you're testing multiple Go packages, be aware that Go will buffer log output—even that sent directly to\r\n`stdout` by `logger.Log` and `logger.Logf`—until all the tests in the package are done. This leads to the same\r\ndifficulties with CI servers and debugging. The workaround is to tell Go to test each package sequentially using the\r\n`-p 1` flag:\r\n\r\n```bash\r\ngo test -timeout 30m -p 1 ./...\r\n```\r\n\r\nSee the [Cleanup]({{site.baseurl}}/docs/testing-best-practices/cleanup/) for more information on how to setup robust clean up procedures in the face of test timeouts and instabilities.\r\n"
  },
  {
    "path": "docs/_docs/02_testing-best-practices/tools-and-plugins.md",
    "content": "---\nlayout: collection-browser-doc\ntitle: Tools and Plugins\ncategory: testing-best-practices\nexcerpt: >-\n  Additional tools and plugins for terratest to make integration with existing workflows easier.\ntags: [\"testing-best-practices\", \"alternatives\",\"plugins\",\"tooling\"]\norder: 214\nnav_title: Documentation\nnav_title_link: /docs/\n---\n\n## Tools and Plugins\n\nThis page contains a list of tools and plugins for Terratest to make integration with existing workflows easier. If you've created other tools that integrate with Terratest, a PR to add it to this list is very welcome!\n\n- [Tools and Plugins](#tools-and-plugins)\n  - [Terratest Maven Plugin](#terratest-maven-plugin)\n\n### Terratest Maven Plugin\n\nThe Terratest Maven Plugin aims to bring Terratest to the JVM world. Create your Go based tests beside your Java code with Maven and run them together. You can export the results into Json or an HTML page. As the plugin is MIT licensed, it is easy and painless to integrate into any Java+Maven combination. To learn more check out the website: [Terratest Maven Plugin](https://terratest-maven-plugin.github.io) and the [GitHub repository](https://github.com/terratest-maven-plugin/terratest-maven-plugin)\n"
  },
  {
    "path": "docs/_docs/02_testing-best-practices/unit-integration-end-to-end-test.md",
    "content": "---\r\nlayout: collection-browser-doc\r\ntitle: Unit tests, integration tests, end-to-end tests\r\ncategory: testing-best-practices\r\nexcerpt: >-\r\n  See the talk about unit tests, integration tests, end-to-end tests, dependency injection, test parallelism, retries, error handling, and static analysis.\r\ntags: [\"testing-best-practices\"]\r\norder: 201\r\nnav_title: Documentation\r\nnav_title_link: /docs/\r\n---\r\n\r\n\r\nFor an introduction to Terratest, including unit tests, integration tests, end-to-end tests, dependency injection, test\r\nparallelism, retries, error handling, and static analysis, see the talk \"Automated Testing for Terraform, Docker,\r\nPacker, Kubernetes, and More\".\r\n\r\n<iframe width=\"100%\" height=\"450\" allowfullscreen src=\"https://www.youtube.com/embed/xhHOW0EF5u8\"></iframe>\r\n\r\nLink to the video at [infoq.com](https://www.infoq.com/presentations/automated-testing-terraform-docker-packer/).\r\n\r\n## Slides\r\n\r\nSlides to the video can be found here: [Slides: How to test infrastructure code](https://www.slideshare.net/brikis98/how-to-test-infrastructure-code-automated-testing-for-terraform-kubernetes-docker-packer-and-more){:target=\"_blank\"}.\r\n"
  },
  {
    "path": "docs/_docs/04_community/contributing.md",
    "content": "---\r\nlayout: collection-browser-doc\r\ntitle: Contributing\r\ncategory: community\r\nexcerpt: >-\r\n  Terratest is an open source project, and contributions from the community are very welcome!\r\ntags: [\"contributing\", \"community\"]\r\norder: 400\r\nnav_title: Documentation\r\nnav_title_link: /docs/\r\ncustom_js:\r\n  - examples\r\n  - prism\r\n  - collection-browser_scroll\r\n  - collection-browser_search\r\n  - collection-browser_toc\r\n---\r\n\r\nTerratest is an open source project, and contributions from the community are very welcome\\! Please check out the\r\n[Contribution Guidelines](#contribution-guidelines) and [Developing Terratest](#developing-terratest) for\r\ninstructions.\r\n\r\n## Contribution Guidelines\r\n\r\nContributions to this repo are very welcome! We follow a fairly standard [pull request\r\nprocess](https://help.github.com/articles/about-pull-requests/) for contributions, subject to the following guidelines:\r\n\r\n1. [Types of contributions](#types-of-contributions)\r\n1. [File a GitHub issue](#file-a-github-issue)\r\n1. [Update the documentation](#update-the-documentation)\r\n1. [Update the tests](#update-the-tests)\r\n1. [Update the code](#update-the-code)\r\n1. [Create a pull request](#create-a-pull-request)\r\n1. [Merge and release](#merge-and-release)\r\n\r\n### Types of contributions\r\n\r\nBroadly speaking, Terratest contains two types of helper functions:\r\n\r\n1. Integrations with external tools\r\n1. Infrastructure and validation helpers\r\n\r\nWe accept different types of contributions for each of these two types of helper functions, as described next.\r\n\r\n#### Integrations with external tools\r\n\r\nThese are helper functions that integrate with various DevOps tools—e.g., Terraform, Docker, Packer, and\r\nKubernetes—that you can use to deploy infrastructure in your automated tests. Examples:\r\n\r\n* `terraform.InitAndApply`: run `terraform init` and `terraform apply`.\r\n* `packer.BuildArtifacts`: run `packer build`.\r\n* `shell.RunCommandAndGetOutput`: run an arbitrary shell command and return `stdout` and `stderr` as a string.\r\n\r\nHere are the guidelines for contributions with external tools:\r\n\r\n1. **Fixes and improvements to existing integrations**: All bug fixes and new features for existing tool integrations\r\n   are very welcome!  \r\n\r\n1. **New integrations**: Before contributing an integration with a totally new tool, please file a GitHub issue to\r\n   discuss with us if it's something we are interested in supporting and maintaining. For example, we may be open to\r\n   new integrations with Docker and Kubernetes tools, but we may not be open to integrations with Chef or Puppet, as\r\n   there are already testing tools available for them.\r\n\r\n#### Infrastructure and validation helpers\r\n\r\nThese are helper functions for creating, destroying, and validating infrastructure directly via API calls or SDKs.\r\nExamples:\r\n\r\n* `http_helper.HttpGetWithRetry`: make an HTTP request, retrying until you get a certain expected response.\r\n* `ssh.CheckSshCommand`: SSH to a server and execute a command.\r\n* `aws.CreateS3Bucket`: create an S3 bucket.\r\n* `aws.GetPrivateIpsOfEc2Instances`:  use the AWS APIs to fetch IPs of some EC2 instances.\r\n\r\nThe number of possible such helpers is nearly infinite, so to avoid Terratest becoming a gigantic, sprawling library\r\nwe ask that contributions for new infrastructure helpers are limited to:\r\n\r\n1. **Platforms**: we currently only support three major public clouds (AWS, GCP, Azure) and Kubernetes. There is some\r\n   code contributed earlier for other platforms (e.g., OCI), but until we have the time/resources to support those\r\n   platforms fully, we will only accept contributions for the major public clouds and Kubernetes.\r\n\r\n1. **Complexity**: we ask that you only contribute infrastructure and validation helpers for code that is relatively\r\n   complex to do from scratch. For example, a helper that merely wraps an existing function in the AWS or GCP SDK is\r\n   not a great choice, as the wrapper isn't contributing much value, but is bloating the Terratest API. On the other\r\n   hand, helpers that expose simple APIs for complex logic are great contributions: `ssh.CheckSshCommand` is a great\r\n   example of this, as it provides a simple one-line interface for dozens of lines of complicated SSH logic.\r\n\r\n1. **Popularity**: Terratest should only contain helpers for common use cases that come up again and again in the\r\n   course of testing. We don't want to bloat the library with lots of esoteric helpers for rarely used tools, so\r\n   here's a quick litmus test: (a) Is this helper something you've used once or twice in your own tests, or is it\r\n   something you're using over and over again? (b) Does this helper only apply to some use case specific to your\r\n   company or is it likely that many other Terratest users are hitting this use case over and over again too?\r\n\r\n1. **Creating infrastructure**: we try to keep helper functions that create infrastructure (e.g., use the AWS SDK to\r\n   create an S3 bucket or EC2 instance) to a minimum, as those functions typically require maintaining state (so that\r\n   they are idempotent and can clean up that infrastructure at the end of the test) and dealing with asynchronous and\r\n   eventually consistent cloud APIs. This can be surprisingly complicated, so we typically recommend using a tool like\r\n   Terraform, which already handles all that complexity, to create any infrastructure you need at test time, and\r\n   running Terratest's built-in `terraform` helpers as necessary. If you're considering contributing a function that\r\n   creates infrastructure directly (e.g., using a cloud provider's APIs), please file a GitHub issue to explain why\r\n   such a function would be a better choice than using a tool like Terraform.\r\n\r\n### File a GitHub issue\r\n\r\nBefore starting any work, we recommend filing a GitHub issue in this repo. This is your chance to ask questions and\r\nget feedback from the maintainers and the community before you sink a lot of time into writing (possibly the wrong)\r\ncode. If there is anything you're unsure about, just ask!\r\n\r\n### Update the documentation\r\n\r\nWe recommend updating the documentation *before* updating any code (see [Readme Driven\r\nDevelopment](http://tom.preston-werner.com/2010/08/23/readme-driven-development.html)). This ensures the documentation\r\nstays up to date and allows you to think through the problem at a high level before you get lost in the weeds of\r\ncoding.\r\n\r\nThe documentation is built with Jekyll and hosted on the Github Pages from `docs` folder on `main` branch. Check out [Terratest website](https://github.com/gruntwork-io/terratest/tree/main/docs#working-with-the-documentation) to learn more about working with the documentation.\r\n\r\n### Update the tests\r\n\r\nWe also recommend updating the automated tests *before* updating any code (see [Test Driven\r\nDevelopment](https://en.wikipedia.org/wiki/Test-driven_development)). That means you add or update a test case,\r\nverify that it's failing with a clear error message, and *then* make the code changes to get that test to pass. This\r\nensures the tests stay up to date and verify all the functionality in this Module, including whatever new\r\nfunctionality you're adding in your contribution. The instructions for running the automated tests can be\r\nfound [here](https://terratest.gruntwork.io/docs/community/contributing/#developing-terratest).\r\n\r\n### Update the code\r\n\r\nAt this point, make your code changes and use your new test case to verify that everything is working. As you work,\r\nplease make every effort to avoid unnecessary backwards incompatible changes. This generally means that you should\r\nnot delete or rename anything in a public API.\r\n\r\nIf a backwards incompatible change cannot be avoided, please make sure to call that out when you submit a pull request,\r\nexplaining why the change is absolutely necessary.\r\n\r\nNote that we use pre-commit hooks with this project. To ensure they run:\r\n\r\n1. Install [pre-commit](https://pre-commit.com/).\r\n1. Run `pre-commit install`.\r\n\r\nOne of the pre-commit hooks we run is [goimports](https://godoc.org/golang.org/x/tools/cmd/goimports). To prevent the\r\nhook from failing, make sure to :\r\n\r\n1. Install [goimports](https://godoc.org/golang.org/x/tools/cmd/goimports)\r\n1. Run `goimports -w .`.\r\n\r\nWe have a [style guide](https://gruntwork.io/guides/style%20guides/golang-style-guide/) for the Go programming language,\r\nin which we documented some best practices for writing Go code. Please ensure your code adheres to the guidelines\r\noutlined in the guide.\r\n\r\n### Create a pull request\r\n\r\n[Create a pull request](https://help.github.com/articles/creating-a-pull-request/) with your changes. Please make sure\r\nto include the following:\r\n\r\n1. A description of the change, including a link to your GitHub issue.\r\n1. The output of your automated test run, preferably in a [GitHub Gist](https://gist.github.com/). We cannot run\r\n   automated tests for pull requests automatically due to [security\r\n   concerns](https://circleci.com/docs/2.0/oss/#security), so we need you to manually provide this\r\n   test output so we can verify that everything is working.\r\n1. Any notes on backwards incompatibility or downtime.\r\n\r\n#### Validate the Pull Request for Azure Platform\r\n\r\nIf you're contributing code for the [Azure Platform](https://azure.com) and if you have an active _Azure subscription_, it's recommended to follow the below guidelines after [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). If you're contributing code for any other platform (e.g., AWS, GCP, etc), you can skip these steps.\r\n\r\n> Once the Terratest maintainers add `Azure` tag and _Approve_ the PR, following pipeline will run automatically to perform a full validation of the Azure contribution. You also can run the pipeline manually on your forked repo by following the below guideline.\r\n\r\n\r\nWe have a separate CI pipeline for _Azure_ code. To run it on a forked repo:\r\n\r\n1. Run the following [Azure Cli](https://docs.microsoft.com/cli/azure/) command on your preferred Terminal to create Azure credentials and copy the output:\r\n\r\n    ```bash\r\n    az ad sp create-for-rbac --name \"terratest-az-cli\" --role contributor --sdk-auth\r\n    ```\r\n\r\n1. Go to Secrets settings page under `Settings` tab in your forked project, `https://github.com/<YOUR_GITHUB_ACCOUNT>/terratest/settings`, on GitHub.\r\n\r\n1. Create a new `Secret` named `AZURE_CREDENTIALS` and paste the Azure credentials you copied from the 1<sup>st</sup> step as the value\r\n\r\n    > `AZURE_CREDENTIALS` will be stored in _your_ GitHub account; neither the Terratest maintainers nor anyone else will have any access to it. Under the hood, GitHub stores your secrets in a secure, encrypted format (see: [GitHub Actions Secrets Reference](https://docs.github.com/en/free-pro-team@latest/actions/reference/encrypted-secrets) for more information). Once the secret is created, it's only possible to update or delete it; the value of the secret can't be viewed. GitHub uses a [libsodium sealed box](https://libsodium.gitbook.io/doc/public-key_cryptography/sealed_boxes) to help ensure that secrets are encrypted before they reach GitHub.\r\n\r\n1. Create a [new Personal Access Token (PAT)](https://github.com/settings/tokens/new) page under [Settings](https://github.com/settings/profile) / [Developer Settings](https://github.com/settings/apps), making sure `write:discussion` and `public_repo` scopes are checked. Click the _Generate token_ button and copy the generated PAT.\r\n\r\n1. Go back to settings/secrets in your fork and [Create a new Secret](https://docs.github.com/actions/reference/encrypted-secrets#creating-encrypted-secrets-for-a-repository) named `PAT`.  Paste the output from the 4<sup>th</sup> step as the value\r\n\r\n    > `PAT` will be stored in _your_ GitHub account; neither the Terratest maintainers nor anyone else will have any access to it. Under the hood, GitHub stores your secrets in a secure, encrypted format (see: [GitHub Actions Secrets Reference](https://docs.github.com/en/free-pro-team@latest/actions/reference/encrypted-secrets) for more information). Once the secret is created, it's only possible to update or delete it; the value of the secret can't be viewed. GitHub uses a [libsodium sealed box](https://libsodium.gitbook.io/doc/public-key_cryptography/sealed_boxes) to help ensure that secrets are encrypted before they reach GitHub.\r\n\r\n1. Go to Actions tab on GitHub (https://github.com/<GITHUB_ACCOUNT>/terratest/actions)\r\n\r\n1. Click `ci-workflow` workflow\r\n\r\n1. Click `Run workflow` button and fill the fields in the drop down\r\n    * _Repository Info_ : name of the forked repo (_e.g. xyz/terratest_)\r\n    * _Name of the branch_ : branch name on the forked repo (_e.g. feature/adding-some-important-module_)\r\n    * _Name of the official terratest repo_ : home of the target pr (_gruntwork-io/terratest_)\r\n    * PR number on the official terratest repo : pr number on the official terratest repo (_e.g. 14, 25, etc._).  Setting this value will leave a success/failure comment in the PR once CI completes execution.\r\n\r\n    * Skip provider registration : set true if you want to skip terraform provider registration for debug purposes (_false_ or _true_)\r\n\r\n1. Wait for the `ci-workflow` to be finished\r\n\r\n    > The pipeline will use the given Azure subscription and deploy real resources in your Azure account as part of running the test. When the tests finish, they will tear down the resources they created. Of course, if there is a bug or glitch that prevents the clean up code from running, some resources may be left behind, but this is rare. Note that these resources may cost you money! You are responsible for all charges in your Azure subscription.\r\n\r\n1. PR with the given _PR Number_ will have the result of the `ci-workflow` as a comment\r\n\r\n### Merge and release\r\n\r\nThe maintainers for this repo will review your code and provide feedback. Once the PR is accepted, they will merge the\r\ncode and release a new version, which you'll be able to find in the [releases page](https://github.com/gruntwork-io/terratest/releases).\r\n\r\n## Developing Terratest\r\n\r\n1. [Running tests](#running-tests)\r\n1. [Versioning](#versioning)\r\n1. [Developing For Azure](#developing-for-azure)\r\n\r\n### Running tests\r\n\r\nTerratest itself includes a number of automated tests.\r\n\r\n**Note #1**: Some of these tests create real resources in an AWS account. That means they cost money to run, especially\r\nif you don't clean up after yourself. Please be considerate of the resources you create and take extra care to clean\r\neverything up when you're done!\r\n\r\n**Note #2**: In order to run tests that access your AWS account, you will need to configure your [AWS CLI\r\ncredentials](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html). For example, you could\r\nset the credentials as the environment variables `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`.\r\n\r\n**Note #3**: Never hit `CTRL + C` or cancel a build once tests are running or the cleanup tasks won't run!\r\n\r\n**Prerequisite**: The tests expect Terraform, Terragrunt, Packer, and/or Docker to already be installed and in your `PATH`.\r\n\r\nTo run all the tests:\r\n\r\n```bash\r\ngo test -v -timeout 30m -p 1 ./...\r\n```\r\n\r\nTo run the tests in a specific folder:\r\n\r\n```bash\r\ncd \"<FOLDER_PATH>\"\r\ngo test -timeout 30m\r\n```\r\n\r\nTo run a specific test in a specific folder:\r\n\r\n```bash\r\ncd \"<FOLDER_PATH>\"\r\ngo test -timeout 30m -run \"<TEST_NAME>\"\r\n```\r\n\r\n### Versioning\r\n\r\nThis repo follows the principles of [Semantic Versioning](http://semver.org/). You can find each new release,\r\nalong with the changelog, in the [Releases Page](https://github.com/gruntwork-io/terratest/releases).\r\n\r\nDuring initial development, the major version will be 0 (e.g., `0.x.y`), which indicates the code does not yet have a\r\nstable API. Once we hit `1.0.0`, we will make every effort to maintain a backwards compatible API and use the MAJOR,\r\nMINOR, and PATCH versions on each release to indicate any incompatibilities.\r\n\r\n### Developing For Azure\r\n\r\nAzure supports multliple cloud environments. In order to properly register the correct environment for you test code, you need to use the Azure SDK Client Factory.\r\n\r\n#### Azure SDK Client Factory\r\n\r\nThis documentation provides and overview of the `client_factory.go` module, targeted use cases, and behaviors.  This module is intended to provide support for and simplify working with Azure's multiple cloud environments (Azure Public, Azure Government, Azure China, Azure Germany and Azure Stack).  Developers looking to contribute to additional support for Azure to Terratest should leverage client_factory and use the patterns below to add a resource REST client from Azure Go SDK.  By doing so, it provides a consistent means for developers using Terratest to test their Azure Infrastructure to connect to the correct cloud and its associated REST apis.\r\n\r\n##### Background\r\n\r\nThe Azure REST APIs support both Public and sovereign cloud environments (at the moment this includes Public, US Government, Germany, China, and Azure Stack environments).  If you are interacting with an environment other than public cloud, you need to set the base URI for the Azure REST API you are interacting with.\r\n\r\n###### Base URI\r\n\r\nYou must use the correct base URI's for the Azure REST API's (either directly or via Azure SDK for GO) to communicate with a cloud environment other than Azure Public. The Azure Go SDK supports this by using the `WithBaseURI` suffixed calls when creating service clients. For example, when using the `VirtualMachinesClient` with the public cloud, a developer would normally write code for the public cloud like so:\r\n\r\n```go\r\nimport (\r\n    \"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2019-07-01/compute\"\r\n)\r\n\r\nfunc SomeVMHelperMethod() {\r\n    subscriptionID := \"your subscription ID\"\r\n\r\n    // Create a VM client and return\r\n    vmClient, err := compute.NewVirtualMachinesClient(subscriptionID)\r\n\r\n    // Use client / etc\r\n}\r\n```\r\n\r\nHowever, this code will not work in non-Public cloud environments as the REST endpoints have different URIs depending on environment.  Instead, you need to use an alternative method (provided in the Azure REST SDK for Go) to get a properly configured client (*all REST API clients should support this alternate method*):\r\n\r\n```go\r\nimport (\r\n    \"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2019-07-01/compute\"\r\n)\r\n\r\nfunc SomeVMHelperMethod() {\r\n    subscriptionID := \"your subscription ID\"\r\n    baseURI := \"management.azure.com\"\r\n\r\n    // Create a VM client and return\r\n    vmClient, err := compute.NewVirtualMachinesClientWithBaseURI(baseURI, subscriptionID)\r\n\r\n    // Use client / etc\r\n}\r\n```\r\n\r\nUsing code similar to above, you can communicate with any Azure cloud environment just by changing the base URI that is passed to the clients (Azure Public shown in above example).\r\n\r\n##### Lookup Environment Metadata\r\n\r\nDevelopers MUST avoid hardcoding these base URI's.  Instead, they should be looked up from an authoritative source. The AutoRest-GO library (used by the Go SDK) provides such functionality. The `client_factory` module makes use of the AutoRest `EnvironmentFromName(envName string)` function to return the appropriate structure.  This method and Environment structure is documented on GoDoc [here](https://godoc.org/github.com/Azure/go-autorest/autorest/azure#EnvironmentFromName).\r\n\r\nTo configure different cloud environments, we will use the same `AZURE_ENVIRONMENT` environment variable that the Go SDK uses. This can currently be set to one of the following values:\r\n\r\n|Value                      |Cloud Environment  |\r\n|---------------------------|-------------------|\r\n|\"AzureChinaCloud\"          |ChinaCloud         |\r\n|\"AzureGermanCloud\"         |GermanCloud        |\r\n|\"AzurePublicCloud\"         |PublicCloud        |\r\n|\"AzureUSGovernmentCloud\"   |USGovernmentCloud  |\r\n|\"AzureStackCloud\"          |Azure stack        |\r\n\r\nWhen using the \"AzureStackCloud\" setting, you MUST also set the `AZURE_ENVIRONMENT_FILEPATH` variable to point to a JSON file containing your Azure Stack URI details.\r\n\r\n##### Putting it all together\r\n\r\n `client_factory` implements this pattern described above in order to instantiate and return properly configured *REST SDK for GO* clients so that test implementers don't have to consider REST API client implementation as long as they have the correct `AZURE_ENVIRONMENT` env setting.  If this environment variable is not set, the client will assume public cloud as the cloud environment to communicate with.  We strongly recommend developers creating Terratest helper methods for Azure use this pattern with client factory to create REST API clients.  This will reduce effort for Terratest users creating test for Azure resources.\r\n\r\nNote the following:\r\n\r\n* TERRAFORM uses [ARM_ENVIRONMENT](https://www.terraform.io/docs/backends/types/azurerm.html#environment) environment variable to set the correct cloud environment.  \r\n* The default behavior of the `client_factory` is to use the AzurePublicCloud environment. This requires no work from the developer to configure, and ensures consistent behavior with the current SDK code.\r\n\r\n###### Wait, I don't see the client in client factory for the rest api I want to interact with\r\n\r\n If you require a client that is not already implemented in client factory for your helper method, you will need to create a corresponding method that instantiates the client and accepts base URI following the patterns discussed.  Below is a walkthrough for adding a client to client factory.\r\n\r\n##### Walkthrough, adding a client to client_factory\r\n\r\n###### Add your client namespace to client factory\r\n\r\nIn the Azure SDK for GO, each service should have a module that implements that services client.  You can find the correct module [here](https://godoc.org/github.com/Azure/azure-sdk-for-go).  Add that module to the client factory imports.  Below is an example for client imports that shows clients for compute, container service and subscriptions.\r\n\r\n{% include examples/explorer.html example_id='client-factory' file_id='client_factory_code' class='wide quick-start-examples' skip_learn_more=true skip_view_on_github=true skip_tags=true snippet_id='client_factory_example.imports' %}\r\n\r\n###### Add your client method to instantiate the client\r\n\r\nThe next step is to add your method to instantiate the client.  Below is an example of adding the method to create a client for Virtual Machines, note that we lookup the environment using `getEnvironmentEndpointE` and then pass that base URI to the actual method on the Virtual Machines Module to create the client `NewVirtualMachinesClientWithBaseURI`.\r\n\r\n{% include examples/explorer.html example_id='client-factory' file_id='client_factory_code' class='wide quick-start-examples' skip_learn_more=true skip_view_on_github=true skip_tags=true snippet_id='client_factory_example.CreateClient' %}\r\n\r\n###### Add a unit test to client_factory_test.go\r\n\r\nIn order to ensure that your CreateClient method works properly, add a unit test to `client_factory_test.go`.  The unit test MUST assert that the base URI is correctly set for your client.  Some key points for writing your unit test are:\r\n\r\n- Use table-driven testing to test the various combinations of cloud environments\r\n- Give the test case a descriptive name so it is easy to identify which test failed.\r\n- PRs will be rejected if a client is added without a corresponding unit test.\r\n\r\nBelow is an example of the Virtual Machines client unit test:\r\n\r\n{% include examples/explorer.html example_id='client-factory' file_id='client_factory_test' class='wide quick-start-examples' skip_learn_more=true skip_view_on_github=true skip_tags=true snippet_id='client_factory_example.UnitTest' %}\r\n\r\n###### Use your CreateClient method in your helper\r\n\r\nWe now can use this client creation method in our helpers to create a Virtual Machines client.  Below is an example for how to call into this create method from `client_factory`:\r\n\r\n{% include examples/explorer.html example_id='client-factory' file_id='client_factory_helper' class='wide quick-start-examples' skip_learn_more=true skip_view_on_github=true skip_tags=true snippet_id='client_factory_example.helper' %}\r\n"
  },
  {
    "path": "docs/_docs/04_community/license.md",
    "content": "---\r\nlayout: collection-browser-doc\r\ntitle: License\r\ncategory: community\r\nexcerpt: >-\r\n  This code is released under the Apache 2.0 License. Read more here.\r\ntags: [\"license\"]\r\norder: 402\r\nnav_title: Documentation\r\nnav_title_link: /docs/\r\n---\r\n\r\n## License\r\n\r\nThis code is released under the Apache 2.0 License. See [LICENSE](https://github.com/gruntwork-io/terratest/blob/main/LICENSE){:target=\"_blank\"} and [NOTICE](https://github.com/gruntwork-io/terratest/blob/main/NOTICE){:target=\"_blank\"} for more details.\r\n"
  },
  {
    "path": "docs/_docs/04_community/support.md",
    "content": "---\r\nlayout: collection-browser-doc\r\ntitle: Support\r\ncategory: community\r\nexcerpt: >-\r\n  Need help?\r\ntags: [\"support\", \"community\"]\r\norder: 401\r\nnav_title: Documentation\r\nnav_title_link: /docs/\r\n---\r\n\r\n## Github Discussions\r\n\r\nSearch our [Knowledge Base](https://github.com/gruntwork-io/knowledge-base/discussions) to find existing questions or ask your own. Github Discussions is a good place for general discussions and questions.\r\n\r\n## Github Issues\r\n\r\nRead through [existing issues](https://github.com/gruntwork-io/terratest/issues) or post a new one. Github issues is a good place to:\r\n\r\n- report a bug,\r\n\r\n- ask for a help,\r\n\r\n- ask for improvements,\r\n\r\n- to start contributing by solving simple issues.\r\n\r\n## Commercial support\r\n\r\nDoes your company rely on Terratest in production? If so, you can get commercial support directly from Gruntwork, the creators of Terratest! Check out the [Gruntwork Support Page](https://gruntwork.io/support) for more details.\r\n"
  },
  {
    "path": "docs/_includes/built-by.html",
    "content": "<div class=\"built-by-cmp box-component\">\n  <div class=\"box shadow\">\n    <h2>Built by <strong>Gruntwork</strong></h2>\n    <p class=\"subtitle\">Your entire infrastructure. Defined as code. In about a day.</p>\n    <a href=\"https://gruntwork.io/\" target=\"_blank\" class=\"btn btn-primary btn-lg\">Explore Gruntwork.io</a>\n  </div>\n</div>\n"
  },
  {
    "path": "docs/_includes/canonical-url.html",
    "content": "{% if include.url %}{% assign url = include.url %}{% else %}{% assign url = page.url %}{% endif %}{{ site.url | replace:'www.','' }}{{site.baseurl}}{{ url | replace:'index.html','' }}\n"
  },
  {
    "path": "docs/_includes/collection_browser/_cta-section.html",
    "content": "{% include links-n-built-by.html %}\n"
  },
  {
    "path": "docs/_includes/collection_browser/_doc-header.html",
    "content": "<div class=\"cb-doc-header\">\n  <ol class=\"breadcrumb text-center\">\n    {% assign crumbs = page.url | split: '/' %}\n    <a href=\"{{site.baseurl}}/{{ page.collection }}\" ga-on=\"click\" ga-event-category=\"{{ page.collection }}-{{ page.title | slugify }}\" ga-event-action=\"{{ page.collection }}-main-index\">{{ page.collection | capitalize }}</a>\n    {% for crumb in crumbs offset: 2 %}\n      {% if forloop.last %}\n        <a href=\"#\"> / <span> {{ page.title | replace: '-',' ' | replace: '%20', ' ' }}</span></a>\n      {% else %}\n        <a href=\"{{ site.baseurl }}/{{ page.collection }}#{{ crumb | replace: '%20', '-' }}\" ga-on=\"click\" ga-event-category=\"{{ page.collection }}-{{ page.title | slugify }}\" ga-event-action=\"{{ page.collection }}-breadcrumb-{{ crumb }}\">/ <span>{{ crumb | replace:'-',' ' | replace: '%20', ' ' | capitalize }}</span></a>\n      {% endif %}\n    {% endfor %}\n  </ol>\n  <div class=\"cb-doc-title text-center\">\n    {% if page.image %}\n      <img class=\"img-center cb-doc-title-image\" src=\"{{ site.baseurl }}{{ page.image }}\" alt=\"post logo\">\n    {% endif %}\n    <h1>{{ page.title }}</h1>\n    {% if page.tags %}\n      {% for tag in page.tags %}\n        <span class=\"badge badge-pill badge-default\">\n          {% capture tag-name %}{{tag | upcase }}{% endcapture %}\n          {{tag-name | capitalize}}\n        </span>\n      {% endfor %}\n    {% endif %}\n  </div>\n</div>\n"
  },
  {
    "path": "docs/_includes/collection_browser/_doc-page.html",
    "content": "<div class=\"cb-doc-detail\">\n  <div class=\"container-fluid\">\n    <div class=\"row\">\n      <div class=\"col-xs-12\">\n        {% include collection_browser/_doc-header.html %}\n      </div>\n    </div>\n  </div>\n  <div class=\"container-cb-lg\">\n    <div class=\"row equal\">\n      {% assign data_scroll_after_selector = '.cb-doc-header' %}\n      {% assign data_scroll_until_selector = '.cb-doc-detail' %}\n      {% include collection_browser/_sidebar.html data_scroll_until_selector=data_scroll_until_selector data_scroll_after_selector=data_scroll_after_selector %}\n      <div class=\"col-xs-12 col-md-7 col-lg-9 cb-section-white\">\n        <div class=\"cb-doc-content js-scroll-spy\" data-scroll-spy-nav-selector=\"#toc\">\n            {{ content }}\n        </div>\n      </div>\n    </div>\n  </div>\n</div>\n"
  },
  {
    "path": "docs/_includes/collection_browser/_doc-thumb.html",
    "content": "{% assign cb_doc_card_class = '' %}\n\n{% if doc.index_list !=nil and doc.index_list.no_hover_enlarge_effect == true %}\n  {% assign cb_doc_card_class = 'no-hover-enlarge' %}\n{% endif %}\n\n<div\n  class=\"cb-doc-card card-shadow {{ cb_doc_card_class }}\"\n  id=\"{{ doc.title | slugify | downcase | split: ' ' | join: '-' | append: '-card' }}\"\n  >\n  <div class=\"card\" ga-on=\"click\" ga-event-category=\"cb-doc-{{ doc.title | slugify }}\" ga-event-action=\"cb-doc-click\">\n    <div class=\"row no-padding\">\n\n      {% if doc.image %}\n        {% if doc.index_list !=nil and doc.index_list.read_more_btn == true %}\n          <a href=\"{{ site.baseurl }}{{ doc.url }}\" class=\"read-more-btn btn btn-primary\">Read more</a>\n        {% endif %}\n        {% unless doc.index_list !=nil and doc.index_list.disable_card_link == true %}\n          <a href=\"{{ site.baseurl }}{{ doc.url }}\">\n        {% endunless %}\n          <div class=\"col-xs-2 img-inner no-padding\">\n            <img src=\"{{ site.baseurl }}{{ doc.image }}\" alt=\"card image\" class=\"img-center\">\n          </div>\n          <div class=\"col-xs-10 card-body cb-doc-card-description float-right\">\n            {% if doc.index_list !=nil and doc.index_list.disable_card_link == true %}\n              <a href=\"{{ site.baseurl }}{{ doc.url }}\" class=\"card-title__link\">\n                <h5 class=\"card-title\"><b>{{ doc.title }}</b></h5>\n              </a>\n            {% else %}\n              <h5 class=\"card-title\"><b>{{ doc.title }}</b></h5>\n            {% endif %}\n            {% include collection_browser/_doc-thumb__excerpt.html doc=doc %}\n          </div>\n        {% unless doc.index_list !=nil and doc.index_list.disable_card_link == true %}\n          </a>\n        {% endunless %}\n      {% else %}\n        {% if doc.index_list !=nil and doc.index_list.read_more_btn == true %}\n          <a href=\"{{ site.baseurl }}{{ doc.url }}\" class=\"read-more-btn btn btn-primary\">Read more</a>\n        {% endif %}\n        {% unless doc.index_list !=nil and doc.index_list.disable_card_link == true %}\n          <a href=\"{{ site.baseurl }}{{ doc.url }}\">\n        {% endunless %}\n          <div class=\"col-xs-12 card-body cb-doc-card-description float-right\">\n            {% if doc.index_list !=nil and doc.index_list.disable_card_link == true %}\n              <a href=\"{{ site.baseurl }}{{ doc.url }}\" class=\"card-title__link\">\n                <h5 class=\"card-title\"><b>{{ doc.title }}</b></h5>\n              </a>\n            {% else %}\n              <h5 class=\"card-title\"><b>{{ doc.title }}</b></h5>\n            {% endif %}\n            {% include collection_browser/_doc-thumb__excerpt.html doc=doc %}\n          </div>\n        {% unless doc.index_list !=nil and doc.index_list.disable_card_link == true %}\n          </a>\n        {% endunless %}\n      {% endif %}\n    </div>\n  </div>\n</div>\n"
  },
  {
    "path": "docs/_includes/collection_browser/_doc-thumb__excerpt.html",
    "content": "{% if doc.excerpt_md %}\n  {{ doc.excerpt_md | markdownify }}\n{% elsif doc.excerpt_html %}\n  {{ doc.excerpt_html }}\n{% else %}\n  <p class=\"card-text\">{{ doc.excerpt | strip_html }}</p>\n{% endif %}\n"
  },
  {
    "path": "docs/_includes/collection_browser/_docs-list.html",
    "content": "<div class=\"col-xs-12 col-md-7 col-lg-9\">\n  <div class=\"row\">\n    <div class=\"cb-doc-listing js-scroll-spy\" data-scroll-spy-nav-selector=\"#toc\">\n      {% for doc_group in include.docs_grouped %}\n        <h3 class=\"category-head\">{{ doc_group.name | capitalize | replace: '-',' ' | replace: '%20', ' ' }}</h3>\n        {% assign sorted = doc_group.items | sort: 'order' %}\n        {% for doc in sorted %}\n          {% include collection_browser/_doc-thumb.html doc=doc %}\n        {% endfor %}\n      {% endfor %}\n    </div>\n  </div>\n</div>\n"
  },
  {
    "path": "docs/_includes/collection_browser/_no-search-results.html",
    "content": "<div id=\"no-matches\" class=\"cb-section-white container-fluid\">\n    <div class=\"row text-center\">\n        <div class=\"no-results no-results-space\">\n            <div class=\"col-md-6 col-md-offset-3\" >\n                <img src=\"{{site.baseurl}}/assets/img/no-search-results.png\" alt=\"no search results\" /><br />\n                <label>No Results Found</label>\n                <p>We couldn’t find any results matching your search.</p>\n            </div>\n        </div>\n    </div>\n</div>\n"
  },
  {
    "path": "docs/_includes/collection_browser/_search.html",
    "content": "<div class='cb-search-cmp container-fluid' data-collection_name='{{collection_name}}'>\n  <div class=\"row\">\n      <div class=\"searchrow center\">\n          <div class=\"col-xs-6 col-sm-7 no-padding\">\n              <div class=\"input-group\">\n                  <div class=\"input-group-btn\" >\n                      <div class=\"btn-group\" role=\"group\">\n                          <button type=\"button\" class=\"btn btn-primary\" >\n                              <span class=\"glyphicon glyphicon-search\" aria-hidden=\"true\"></span>\n                          </button>\n                      </div>\n                  </div>\n                  <input id=\"cb-search-box-{{collection_name}}\" type=\"text\" class=\"form-control guides-input-box\" placeholder=\"Search....\" data-collection_name='{{collection_name}}'>\n              </div>\n          </div>\n          <div class=\"col-xs-2 col-sm-1 no-padding\" id=\"tags-filter\">\n              <button class=\"btn tags-filter\" type=\"button\" data-toggle=\"collapse\" data-target=\"#filter-options\" aria-expanded=\"false\" aria-controls=\"filter-options\">\n                  Filter <i class=\"fa fa-caret-down\" aria-hidden=\"true\"></i>\n              </button>\n          </div>\n      </div>\n      <div class=\"row\">\n          <div class=\"filter-options center\">\n              <div class=\"col-sm-8 no-padding\">\n                  <div class=\"collapse\" id=\"filter-options\" aria-expanded=\"true\">\n                      <div class=\"card card-body\">\n                          <p>Tags</p>\n                          <div class=\"tags\">\n                            {% for tag in tags %}\n                              {% assign tag_name = tag %}\n                              <div class=\"checkbox\"><input value=\"{{ tag }}\" id=\"filter-{{ tag }}\" type=\"checkbox\"><label for=\"filter-{{ tag }}\">{{ tag_name }}</label></div>\n                            {% endfor %}\n                          </div>\n                      </div>\n                  </div>\n              </div>\n          </div>\n      </div>\n  </div>\n</div>\n"
  },
  {
    "path": "docs/_includes/collection_browser/_sidebar.html",
    "content": "<div class=\"col-md-2-5\">\n  <div id=\"toc\" class=\"cb-doc-sidebar js-scroll-with-user\" data-scroll-after-selector=\"{{ include.data_scroll_after_selector }}\" data-scroll-until-selector=\"{{ include.data_scroll_until_selector }}\" data-nav-current-cat=\"{{ page.categories | downcase | slugify }}\" data-nav-current-page=\"{{ page.title | slugify }}\">\n    <div class=\"toc-toggle visible-xs visible-sm\">\n      <button id=\"toc-toggle-open\" class=\"toc-toggle__button\"><span class=\"glyphicon glyphicon-menu-hamburger\"></span></button>\n      <button id=\"toc-toggle-close\" class=\"toc-toggle__button\"><span class=\"glyphicon glyphicon-remove\"></span></button>\n    </div>\n\n    {% capture nav_title %}{{page.nav_title}}{% endcapture %}\n    {% capture nav_title_link %}{{page.nav_title_link}}{% endcapture %}\n\n    {% if nav_title != empty and nav_title_link != empty  %}\n      <div class=\"nav-doc-collection-link\">\n        <a href=\"{{ site.baseurl }}{{ page.nav_title_link }}\"><span class=\"glyphicon glyphicon-share-alt icon-flipped\"></span> {{ page.nav_title | replace: \"-\", \" \" }}</a>\n      </div>\n    {% elsif nav_title != empty and nav_title_link == empty  %}\n      <div class=\"nav-doc-collection-link\">\n        {{ page.nav_title | replace: \"-\", \" \" }}\n      </div>\n    {% endif %}\n\n    {% include collection_browser/navigation/_collection_toc.html collection_name='docs' %}\n\n  </div>\n</div>\n"
  },
  {
    "path": "docs/_includes/collection_browser/browser.html",
    "content": "<!-- Basic collection data -->\n{% assign collection_name = include.collection_name %}\n{% assign nav_title = include.nav_title %}\n{% assign nav_title_link = include.nav_title_link %}\n\n<!-- Get categories and tags from collection -->\n{% assign categories = \"\" | split: \"|\" %}\n{% assign tags = \"\" | split: \"|\" %}\n{% for doc in include.collection %}\n    {% for category in doc.categories %}\n        {% assign categories = categories | push: category | uniq %}\n    {% endfor %}\n\n    {% for tag in doc.tags %}\n        {% assign tags = tags | push: tag | uniq %}\n    {% endfor %}\n{% endfor %}\n\n<!-- Get posts grouped in categories -->\n{% assign docs_grouped = include.collection | group_by: \"category\" %}\n\n  <div class=\"main\">\n\n    <div class=\"cb-section-white\">\n\n        <!-- Search -->\n        {% include collection_browser/_search.html tags=tags collection_name=include.collection_name %}\n        <div class=\"row\">\n          <div class=\"container-cb-lg\">\n            <div id=\"cb-doc-listings\">\n              <div class=\"row equal\">\n\n                <!-- Sidebar -->\n                {% assign data_scroll_after_selector = '.cb-search-cmp' %}\n                {% assign data_scroll_until_selector = '.cb-doc-listing' %}\n                {% include collection_browser/_sidebar.html data_scroll_until_selector=data_scroll_until_selector data_scroll_after_selector=data_scroll_after_selector %}\n\n                <!-- Listing -->\n                {% include collection_browser/_docs-list.html categories=categories collection_name=include.collection_name docs_grouped=docs_grouped %}\n\n              </div>\n            </div>\n          </div>\n        </div>\n\n      <!-- No results -->\n      {% include collection_browser/_no-search-results.html %}\n\n    </div>\n\n  </div>\n\n  <!-- CTA Section -->\n  {% include collection_browser/_cta-section.html %}\n\n  <script>\n      var cName = \"bc_{{include.collection_name}}Entries\"\n      window[cName] = [\n          {% for doc_group in docs_grouped %}\n            {% for doc in doc_group.items %}\n              {\n                  \"id\"       : \"{{ doc.title | strip_html | slugify | downcase | split: ' ' | join: '-' | append: '-card' }}\",\n                  \"title\"    : \"{{ doc.title | strip_html | slugify | escape | downcase}}\",\n                  \"category\" : \"{{ doc.categories | join: ', ' | downcase}}\",\n                  \"excerpt\" : \"{{ doc.excerpt | strip_html | strip_newlines | escape | downcase}}\",\n                  \"content\"  : {{ doc.content | strip_html | strip_newlines | jsonify | downcase }},\n                  \"tags\"     : \"{{ doc.tags | join: ', ' | downcase}}\",\n                  \"cloud\"     : \"{{ doc.cloud | join: ', ' | downcase}}\"\n              },\n            {% endfor %}\n            {% unless forloop.last %},{% endunless %}\n          {% endfor %}\n      ];\n  </script>\n"
  },
  {
    "path": "docs/_includes/collection_browser/navigation/_collection_toc.html",
    "content": "{% capture tocWorkspace %}\r\n  {% comment %}\r\n    To build navigation:\r\n    == A1: Group docs by categories\r\n    == A2: [Loop] Go by categories...\r\n    == A3: ADD CATEGORY LINK TO THE NAV (It's the uppermost level of links in navigation.)\r\n    == A4: [Loop] Go by documents in the category...\r\n      == B1: ADD DOC'S TITLE TO NAV\r\n      == B2: [Loop] Go by headings in a doc...\r\n        == B3: ADD HEADING TO THE NAV\r\n    == A6: Final steps\r\n        Add ID and classes to the navigation, and `markdonify` content.\r\n  {% endcomment %}\r\n\r\n  {% capture my_toc %}{% endcapture %}\r\n\r\n  {% assign collection_name = include.collection_name | default: 'posts' %}\r\n  {% comment %}\r\n    ======================= A1: GROUP BY CATEGORIES =======================\r\n  {% endcomment %}\r\n  {% assign collection_grouped = site[collection_name] | group_by: 'category' %}\r\n  {% assign sort_by = include.sort_by | default: 'order' %}\r\n  {% assign orderedList = include.ordered | default: false %}\r\n  {% assign minHeader = include.h_min | default: 1 %}\r\n  {% assign maxHeader = include.h_max | default: 6 %}\r\n  {% assign firstHeader = true %}\r\n  {% assign level = 0 %}\r\n\r\n  {% assign docHeaderIndentAmount = 1 %}\r\n\r\n  {% comment %}\r\n    ======================= A2: CATEGORIES LOOP =======================\r\n  {% endcomment %}\r\n  {% for category in collection_grouped %}\r\n\r\n    {% capture col_id %}{{ collection_name | downcase | replace: ' ', '-' | replace: ':', ''}}{% endcapture %}\r\n    {% capture cat_id %}{{ category.name | downcase | replace: ' ', '-' | replace: ':', ''}}{% endcapture %}\r\n\r\n    {% comment %}\r\n      ======================= A3: BUILD CATEGORY LINK =======================\r\n      The category link is defined as link to the heading on index page.\r\n    {% endcomment %}\r\n\r\n    {% capture category_link %}{{ site.baseurl }}/{{ collection_name }}#{{ category.name }}{% endcapture %}\r\n    {% capture my_toc %}{{ my_toc }}\r\n- <span\r\n  class='nav-collapse-handler category-head collapsed'\r\n  id='cat-nav-id-{{ col_id }}-{{ cat_id }}'\r\n  data-toggle='collapse'\r\n  role='button'\r\n  aria-expanded='false'\r\n  aria-controls='{{ col_id }}-{{ cat_id }}'>\r\n  <span class='arrow-icon glyphicon glyphicon-triangle-bottom'></span>\r\n</span>[{{ category.name | replace: '-',' ' | replace: '%20', ' ' | capitalize }}]({{ category_link }}){% endcapture %}\r\n\r\n\r\n    {% comment %}\r\n      ======================= A4: DOCS LOOP =======================\r\n      Loop over the docs within a category.\r\n    {% endcomment %}\r\n    {% assign items = category.items | sort: sort_by %}\r\n    {% for doc in items %}\r\n\r\n        {% capture n_cont %}{{ doc.content }}{% endcapture %}\r\n        {% assign n_cont_md = n_cont | markdownify %}\r\n\r\n        {% comment %}\r\n          ======================= B1: ADD DOC TITLE TO NAV =======================\r\n          Add document's title to the navigation.\r\n        {% endcomment %}\r\n        {% capture doc_id %}{{ doc.title | downcase | downcase | replace: ' ', '-' | replace: ':', ''}}{% endcapture %}\r\n        {% assign doc_url = doc.url %}\r\n        {% if doc.redirect_to %}\r\n          {% assign doc_url = doc.redirect_to.first %}\r\n        {% endif %}\r\n        {% capture doc_url_prefix %}{{ doc_url | slice: 0, 4 }}{% endcapture %}\r\n        {% unless doc_url_prefix == \"http\" %}\r\n          {% capture doc_url %}{{ site.baseurl }}{{ doc_url }}{% endcapture %}\r\n        {% endunless %}\r\n        {% capture target_blank %}{% endcapture %}\r\n        {% if doc.target_blank %}\r\n          {% capture target_blank %}{:target=\"_blank\"}{% endcapture %}\r\n        {% endif %}\r\n        {% capture my_toc %}{{ my_toc }}\r\n  - <span\r\n    class='nav-collapse-handler category-head collapsed'\r\n    id='cat-nav-id-{{ doc_id }}-{{ cat_id }}'\r\n    data-toggle='collapse'\r\n    role='button'\r\n    aria-expanded='false'\r\n    aria-controls='{{ doc_id }}-{{ cat_id }}'>\r\n  <span class='arrow-icon glyphicon glyphicon-triangle-bottom'></span>\r\n  </span>[{{ doc.title }}]({{ doc_url }}){{ target_blank }}{% endcapture %}\r\n\r\n        {% comment %}\r\n          =============== B2: Find headings in doc and add to nav ================\r\n          Goes heading by heading (h2, h3, h4, etc.), and adds it to the navigation.\r\n        {% endcomment %}\r\n        {% assign nodes = n_cont_md | split: '<h' %}\r\n        {% for node in nodes %}\r\n\r\n          {% if node == \"\" %}\r\n              {% continue %}\r\n          {% endif %}\r\n\r\n          {% assign headerLevel = node | replace: '\"', '' | slice: 0, 1 | times: 1 %}\r\n\r\n          {% if headerLevel < minHeader or headerLevel > maxHeader %}\r\n              {% continue %}\r\n          {% endif %}\r\n\r\n          {% if firstHeader %}\r\n              {% assign firstHeader = false %}\r\n              {% assign minHeader = headerLevel %}\r\n          {% endif %}\r\n\r\n          {% assign indentAmount = headerLevel | minus: minHeader | plus: docHeaderIndentAmount %}\r\n          {% assign _workspace = node | split: '</h' %}\r\n\r\n          {% assign _idWorkspace = _workspace[0] | split: 'id=\"' %}\r\n          {% assign _idWorkspace = _idWorkspace[1] | split: '\"' %}\r\n          {% assign html_id = _idWorkspace[0] %}\r\n\r\n          {% assign _classWorkspace = _workspace[0] | split: 'class=\"' %}\r\n          {% assign _classWorkspace = _classWorkspace[1] | split: '\"' %}\r\n          {% assign html_class = _classWorkspace[0] %}\r\n\r\n          {% if html_class contains \"no_toc\" %}\r\n              {% continue %}\r\n          {% endif %}\r\n\r\n          {% capture _hAttrToStrip %}{{ _workspace[0] | split: '>' | first }}>{% endcapture %}\r\n          {% assign header = _workspace[0] | replace: _hAttrToStrip, '' %}\r\n\r\n          {% assign space = '' %}\r\n          {% for i in (1..indentAmount) %}\r\n              {% assign space = space | prepend: '    ' %}\r\n          {% endfor %}\r\n\r\n          {% unless include.item_class == blank %}\r\n              {% capture listItemClass %}{{ include.item_class | replace: '%level%', headerLevel }}{% endcapture %}\r\n          {% endunless %}\r\n\r\n          {% capture heading_body %}{% if include.sanitize %}{{ header | strip_html }}{% else %}{{ header }}{% endif %}{% endcapture %}\r\n\r\n          {% comment %}\r\n            ======================= B3: ADD HEADING TO THE NAV =======================\r\n            Go heading-by-heading (h2, h3, h4, etc.), and add it to the navigation.\r\n          {% endcomment %}\r\n          {% capture my_toc %}{{ my_toc }}\r\n{{ space }}- <span\r\n  class='nav-collapse-handler category-head collapsed'\r\n  id='cat-nav-id-{{ doc_id }}-{{ cat_id }}-{{ increment level }}'\r\n  data-toggle='collapse'\r\n  role='button'\r\n  aria-expanded='false'\r\n  aria-controls='{{ doc_id }}-{{ cat_id }}-{{ increment level }}'>\r\n  <span class='arrow-icon glyphicon glyphicon-triangle-bottom'></span>\r\n</span>[{{ heading_body | replace: \"|\", \"\\|\" }}]({% if include.baseurl %}{{ include.baseurl }}{% endif %}{{ site.baseurl }}{{doc.url}}#{{ html_id }}){% if include.anchor_class %}{:.{{ include.anchor_class }}}{% endif %}{% endcapture %}\r\n\r\n        {% endfor %}\r\n\r\n\r\n    {% endfor %}\r\n\r\n  {% endfor %}\r\n\r\n\r\n  {% comment %}\r\n    ======================= A6: FINAL STEPS =======================\r\n  {% endcomment %}\r\n\r\n  {% if include.class %}\r\n      {% capture my_toc %}{:.{{ include.class }}}\r\n{{ my_toc | lstrip }}{% endcapture %}\r\n  {% endif %}\r\n  {% if include.id %}\r\n      {% capture my_toc %}{: #{{ include.id }}}\r\n{{ my_toc | lstrip }}{% endcapture %}\r\n  {% endif %}\r\n\r\n{% endcapture %}\r\n{% assign tocWorkspace = '' %}\r\n\r\n{{ my_toc | markdownify | strip }}\r\n"
  },
  {
    "path": "docs/_includes/examples/example.html",
    "content": "<div id=\"example__block-{{ include.example.id }}\" class=\"examples__block active\" data-target=\"{{ include.example.id }}\">\r\n\r\n  <div class=\"examples__tabs\" data-target=\"{{ include.example.id }}\">\r\n    {% for file in include.example.files %}\r\n      {% if include.file_id == nil or include.file_id == file.id %}\r\n        {% assign assign_class = '' %}\r\n        {% if file.default or include.file_id == file.id %}\r\n          {% assign assign_class = 'active' %}\r\n        {% endif %}\r\n\r\n        {% assign url_split = file.url | split: '/' %}\r\n        {% assign file_name = url_split.last %}\r\n        {% if file.name %}\r\n          {% assign file_name = file.name %}\r\n        {% endif %}\r\n\r\n        <div class=\"tab {{ assign_class }}\" data-target=\"#example__code-{{ include.example.id }}-{{ file_name | replace: '.', '-' | replace: ' ', '-' }}\">\r\n          <label>{{ file_name }}</label>\r\n        </div>\r\n      {% endif %}\r\n    {% endfor %}\r\n  </div>\r\n\r\n  {% for file in include.example.files %}\r\n    {% if include.file_id == nil or include.file_id == file.id %}\r\n      {% assign assign_class = '' %}\r\n      {% if file.default or include.file_id == file.id %}\r\n        {% assign assign_class = 'active' %}\r\n      {% endif %}\r\n\r\n      {% assign url_split = file.url | split: '/' %}\r\n      {% assign file_name = url_split.last %}\r\n      {% if file.name %}\r\n        {% assign file_name = file.name %}\r\n      {% endif %}\r\n\r\n      {% assign prism_lang = '' %}\r\n      {% assign file_name_splitted = file.url | split: '.' %}\r\n      {% if site.data.prism_extends[file_name_splitted.last] %}\r\n        {% assign prism_lang = site.data.prism_extends[file_name_splitted.last] %}\r\n      {% elsif file_name_splitted.last %}\r\n        {% assign prism_lang = file_name_splitted.last %}\r\n      {% endif %}\r\n      {% if file.prism_lang %}\r\n        {% assign prism_lang = file.prism_lang %}\r\n      {% endif %}\r\n\r\n      <div\r\n        id=\"example__code-{{ include.example.id }}-{{ file_name | replace: '.', '-' | replace: ' ', '-' }}\"\r\n        class=\"examples__code examples__code--example {{ assign_class }}\"\r\n        data-example=\"{{ include.example.id }}\"\r\n        data-target=\"{{ file_name | replace: '.', '-' | replace: ' ', '-' }}\"\r\n        data-url=\"{{ site.github_api_url }}{{ file.url }}\"\r\n        data-skip-tags=\"{{include.skip_tags}}\"\r\n        data-snippet-id=\"{{include.snippet_id}}\"\r\n        >\r\n        <pre><code class=\"language-{{ prism_lang }}\">Loading...</code></pre>\r\n        {% unless include.skip_view_on_github %}\r\n          <div class=\"example__file-link\">\r\n            <p>View on GitHub:</p>\r\n            <a href=\"https://{{ site.repository }}/tree/main{{ file.url }}\">{{ site.repository }}{{ file.url }}</a>\r\n          </div>\r\n        {% endunless %}\r\n      </div>\r\n    {% endif %}\r\n  {% endfor %}\r\n\r\n  {% if include.example.learn_more and include.skip_learn_more != true %}\r\n  <div class=\"examples__learn-more\">\r\n    <span class=\"title\">Learn more:</span>\r\n    <ul>\r\n      {% for link in include.example.learn_more %}\r\n        <li>\r\n          <a href='{{ link.url }}'>\r\n            {{ link.name }}\r\n          </a>\r\n        </li>\r\n      {% endfor %}\r\n    </ul>\r\n  </div>\r\n  {% endif %}\r\n\r\n</div>\r\n"
  },
  {
    "path": "docs/_includes/examples/explorer.html",
    "content": "<div id=\"{{ include.id }}\" class=\"examples__container {{ include.class }}\">\r\n  <nav class=\"examples__nav\">\r\n    <div class=\"hidden-navs\">\r\n      {% for example in site.data.examples %}\r\n        {% if include.example_id == nil or include.example_id == example.id %}\r\n          {% assign render_example = false %}\r\n          \r\n          {% if example.display_in_examples == nil or example.display_in_examples == true %}\r\n            {% assign render_example = true %}\r\n          {% endif %}\r\n\r\n          {% if render_example %}\r\n            <div\r\n              class=\"examples__nav-item nav-{{ example.id }}\"\r\n              data-id=\"{{ example.id }}\"\r\n              data-name=\"{{ example.name }}\"\r\n              data-url=\"{{ site.github_api_url }}{{ example.url }}\"\r\n              >\r\n              <img src=\"{{ site.baseurl }}{{ example.image }}\" alt=\"{{ example.name }}\" />\r\n            </div>\r\n          {% endif %}\r\n        {% endif %}\r\n      {% endfor %}\r\n    </div>\r\n\r\n    {% if include.example_id == nil %}\r\n      <div class=\"hidden-navs__static-links\">\r\n        <a\r\n          href=\"https://github.com/gruntwork-io/terratest/tree/main/examples\"\r\n          class=\"examples__nav-item static-link nav-{{ example.id }}\"\r\n          target=\"_blank\"\r\n          >\r\n          See more examples\r\n        </a>\r\n      </div>\r\n\r\n      <div class=\"navs\">\r\n        <div class=\"navs__visible-bar\"></div>\r\n        <div class=\"navs__dropdown-input\"></div>\r\n        <div class=\"navs__dropdown-arrow\">\r\n          <span class=\"glyphicon glyphicon-menu-down\"></span>\r\n        </div>\r\n        <div class=\"navs__dropdown-menu\"></div>\r\n      </div>\r\n    {% endif %}\r\n  </nav>\r\n\r\n  {% for example in site.data.examples %}\r\n    {% if include.example_id == nil or include.example_id == example.id %}\r\n      {% assign render_example = true %}\r\n      \r\n      {% if render_example %}\r\n        {% include examples/example.html example=example file_id=include.file_id skip_learn_more=include.skip_learn_more skip_view_on_github=include.skip_view_on_github skip_tags=include.skip_tags snippet_id=include.snippet_id %}\r\n      {% endif %}\r\n    {% endif %}\r\n  {% endfor %}\r\n\r\n</div>\r\n"
  },
  {
    "path": "docs/_includes/favicon.html",
    "content": "{% assign base_favicon_url = site.assets_base_url | append: 'img/favicon' | prepend: site.baseurl %}\n\n<link rel=\"icon\" type=\"image/x-icon\" href=\"{{ base_favicon_url }}/favicon.ico\">\n<link rel=\"apple-touch-icon\" sizes=\"57x57\" href=\"{{ base_favicon_url }}/apple-icon-57x57.png\">\n<link rel=\"apple-touch-icon\" sizes=\"60x60\" href=\"{{ base_favicon_url }}/apple-icon-60x60.png\">\n<link rel=\"apple-touch-icon\" sizes=\"72x72\" href=\"{{ base_favicon_url }}/apple-icon-72x72.png\">\n<link rel=\"apple-touch-icon\" sizes=\"76x76\" href=\"{{ base_favicon_url }}/apple-icon-76x76.png\">\n<link rel=\"apple-touch-icon\" sizes=\"114x114\" href=\"{{ base_favicon_url }}/apple-icon-114x114.png\">\n<link rel=\"apple-touch-icon\" sizes=\"120x120\" href=\"{{ base_favicon_url }}/apple-icon-120x120.png\">\n<link rel=\"apple-touch-icon\" sizes=\"144x144\" href=\"{{ base_favicon_url }}/apple-icon-144x144.png\">\n<link rel=\"apple-touch-icon\" sizes=\"152x152\" href=\"{{ base_favicon_url }}/apple-icon-152x152.png\">\n<link rel=\"apple-touch-icon\" sizes=\"180x180\" href=\"{{ base_favicon_url }}/apple-icon-180x180.png\">\n<link rel=\"icon\" type=\"image/png\" sizes=\"192x192\"  href=\"{{ base_favicon_url }}/android-icon-192x192.png\">\n<link rel=\"icon\" type=\"image/png\" sizes=\"32x32\" href=\"{{ base_favicon_url }}/favicon-32x32.png\">\n<link rel=\"icon\" type=\"image/png\" sizes=\"96x96\" href=\"{{ base_favicon_url }}/favicon-96x96.png\">\n<link rel=\"icon\" type=\"image/png\" sizes=\"16x16\" href=\"{{ base_favicon_url }}/favicon-16x16.png\">\n<link rel=\"manifest\" href=\"{{ base_favicon_url }}/manifest.json\">\n<meta name=\"msapplication-TileColor\" content=\"#ffffff\">\n<meta name=\"msapplication-TileImage\" content=\"{{ base_favicon_url }}/ms-icon-144x144.png\">\n<meta name=\"theme-color\" content=\"#ffffff\">\n<meta name=\"background-color\" content=\"#161d25\">\n"
  },
  {
    "path": "docs/_includes/footer.html",
    "content": "<footer class=\"footer\">\n  <div class=\"container-fluid\">\n    <div class=\"row\">\n\n      <div class=\"col-xs-12 col-sm-6\">\n        {% include logo.html %}\n        <p class=\"subtitle\">Your entire infrastructure. Defined as code. In about a day.</p>\n        <a href=\"https://gruntwork.io\" target=\"_blank\" class=\"btn btn-primary\">Discover Gruntwork.io</a>\n      </div>\n\n      <div class=\"col-xs-12 col-sm-6 learn-col\">\n        <h4>Learn</h4>\n        <ul>\n          <li><a href=\"{{site.baseurl}}/docs/getting-started/quick-start/\">Quick Start</a></li>\n          <li><a href=\"{{site.baseurl}}/docs/\">Documentation</a></li>\n          <li><a href=\"{{site.baseurl}}/examples/\">Examples</a></li>\n        </ul>\n        <div class=\"copy-container\">\n          <ul>\n            <li><a href=\"https://github.com/gruntwork-io/terratest\" class=\"github-nav-link\"><img src=\"{{ site.baseurl}}/assets/img/logos/github-logo.png\" alt=\"GitHub\" /></a></li>\n            <li><a href=\"{{site.baseurl}}/cookie-policy/\">Cookie Policy</a></li>\n            <li><a href=\"{{site.baseurl}}/docs/community/support/\">Support</a></li>\n          </ul>\n          <div class=\"footer__copyright\">\n            &copy; {{ 'now' | date: \"%Y\" }} Gruntwork, Inc. All rights reserved.\n          </div>\n        </div>\n      </div>\n\n    </div>\n  </div>\n</footer>\n"
  },
  {
    "path": "docs/_includes/get-access.html",
    "content": "<div class=\"get-access-cmp\">\n    <span>Get access to a library of over 300,000 lines of battle-tested, production grade infrastructure code, all thoroughly tested with Terratest.</span>\n    <a href=\"https://gruntwork.io\" target=\"_blank\" class=\"btn btn-primary\">Explore Gruntwork.io</a>\n</div>\n  "
  },
  {
    "path": "docs/_includes/head.html",
    "content": "<meta charset=\"utf-8\">\n<meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<!-- The above 3 meta tags *must* come first in the head; any other head content must come *after* these tags -->\n<link href=\"https://fonts.googleapis.com/css?family=Source+Sans+Pro:200,400,700\" rel=\"stylesheet\">\n<link href=\"https://fonts.googleapis.com/css?family=Fira+Mono&display=swap\" rel=\"stylesheet\">\n<link href=\"https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css\" rel=\"stylesheet\" type=\"text/css\">\n<link rel=\"stylesheet\" href=\"{{ '/assets/css/prism.css' | prepend: site.baseurl }}\">\n\n<!--[if lt IE 9]>\n    <script src=\"https://oss.maxcdn.com/html5shiv/3.7.3/html5shiv.min.js\"></script>\n    <script src=\"https://oss.maxcdn.com/respond/1.4.2/respond.min.js\"></script>\n<![endif]-->\n<title>{% if page.title %}{{ page.title }}{% else %}{{ site.name }}{% endif %}</title>\n<meta name=\"description\" content=\"{% if page.excerpt %}{{ page.excerpt | strip_html | strip_newlines }}{% else %}{{ site.description }}{% endif %}\">\n<link rel=\"canonical\" href=\"{% include canonical-url.html %}\">\n{% if page.noindex == true  %}\n<meta name=\"robots\" CONTENT=\"noindex\">\n{% else %}\n<META NAME=\"robots\" CONTENT=\"index\">\n{% endif %}\n{% include styles.html %}\n{% include favicon.html %}\n{% include share-meta.html %}\n"
  },
  {
    "path": "docs/_includes/header-min.html",
    "content": "<div class=\"subpage__header subpage__header--min section-colorful-bg\">\n  {% include navbar.html %}\n  <img src='{{site.baseurl}}/assets/img/terratest_subpage_short_top.svg' alt='Shape' class=\"header-shapes-top\" />\n</div>\n"
  },
  {
    "path": "docs/_includes/header.html",
    "content": "<div class=\"subpage__header section-colorful-bg\">\n  {% include navbar.html %}\n  <div class=\"subpage__hero\">\n    <h1>{{ page.title }}</h1>\n    <span class='subtitle'>{{ page.subtitle }}</span>\n  </div>\n  <div class='header-bg'>\n    <div class='overflow-hide'>\n    </div>\n    <img src='{{site.baseurl}}/assets/img/terratest_subpage_left.svg' alt='Shape' class=\"header-shapes-left\" />\n    <img src='{{site.baseurl}}/assets/img/terratest_subpage_right.svg' alt='Hero' class=\"header-shapes-right\" />\n  </div>\n</div>\n"
  },
  {
    "path": "docs/_includes/links-n-built-by.html",
    "content": "<section>\n  {% include built-by.html %}\n  {% include links-section.html %}\n</section>\n"
  },
  {
    "path": "docs/_includes/links-n-get-access.html",
    "content": "<section class='links-n-get-access__section'>\n    {% include get-access.html %}\n    {% include links-section.html %}\n</section>\n  "
  },
  {
    "path": "docs/_includes/links-section.html",
    "content": "<div class=\"links-section-cmp section-colorful-bg gradient-primary gradient-diagonal\">\n  <h2>Links</h2>\n  <div class=\"links\">\n    <a href=\"{{site.baseurl}}/docs/getting-started/quick-start\" class=\"link-tag-styled\">\n      <img src='{{site.baseurl}}/assets/img/link-icon.svg' class='link-icon' alt='X' />\n      Quick start\n    </a>\n    <a href=\"{{site.baseurl}}/docs\" class=\"link-tag-styled\">\n      <img src='{{site.baseurl}}/assets/img/link-icon.svg' class='link-icon' alt='X' />\n      Documentation\n    </a>\n    <a href=\"{{site.baseurl}}/examples/\" class=\"link-tag-styled\">\n      <img src='{{site.baseurl}}/assets/img/link-icon.svg' class='link-icon' alt='X' />\n      Examples\n    </a>\n  </div>\n</div>\n"
  },
  {
    "path": "docs/_includes/logo.html",
    "content": "<div class=\"terragrunt-logo\">\n  <a class=\"logo-terragrunt\" href=\"{{site.baseurl}}/\">Terratest</a>\n  <a class=\"gruntwork\" href=\"https://gruntwork.io/\" target=\"_blank\">by <strong>Gruntwork.io</strong></a>\n</div>\n"
  },
  {
    "path": "docs/_includes/navbar.html",
    "content": "<header class=\"header\">\n  <nav class=\"navbar navbar-default\">\n    <div class=\"container-fluid\">\n      <div class=\"navbar-header\">\n        <button type=\"button\" class=\"navbar-toggle collapsed\" data-toggle=\"collapse\" data-target=\"#bs-example-navbar-collapse-2\" aria-expanded=\"false\">\n          <span class=\"sr-only\">Toggle navigation</span>\n          <span class=\"icon-bar\"></span>\n          <span class=\"icon-bar\"></span>\n          <span class=\"icon-bar\"></span>\n        </button>\n        <div class=\"navbar-brand navbar-brand-beta\">\n          {% include logo.html %}\n        </div>\n      </div>\n      <div class=\"collapse navbar-collapse\" id=\"bs-example-navbar-collapse-2\">\n        <ul class=\"nav navbar-nav navbar-right\">\n          <li><a href=\"https://gruntwork.io/careers\"><span class=\"new-label\">NEW</span>We're Hiring!</a></li>\n          <li><a href=\"{{site.baseurl}}/docs/getting-started/quick-start\" ga-on=\"click\" ga-event-category=\"nav-{{ page.path | slugify }}\" ga-event-action=\"quick-start\">Quick Start</a></li>\n          <li><a href=\"{{site.baseurl}}/docs/\" ga-on=\"click\" ga-event-category=\"nav-{{ page.path | slugify }}\" ga-event-action=\"docs\">Docs</a></li>\n          <li><a href=\"{{site.baseurl}}/examples/\" ga-on=\"click\" ga-event-category=\"nav-examples\" ga-event-action=\"examples\">Examples</a></li>\n          <li>\n            <a href=\"https://github.com/gruntwork-io/terratest\" ga-on=\"click\" ga-event-category=\"nav-github\" ga-event-action=\"use-cases\" class=\"github-nav-link\">\n              <img src=\"{{ site.baseurl}}/assets/img/logos/github-logo.png\" alt=\"GitHub\" />\n            </a>\n          </li>\n        </ul>\n      </div>\n    </div>\n  </nav>\n</header>\n"
  },
  {
    "path": "docs/_includes/scripts.html",
    "content": "<script src=\"{{site.baseurl}}{{ site.assets_base_url }}js/main.js\"></script>\n{% if page.custom_js %}\n  {% for js_file in page.custom_js %}\n  <script src='{{site.baseurl}}/assets/js/{{ js_file }}.js' type=\"text/javascript\"></script>\n  {% endfor %}\n{% elsif layout.custom_js %}\n  {% for js_file in layout.custom_js %}\n    <script src='{{site.baseurl}}/assets/js/{{ js_file }}.js' type=\"text/javascript\"></script>\n  {% endfor %}\n{% endif %}\n\n<!-- Add here more: Google Analytics etc. -->\n{% if site.gtm_tracker %}\n\n<!-- Google Tag Manager -->\n<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':\n  new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],\n  j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=\n  'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);\n  })(window,document,'script','dataLayer','{{ site.gtm_tracker }}');</script>\n  <!-- End Google Tag Manager -->\n\n<script async src=\"https://cdnjs.cloudflare.com/ajax/libs/autotrack/2.4.1/autotrack.js\"></script>\n\n<script src=\"{{site.baseurl}}{{ site.assets_base_url }}js/cookie.js\" async></script>\n\n{% endif %}\n"
  },
  {
    "path": "docs/_includes/share-meta.html",
    "content": "{% capture title %}{% if page.title %}{{ page.title }}{% else %}{{ site.name }}{% endif %}{% endcapture %}\n{% capture description %}{% if page.excerpt %}{{ page.excerpt | strip_html | strip_newlines }}{% else %}{{ site.description }}{% endif %}{% endcapture %}\n{% capture img %}{% if page.image %}{{ page.image }}{% else %}{{ site.thumbnail_path }}{% endif %}{% endcapture %}\n\n<meta name=\"twitter:card\" content=\"summary\" />\n<meta name=\"twitter:site\" content=\"{{ site.data.contact.twitter.username }}\" />\n<meta name=\"twitter:title\" content=\"{{ title }}\" />\n<meta name=\"twitter:url\" content=\"{% include canonical-url.html %}\" />\n<meta name=\"twitter:description\" content=\"{{ description }}\" />\n<meta name=\"twitter:image\" content=\"{{ site.url }}{{site.baseurl}}{{ img }}\" />\n\n<meta property=\"og:title\" content=\"{{ title }}\"/>\n<meta property=\"og:url\" content=\"{% include canonical-url.html %}\"/>\n<meta property=\"og:site_name\" content=\"{{ site.name }}\"/>\n<meta property=\"article:author\" content=\"{{ site.data.contact.facebook.url }}\"/>\n<meta property=\"og:description\" content=\"{{ description }}\"/>\n<meta property=\"og:image\" content=\"{{site.url}}{{site.baseurl}}{{ img }}\"/>\n"
  },
  {
    "path": "docs/_includes/styles.html",
    "content": "<link rel=\"stylesheet\" href=\"{{site.baseurl}}{{ site.assets_base_url }}css/styles.css\" />\n"
  },
  {
    "path": "docs/_includes/switch.html",
    "content": "<div class=\"switch-cmp {{ include.class }}\">\n  <span class=\"marker\" />\n</div>\n"
  },
  {
    "path": "docs/_includes/video-player.html",
    "content": "<div class=\"video-player\" data-video-url=\"{{ include.url }}\">\n  <img src=\"{{ site.baseurl }}/assets/img/terratest_video_frame.png\" alt=\"video\" class=\"frame\" />\n  <img src=\"{{ site.baseurl }}/assets/img/Terratest_video_button.svg\" alt=\"video\" class=\"btn-video\" />\n</div>\n"
  },
  {
    "path": "docs/_layouts/collection-browser-doc.html",
    "content": "---\ncustom_js:\n- prism\n- collection-browser_scroll\n- collection-browser_search\n- collection-browser_toc\n- video-player\n---\n\n<!doctype html>\n<html lang=\"en\">\n    <head>\n        {% include head.html %}\n    </head>\n    <body{% if page.slug %} class=\"{{ page.slug }} collection-browser collection-browser-doc subpage\" {% endif %}>\n        {% include header-min.html %}\n        {% include collection_browser/_doc-page.html content=content %}\n        {% include links-n-built-by.html %}\n        {% include footer.html %}\n        {% include scripts.html %}\n    </body>\n</html>\n"
  },
  {
    "path": "docs/_layouts/collection-browser.html",
    "content": "---\ncustom_js:\n- prism\n- collection-browser_scroll\n- collection-browser_search\n- collection-browser_toc\n- video-player\n---\n\n<!doctype html>\n<html lang=\"en\">\n    <head>\n        {% include head.html %}\n    </head>\n    <body{% if page.slug %} class=\"{{ page.slug }} collection-browser subpage\" {% endif %}>\n        {% include header.html %}\n        {{ content }}\n        {% include footer.html %}\n        {% include scripts.html %}\n    </body>\n</html>\n"
  },
  {
    "path": "docs/_layouts/contact.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    {% include head.html %}\n  </head>\n  <body{% if page.slug %} class=\"{{ page.slug }} subpage\" {% endif %}>\n    <div class=\"subpage__header subpage__header--min section-colorful-bg\">\n      {% include navbar.html %}\n      <img src='{{site.baseurl}}/assets/img/contact/top.svg' alt='Shape' class=\"header-shapes-top hidden-shape-xs\" />\n      <img src='{{site.baseurl}}/assets/img/contact/right.svg' alt='Shape' class=\"header-shapes-top hidden-shape\" />\n    </div>\n    <div class=\"contact subpage__contact {{ page.classes }}\">\n      {{ content }}\n    </div>\n    <section>\n      {% include links-section.html %}\n    </section>\n    {% include footer.html %}\n    {% include scripts.html %}\n  </body>\n</html>\n"
  },
  {
    "path": "docs/_layouts/default.html",
    "content": "---\ncustom_js:\n- video-player\n- examples\n- prism\n---\n\n<!doctype html>\n<html lang=\"en\">\n    <head>\n        {% include head.html %}\n    </head>\n    <body{% if page.slug %} class=\"{{ page.slug }}{% if page.slug != 'index-page' and page.slug !='how-it-works' and page.slug != 'guides' %} sub-page{% endif %}\" {% endif %}>\n        {{ content }}\n        {% include footer.html %}\n        {% include scripts.html %}\n    </body>\n</html>\n"
  },
  {
    "path": "docs/_layouts/post.html",
    "content": "---\nlayout: default\ncustom_js:\n- prism\n- video-player\n---\n\n<div  class=\"post-bg\">\n  {{ content }}\n</div>\n"
  },
  {
    "path": "docs/_layouts/subpage.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    {% include head.html %}\n  </head>\n  <body{% if page.slug %} class=\"{{ page.slug }} subpage\" {% endif %}>\n    {% include header.html %}\n    <div class=\"main subpage__main {{ page.classes }}\">\n      {{ content }}\n    </div>\n    {% include links-n-built-by.html %}\n    {% include footer.html %}\n    {% include scripts.html %}\n  </body>\n</html>\n"
  },
  {
    "path": "docs/_pages/404/404.md",
    "content": "---\npermalink: /404.html\nslug: 404\nlayout: subpage\ntitle: 404\nsubtitle: Page not found :(\nclasses: text-large text-center subpage-404\n---\n\nThe requested page could not be found.\n"
  },
  {
    "path": "docs/_pages/commercial-support/index.html",
    "content": "---\npermalink: /commercial-support/\nredirect_to:\n  - https://gruntwork.io/support\n---\n"
  },
  {
    "path": "docs/_pages/contact/_contact-form.html",
    "content": "<div class=\"contact-form-container\">\n  <img class='contact-form-back' src='{{ site.baseurl}}/assets/img/contact/right.svg' alt='contact-form-back' />\n  <div id=\"error-message\"></div>\n  <form id=\"contact-form\" method=\"post\" target=\"_blank\" action=\"https://api.formbucket.com/f/buk_eldvLHDisTrizpsFPDfOm7fY\">\n    <label>Name</label>\n    <input required type=\"text\" name=\"name\" id=\"name\" value=\"\" placeholder=\"Jon Doe\" />\n    <label>Email</label>\n    <input required type=\"email\" name=\"email\" value=\"\" placeholder=\"John@acme.com\" />\n    <label>Company</label>\n    <input type=\"text\" name=\"company\" placeholder=\"Acme, Inc.\" />\n    <span>Which plans are you interested in?</span>\n    <div class='radio-element'>\n        <label class=\"radio-label\" for=\"pro\">\n          <input class=\"radio-btn-contact\" type=\"radio\" id=\"pro\" name=\"terratest-plan\" value=\"terratest-pro\">\n          <span class=\"checkmark\"></span>\n        </label>\n        <label for=\"pro\">Terratest pro</label><br>\n    </div>\n    <div class='radio-element'>\n      <label class=\"radio-label\" for=\"enterprise\">\n        <input class=\"radio-btn-contact\" type=\"radio\" id=\"enterprise\" name=\"terratest-plan\" value=\"terratest-enterprise\">\n        <span class=\"checkmark\"></span>\n      </label>\n      <label for=\"enterprise\">Terratest enterprise</label><br>\n    </div>\n    <label>How can we help?</label>\n    <textarea id=\"message\" name=\"message\" rows=\"4\" cols=\"50\" required placeholder=\"Your message\"></textarea>\n\n    <div class=\"text-center\">\n      <br />\n      <button\n        type=\"submit\"\n        id=\"submit-button\"\n        class=\"btn btn-primary\"\n        ga-on=\"click\"\n        ga-event-category=\"contact\"\n        ga-event-action=\"submit\"\n      >\n        Submit\n      </button>\n    </div>\n  </form>\n</div>\n"
  },
  {
    "path": "docs/_pages/contact/index.html",
    "content": "---\nlayout: contact\ntitle: contact\nsubtitle: Get help via email, chat, and phone/video from the team that created Terratest.\nexcerpt: Get help via email, chat, and phone/video from the team that created Terratest.\npermalink: /contact/\nslug: contact\nnav_title: Contact\nnav_title_link: /docs/\ncustom_js:\n  - prism\n  - examples\n  - collection-browser_toc\n  - contact-form\nuse_recaptcha: true\n---\n\n<div class=\"contact__page\">\n  <div class=\"col-md-6 contact-column\">\n    <h1>Contact Us</h1>\n    <p class=\"contact-subtitle\">\n      Speak to a real human! \n    </p>\n    <p class=\"contact-subtitle\">\n      Use the form below or send an email to\n      <a href=\"mailto:info@gruntwork.io\" >info@gruntwork.io.</a>\n    </p>\n    <img class='contact-subtitle-back hidden-shape-xs' src='{{ site.baseurl}}/assets/img/contact/bottom.svg' alt='contact-form-back' />\n  </div>\n  <div class=\"col-md-6 form-column\">\n    {% include_relative _contact-form.html %}\n    <img src='{{site.baseurl}}/assets/img/contact/contact-mobile-bottom.svg' alt='Shape' class=\"header-shapes-bottom hidden-shape\" />\n  </div>\n</div>\n\n"
  },
  {
    "path": "docs/_pages/cookie-policy/index.md",
    "content": "---\nlayout: subpage\npermalink: /cookie-policy/\nslug: cookie-policy\nredirect_to:\n  - https://gruntwork.io/cookie-policy/\n---\n"
  },
  {
    "path": "docs/_pages/docs/index.html",
    "content": "---\nlayout: collection-browser\ntitle: Documentation\nsubtitle: Learn how to work with Terratest.\nexcerpt: Learn how to work with Terratest.\npermalink: /docs/\nslug: docs\nnav_title: Documentation\n---\n\n{% include collection_browser/browser.html collection=site.docs collection_name='docs' %}\n"
  },
  {
    "path": "docs/_pages/examples/index.html",
    "content": "---\nlayout: collection-browser\ntitle: Examples\nsubtitle: The best way to learn how to use Terratest is through examples.\nexcerpt: The best way to learn how to use Terratest is through examples.\npermalink: /examples/\nslug: examples\nnav_title: Documentation\nnav_title_link: /docs/\ncustom_js:\n- prism\n- examples\n- collection-browser_scroll\n- collection-browser_toc\n---\n\n<div class=\"main\">\n  <div class=\"cb-section-white\">\n\n    <div class=\"row\">\n      <div class=\"container-cb-lg\">\n        <div id=\"cb-doc-listings\">\n          <div class=\"row equal\">\n\n            {% assign data_scroll_after_selector = '.main' %}\n            {% assign data_scroll_until_selector = '#cb-doc-listings' %}\n            {% include collection_browser/_sidebar.html data_scroll_until_selector=data_scroll_until_selector data_scroll_after_selector=data_scroll_after_selector %}\n\n            <div class=\"col-xs-12 col-md-7 col-lg-9\">\n              <div class=\"row\">\n                <div class=\"cb-doc-listing js-scroll-spy\" data-scroll-spy-nav-selector=\"#toc\">\n\n                  {% include examples/explorer.html class='wide' id='examples_page' test_for_display=true %}\n\n                </div>\n              </div>\n            </div>\n\n          </div>\n        </div>\n      </div>\n    </div>\n\n  </div>\n</div>\n"
  },
  {
    "path": "docs/_pages/index/_built_by.html",
    "content": "<section class=\"section index-page__built-by\">\n  <h2>Built by <strong>Gruntwork</strong></h2>\n  <p class=\"subtitle\">Your entire infrastructure. Defined as code. In about a day.</p>\n  <a class=\"btn btn-primary btn-lg\" href=\"https://gruntwork.io/\" target=\"_blank\">Explore Gruntwork.io</a>\n  <img src=\"{{site.baseurl}}/assets/img/home/built-by-bg.svg\" alt=\"shapes\" class=\"bg-image\" />\n</section>\n"
  },
  {
    "path": "docs/_pages/index/_cta_section.html",
    "content": "<div class=\"index-page__cta-section section gradient-primary gradient-diagonal text-center\">\r\n  <a href=\"{{ site.baseurl }}/docs/getting-started/quick-start/\" class=\"btn btn-white btn-lg\">Start testing <span class=\"hide-on-cxs\">infrastructure code</span></a>\r\n  <img src=\"{{ site.baseurl }}/assets/img/home/terratest_middle_left.svg\" class=\"left-img\" alt=\"shape\" />\r\n  <img src=\"{{ site.baseurl }}/assets/img/home/terratest_middle_right.svg\" class=\"right-img\" alt=\"shape\" />\r\n</div>\r\n"
  },
  {
    "path": "docs/_pages/index/_header.html",
    "content": "<div class=\"section section-hero section-colorful-bg\">\n  {% include navbar.html %}\n  <div class=\"index-page__header\">\n    <div class=\"img-container\">\n      <img src='{{site.baseurl}}/assets/img/home/terratest_top_left.svg' alt='Hero' class=\"header-shapes-hero\" />\n    </div>\n    <div class=\"text-container\">\n        <h1><strong>Automated tests</strong> for your infrastructure code.</h1>\n        <span class='subtitle'>\n          Terratest is a Go library that provides patterns\n          and helper functions for testing infrastructure,\n          with 1st-class support for\n          <span class=\"link-to-test-with-terratest\" data-target=\"terraform\">Terraform</span>,\n          <span class=\"link-to-test-with-terratest\" data-target=\"packer\">Packer</span>,\n          <span class=\"link-to-test-with-terratest\" data-target=\"docker\">Docker</span>,\n          <span class=\"link-to-test-with-terratest\" data-target=\"kubernetes\">Kubernetes</span>,\n          <span class=\"link-to-test-with-terratest\" data-target=\"aws\">AWS</span>,\n          <span class=\"link-to-test-with-terratest\" data-target=\"gcp\">GCP</span>,\n          and more.\n        </span>\n        <a href=\"{{site.baseurl}}/docs#getting-started\" class='btn btn-white btn-lg'>Let's Get Started</a>\n    </div>\n  </div>\n  <div class=\"header-shapes-bottom\"></div>\n</div>\n"
  },
  {
    "path": "docs/_pages/index/_terratest-in-4-steps.html",
    "content": "<div class=\"index-page__terratest-in-4-steps\">\n\n  <div class=\"section text-center container-fluid\">\n    <div class=\"row\">\n      <div class=\"col-xs-12\">\n        <h2>Test infrastructure code with Terratest <strong>in 4 steps</strong></h2>\n      </div>\n    </div>\n  </div>\n\n  <div class=\"test-steps\">\n\n    <div class=\"test-step\">\n      <div class=\"icon-wrapper\">\n        <img src=\"{{site.baseurl}}/assets/img/home/terratest_icon_1.svg\" alt=\"Write test using Go\" />\n        <span class=\"line\"></span>\n      </div>\n      <div class=\"text-wrapper\">\n        <label>Write test code using Go</label>\n        <span class=\"desc\">\n          Create a file ending in <code class=\"code-snippet purple\">_test.go</code> and run tests with the go test command. E.g.,\n          <code class=\"code-snippet purple\">go test my_test.go</code>.\n        </span>\n      </div>\n    </div>\n\n    <div class=\"test-step\">\n      <div class=\"icon-wrapper\">\n        <span class=\"line\"></span>\n        <img src=\"{{site.baseurl}}/assets/img/home/terratest_icon_2.svg\" alt=\"Use Terratest to deploy infrastructure\" />\n        <span class=\"line\"></span>\n      </div>\n      <div class=\"text-wrapper\">\n        <label>Use Terratest to deploy infrastructure</label>\n        <span class=\"desc\">\n          Use Terratest to execute your real IaC tools (e.g., Terraform, Packer, etc.)\n          to deploy real infrastructure (e.g., servers) in a real environment (e.g., AWS).\n        </span>\n      </div>\n    </div>\n\n    <div class=\"test-step\">\n      <div class=\"icon-wrapper\">\n        <span class=\"line\"></span>\n        <img src=\"{{site.baseurl}}/assets/img/home/terratest_icon_3.svg\" alt=\"Validate infrastructure\" />\n        <span class=\"line\"></span>\n      </div>\n      <div class=\"text-wrapper\">\n        <label>Validate infrastructure with Terratest</label>\n        <span class=\"desc\">\n          Use the tools built into Terratest to validate that the infrastructure\n          works correctly in that environment by making HTTP requests, API calls, SSH connections, etc.\n        </span>\n      </div>\n    </div>\n\n    <div class=\"test-step\">\n      <div class=\"icon-wrapper\">\n        <span class=\"line\"></span>\n        <img src=\"{{site.baseurl}}/assets/img/home/terratest_icon_4.svg\" alt=\"Undeploy\" />\n      </div>\n      <div class=\"text-wrapper\">\n        <label>Undeploy</label>\n        <span class=\"desc\">\n          Undeploy everything at the end of the test.\n        </span>\n      </div>\n    </div>\n  </div>\n\n</div>\n"
  },
  {
    "path": "docs/_pages/index/_test-with-terratest.html",
    "content": "<div id=\"index-page__test-with-terratest\" class=\"section index-page__test-with-terratest\">\n\n  <div class=\"text-center container-fluid\">\n    <div class=\"row\">\n      <div class=\"col-xs-12\">\n        <h2><strong>Test</strong> with Terratest</h2>\n      </div>\n    </div>\n  </div>\n\n  {% include examples/explorer.html id='index_page' %}\n\n</div>\n"
  },
  {
    "path": "docs/_pages/index/_watch.html",
    "content": "<div class=\"index-page__watch\">\r\n\r\n  <div class=\"section text-center container-fluid\">\r\n    <div class=\"row flex align-items--stretch\">\r\n      <div class=\"col-xs-12 col-md-6 text-left\">\r\n        <span class=\"title-label\">Watch:</span>\r\n        <img src=\"{{ site.baseurl }}/assets/img/icons/quote.svg\" alt=\"quote\" />\r\n        <h2>How to test infrastructure code:<br/>\r\n          <strong>automated testing for Terraform,\r\n          Kubernetes, Docker, Packer and more</strong>\r\n        </h2>\r\n        <p>\r\n          The talk is a step-by-step, live-coding class on how to write automated tests for infrastructure code,\r\n          including the code you write for use with tools such as Terraform, Kubernetes, Docker, and Packer.\r\n          Topics covered include unit tests, integration tests, end-to-end tests, test parallelism,\r\n          retries, error handling, static analysis, and more.\r\n        </p>\r\n        <div class=\"check-out-wrapper\">\r\n          <a href=\"{{ site.baseurl }}/docs/getting-started/introduction/#watch-how-to-test-infrastructure-code\" class=\"btn btn-primary btn-sm\">Check out</a>\r\n          <span>the video and slides for the talk.</span>\r\n        </div>\r\n      </div>\r\n\r\n      <div class=\"col-xs-12 col-md-6 flex align-items--end\">\r\n        {% include video-player.html url='https://www.youtube.com/embed/xhHOW0EF5u8?autoplay=1' %}\r\n      </div>\r\n    </div>\r\n  </div>\r\n\r\n</div>\r\n"
  },
  {
    "path": "docs/_pages/index/index.html",
    "content": "---\nlayout: default\npermalink: /\nslug: index-page\n---\n\n<div class=\"main\">\n  {% include_relative _header.html %}\n  {% include_relative _terratest-in-4-steps.html %}\n  <hr class=\"short\" />\n  {% include_relative _test-with-terratest.html %}\n  {% include_relative _cta_section.html %}\n  {% include_relative _watch.html %}\n  <hr class=\"short\" />\n  {% include links-n-built-by.html %}\n</div>\n"
  },
  {
    "path": "docs/_pages/thanks/index.html",
    "content": "---\nlayout: default\ntitle: Thank you.\nexcerpt: Thank you for contacting us. A Grunt will be in touch soon!\npermalink: /thanks/\n---\n\n<div class=\"thanks-page\">\n  {% include navbar.html %}\n  <div class=\"main\">\n    <div class=\"section section-hero section-thanks\">\n      <div class=\"container\">\n        <div class=\"row\">\n          <div class=\"col-xs-12 text-center\">\n            <h1>{{ page.title }}</h1>\n            <p>{{ page.excerpt }}</p>\n            <p>\n              <a href=\"/\" class=\"btn btn-info\" role=\"button\"\n                >Go to the Terratest home page</a\n              >\n            </p>\n          </div>\n        </div>\n      </div>\n    </div>\n  </div>\n</div>"
  },
  {
    "path": "docs/_posts/.keep",
    "content": ""
  },
  {
    "path": "docs/assets/css/_variables.scss",
    "content": "$primary-color: #07a7fd;\n$primary-color-2: #068ee4;\n\n$gray-color-1: #f0f0f1;\n$gray-color-2: #dedede;\n$gray-color-3: #bbbdc0;\n\n$text-color: #1e252f;\n$inline-code-color-base: #0352c2;\n$inline-code-bg-color-base: $gray-color-1;\n\n$primary-gradient-start-color: #001191;\n$primary-gradient-stop-color: #06a3ff;\n\n$secondary-gradient-start-color: #4eb9fb;\n$secondary-gradient-stop-color: #2683f5;\n\n$font-size-base: 16px;\n$font-size-xxs: 11px;\n$font-size-xs: 13px;\n$font-size-sm: 14px;\n$font-size-lg: 20px;\n\n$font-size-h1: 35px;\n$font-size-h2: 28px;\n\n$box-shadow-sm: 0 2px 34px 0 rgba(0, 0, 0, 0.1);\n"
  },
  {
    "path": "docs/assets/css/bootstrap/scss/bootstrap.scss",
    "content": "/*!\n * Bootstrap v3.3.7 (http://getbootstrap.com)\n * Copyright 2011-2016 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n */\n/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */\nhtml {\n  font-family: sans-serif;\n  -ms-text-size-adjust: 100%;\n  -webkit-text-size-adjust: 100%;\n}\nbody {\n  margin: 0;\n}\narticle,\naside,\ndetails,\nfigcaption,\nfigure,\nfooter,\nheader,\nhgroup,\nmain,\nmenu,\nnav,\nsection,\nsummary {\n  display: block;\n}\naudio,\ncanvas,\nprogress,\nvideo {\n  display: inline-block;\n  vertical-align: baseline;\n}\naudio:not([controls]) {\n  display: none;\n  height: 0;\n}\n[hidden],\ntemplate {\n  display: none;\n}\na {\n  background-color: transparent;\n}\na:active,\na:hover {\n  outline: 0;\n}\nabbr[title] {\n  border-bottom: 1px dotted;\n}\nb,\nstrong {\n  font-weight: bold;\n}\ndfn {\n  font-style: italic;\n}\nh1 {\n  font-size: 2em;\n  margin: 0.67em 0;\n}\nmark {\n  background: #ff0;\n  color: #000;\n}\nsmall {\n  font-size: 80%;\n}\nsub,\nsup {\n  font-size: 75%;\n  line-height: 0;\n  position: relative;\n  vertical-align: baseline;\n}\nsup {\n  top: -0.5em;\n}\nsub {\n  bottom: -0.25em;\n}\nimg {\n  border: 0;\n}\nsvg:not(:root) {\n  overflow: hidden;\n}\nfigure {\n  margin: 1em 40px;\n}\nhr {\n  box-sizing: content-box;\n  height: 0;\n}\npre {\n  overflow: auto;\n}\ncode,\nkbd,\npre,\nsamp {\n  font-family: monospace, monospace;\n  font-size: 1em;\n}\nbutton,\ninput,\noptgroup,\nselect,\ntextarea {\n  color: inherit;\n  font: inherit;\n  margin: 0;\n}\nbutton {\n  overflow: visible;\n}\nbutton,\nselect {\n  text-transform: none;\n}\nbutton,\nhtml input[type=\"button\"],\ninput[type=\"reset\"],\ninput[type=\"submit\"] {\n  -webkit-appearance: button;\n  cursor: pointer;\n}\nbutton[disabled],\nhtml input[disabled] {\n  cursor: default;\n}\nbutton::-moz-focus-inner,\ninput::-moz-focus-inner {\n  border: 0;\n  padding: 0;\n}\ninput {\n  line-height: normal;\n}\ninput[type=\"checkbox\"],\ninput[type=\"radio\"] {\n  box-sizing: border-box;\n  padding: 0;\n}\ninput[type=\"number\"]::-webkit-inner-spin-button,\ninput[type=\"number\"]::-webkit-outer-spin-button {\n  height: auto;\n}\ninput[type=\"search\"] {\n  -webkit-appearance: textfield;\n  box-sizing: content-box;\n}\ninput[type=\"search\"]::-webkit-search-cancel-button,\ninput[type=\"search\"]::-webkit-search-decoration {\n  -webkit-appearance: none;\n}\nfieldset {\n  border: 1px solid #c0c0c0;\n  margin: 0 2px;\n  padding: 0.35em 0.625em 0.75em;\n}\nlegend {\n  border: 0;\n  padding: 0;\n}\ntextarea {\n  overflow: auto;\n}\noptgroup {\n  font-weight: bold;\n}\ntable {\n  border-collapse: collapse;\n  border-spacing: 0;\n}\ntd,\nth {\n  padding: 0;\n}\n/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */\n@media print {\n  *,\n  *:before,\n  *:after {\n    background: transparent !important;\n    color: #000 !important;\n    box-shadow: none !important;\n    text-shadow: none !important;\n  }\n  a,\n  a:visited {\n    text-decoration: underline;\n  }\n  a[href]:after {\n    content: \" (\" attr(href) \")\";\n  }\n  abbr[title]:after {\n    content: \" (\" attr(title) \")\";\n  }\n  a[href^=\"#\"]:after,\n  a[href^=\"javascript:\"]:after {\n    content: \"\";\n  }\n  pre,\n  blockquote {\n    border: 1px solid #999;\n    page-break-inside: avoid;\n  }\n  thead {\n    display: table-header-group;\n  }\n  tr,\n  img {\n    page-break-inside: avoid;\n  }\n  img {\n    max-width: 100% !important;\n  }\n  p,\n  h2,\n  h3 {\n    orphans: 3;\n    widows: 3;\n  }\n  h2,\n  h3 {\n    page-break-after: avoid;\n  }\n  .navbar {\n    display: none;\n  }\n  .btn > .caret,\n  .dropup > .btn > .caret {\n    border-top-color: #000 !important;\n  }\n  .label {\n    border: 1px solid #000;\n  }\n  .table {\n    border-collapse: collapse !important;\n  }\n  .table td,\n  .table th {\n    background-color: #fff !important;\n  }\n  .table-bordered th,\n  .table-bordered td {\n    border: 1px solid #ddd !important;\n  }\n}\n@font-face {\n  font-family: 'Glyphicons Halflings';\n  src: url('../fonts/glyphicons-halflings-regular.eot');\n  src: url('../fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'), url('../fonts/glyphicons-halflings-regular.woff2') format('woff2'), url('../fonts/glyphicons-halflings-regular.woff') format('woff'), url('../fonts/glyphicons-halflings-regular.ttf') format('truetype'), url('../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg');\n}\n.glyphicon {\n  position: relative;\n  top: 1px;\n  display: inline-block;\n  font-family: 'Glyphicons Halflings';\n  font-style: normal;\n  font-weight: normal;\n  line-height: 1;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n.glyphicon-asterisk:before {\n  content: \"\\002a\";\n}\n.glyphicon-plus:before {\n  content: \"\\002b\";\n}\n.glyphicon-euro:before,\n.glyphicon-eur:before {\n  content: \"\\20ac\";\n}\n.glyphicon-minus:before {\n  content: \"\\2212\";\n}\n.glyphicon-cloud:before {\n  content: \"\\2601\";\n}\n.glyphicon-envelope:before {\n  content: \"\\2709\";\n}\n.glyphicon-pencil:before {\n  content: \"\\270f\";\n}\n.glyphicon-glass:before {\n  content: \"\\e001\";\n}\n.glyphicon-music:before {\n  content: \"\\e002\";\n}\n.glyphicon-search:before {\n  content: \"\\e003\";\n}\n.glyphicon-heart:before {\n  content: \"\\e005\";\n}\n.glyphicon-star:before {\n  content: \"\\e006\";\n}\n.glyphicon-star-empty:before {\n  content: \"\\e007\";\n}\n.glyphicon-user:before {\n  content: \"\\e008\";\n}\n.glyphicon-film:before {\n  content: \"\\e009\";\n}\n.glyphicon-th-large:before {\n  content: \"\\e010\";\n}\n.glyphicon-th:before {\n  content: \"\\e011\";\n}\n.glyphicon-th-list:before {\n  content: \"\\e012\";\n}\n.glyphicon-ok:before {\n  content: \"\\e013\";\n}\n.glyphicon-remove:before {\n  content: \"\\e014\";\n}\n.glyphicon-zoom-in:before {\n  content: \"\\e015\";\n}\n.glyphicon-zoom-out:before {\n  content: \"\\e016\";\n}\n.glyphicon-off:before {\n  content: \"\\e017\";\n}\n.glyphicon-signal:before {\n  content: \"\\e018\";\n}\n.glyphicon-cog:before {\n  content: \"\\e019\";\n}\n.glyphicon-trash:before {\n  content: \"\\e020\";\n}\n.glyphicon-home:before {\n  content: \"\\e021\";\n}\n.glyphicon-file:before {\n  content: \"\\e022\";\n}\n.glyphicon-time:before {\n  content: \"\\e023\";\n}\n.glyphicon-road:before {\n  content: \"\\e024\";\n}\n.glyphicon-download-alt:before {\n  content: \"\\e025\";\n}\n.glyphicon-download:before {\n  content: \"\\e026\";\n}\n.glyphicon-upload:before {\n  content: \"\\e027\";\n}\n.glyphicon-inbox:before {\n  content: \"\\e028\";\n}\n.glyphicon-play-circle:before {\n  content: \"\\e029\";\n}\n.glyphicon-repeat:before {\n  content: \"\\e030\";\n}\n.glyphicon-refresh:before {\n  content: \"\\e031\";\n}\n.glyphicon-list-alt:before {\n  content: \"\\e032\";\n}\n.glyphicon-lock:before {\n  content: \"\\e033\";\n}\n.glyphicon-flag:before {\n  content: \"\\e034\";\n}\n.glyphicon-headphones:before {\n  content: \"\\e035\";\n}\n.glyphicon-volume-off:before {\n  content: \"\\e036\";\n}\n.glyphicon-volume-down:before {\n  content: \"\\e037\";\n}\n.glyphicon-volume-up:before {\n  content: \"\\e038\";\n}\n.glyphicon-qrcode:before {\n  content: \"\\e039\";\n}\n.glyphicon-barcode:before {\n  content: \"\\e040\";\n}\n.glyphicon-tag:before {\n  content: \"\\e041\";\n}\n.glyphicon-tags:before {\n  content: \"\\e042\";\n}\n.glyphicon-book:before {\n  content: \"\\e043\";\n}\n.glyphicon-bookmark:before {\n  content: \"\\e044\";\n}\n.glyphicon-print:before {\n  content: \"\\e045\";\n}\n.glyphicon-camera:before {\n  content: \"\\e046\";\n}\n.glyphicon-font:before {\n  content: \"\\e047\";\n}\n.glyphicon-bold:before {\n  content: \"\\e048\";\n}\n.glyphicon-italic:before {\n  content: \"\\e049\";\n}\n.glyphicon-text-height:before {\n  content: \"\\e050\";\n}\n.glyphicon-text-width:before {\n  content: \"\\e051\";\n}\n.glyphicon-align-left:before {\n  content: \"\\e052\";\n}\n.glyphicon-align-center:before {\n  content: \"\\e053\";\n}\n.glyphicon-align-right:before {\n  content: \"\\e054\";\n}\n.glyphicon-align-justify:before {\n  content: \"\\e055\";\n}\n.glyphicon-list:before {\n  content: \"\\e056\";\n}\n.glyphicon-indent-left:before {\n  content: \"\\e057\";\n}\n.glyphicon-indent-right:before {\n  content: \"\\e058\";\n}\n.glyphicon-facetime-video:before {\n  content: \"\\e059\";\n}\n.glyphicon-picture:before {\n  content: \"\\e060\";\n}\n.glyphicon-map-marker:before {\n  content: \"\\e062\";\n}\n.glyphicon-adjust:before {\n  content: \"\\e063\";\n}\n.glyphicon-tint:before {\n  content: \"\\e064\";\n}\n.glyphicon-edit:before {\n  content: \"\\e065\";\n}\n.glyphicon-share:before {\n  content: \"\\e066\";\n}\n.glyphicon-check:before {\n  content: \"\\e067\";\n}\n.glyphicon-move:before {\n  content: \"\\e068\";\n}\n.glyphicon-step-backward:before {\n  content: \"\\e069\";\n}\n.glyphicon-fast-backward:before {\n  content: \"\\e070\";\n}\n.glyphicon-backward:before {\n  content: \"\\e071\";\n}\n.glyphicon-play:before {\n  content: \"\\e072\";\n}\n.glyphicon-pause:before {\n  content: \"\\e073\";\n}\n.glyphicon-stop:before {\n  content: \"\\e074\";\n}\n.glyphicon-forward:before {\n  content: \"\\e075\";\n}\n.glyphicon-fast-forward:before {\n  content: \"\\e076\";\n}\n.glyphicon-step-forward:before {\n  content: \"\\e077\";\n}\n.glyphicon-eject:before {\n  content: \"\\e078\";\n}\n.glyphicon-chevron-left:before {\n  content: \"\\e079\";\n}\n.glyphicon-chevron-right:before {\n  content: \"\\e080\";\n}\n.glyphicon-plus-sign:before {\n  content: \"\\e081\";\n}\n.glyphicon-minus-sign:before {\n  content: \"\\e082\";\n}\n.glyphicon-remove-sign:before {\n  content: \"\\e083\";\n}\n.glyphicon-ok-sign:before {\n  content: \"\\e084\";\n}\n.glyphicon-question-sign:before {\n  content: \"\\e085\";\n}\n.glyphicon-info-sign:before {\n  content: \"\\e086\";\n}\n.glyphicon-screenshot:before {\n  content: \"\\e087\";\n}\n.glyphicon-remove-circle:before {\n  content: \"\\e088\";\n}\n.glyphicon-ok-circle:before {\n  content: \"\\e089\";\n}\n.glyphicon-ban-circle:before {\n  content: \"\\e090\";\n}\n.glyphicon-arrow-left:before {\n  content: \"\\e091\";\n}\n.glyphicon-arrow-right:before {\n  content: \"\\e092\";\n}\n.glyphicon-arrow-up:before {\n  content: \"\\e093\";\n}\n.glyphicon-arrow-down:before {\n  content: \"\\e094\";\n}\n.glyphicon-share-alt:before {\n  content: \"\\e095\";\n}\n.glyphicon-resize-full:before {\n  content: \"\\e096\";\n}\n.glyphicon-resize-small:before {\n  content: \"\\e097\";\n}\n.glyphicon-exclamation-sign:before {\n  content: \"\\e101\";\n}\n.glyphicon-gift:before {\n  content: \"\\e102\";\n}\n.glyphicon-leaf:before {\n  content: \"\\e103\";\n}\n.glyphicon-fire:before {\n  content: \"\\e104\";\n}\n.glyphicon-eye-open:before {\n  content: \"\\e105\";\n}\n.glyphicon-eye-close:before {\n  content: \"\\e106\";\n}\n.glyphicon-warning-sign:before {\n  content: \"\\e107\";\n}\n.glyphicon-plane:before {\n  content: \"\\e108\";\n}\n.glyphicon-calendar:before {\n  content: \"\\e109\";\n}\n.glyphicon-random:before {\n  content: \"\\e110\";\n}\n.glyphicon-comment:before {\n  content: \"\\e111\";\n}\n.glyphicon-magnet:before {\n  content: \"\\e112\";\n}\n.glyphicon-chevron-up:before {\n  content: \"\\e113\";\n}\n.glyphicon-chevron-down:before {\n  content: \"\\e114\";\n}\n.glyphicon-retweet:before {\n  content: \"\\e115\";\n}\n.glyphicon-shopping-cart:before {\n  content: \"\\e116\";\n}\n.glyphicon-folder-close:before {\n  content: \"\\e117\";\n}\n.glyphicon-folder-open:before {\n  content: \"\\e118\";\n}\n.glyphicon-resize-vertical:before {\n  content: \"\\e119\";\n}\n.glyphicon-resize-horizontal:before {\n  content: \"\\e120\";\n}\n.glyphicon-hdd:before {\n  content: \"\\e121\";\n}\n.glyphicon-bullhorn:before {\n  content: \"\\e122\";\n}\n.glyphicon-bell:before {\n  content: \"\\e123\";\n}\n.glyphicon-certificate:before {\n  content: \"\\e124\";\n}\n.glyphicon-thumbs-up:before {\n  content: \"\\e125\";\n}\n.glyphicon-thumbs-down:before {\n  content: \"\\e126\";\n}\n.glyphicon-hand-right:before {\n  content: \"\\e127\";\n}\n.glyphicon-hand-left:before {\n  content: \"\\e128\";\n}\n.glyphicon-hand-up:before {\n  content: \"\\e129\";\n}\n.glyphicon-hand-down:before {\n  content: \"\\e130\";\n}\n.glyphicon-circle-arrow-right:before {\n  content: \"\\e131\";\n}\n.glyphicon-circle-arrow-left:before {\n  content: \"\\e132\";\n}\n.glyphicon-circle-arrow-up:before {\n  content: \"\\e133\";\n}\n.glyphicon-circle-arrow-down:before {\n  content: \"\\e134\";\n}\n.glyphicon-globe:before {\n  content: \"\\e135\";\n}\n.glyphicon-wrench:before {\n  content: \"\\e136\";\n}\n.glyphicon-tasks:before {\n  content: \"\\e137\";\n}\n.glyphicon-filter:before {\n  content: \"\\e138\";\n}\n.glyphicon-briefcase:before {\n  content: \"\\e139\";\n}\n.glyphicon-fullscreen:before {\n  content: \"\\e140\";\n}\n.glyphicon-dashboard:before {\n  content: \"\\e141\";\n}\n.glyphicon-paperclip:before {\n  content: \"\\e142\";\n}\n.glyphicon-heart-empty:before {\n  content: \"\\e143\";\n}\n.glyphicon-link:before {\n  content: \"\\e144\";\n}\n.glyphicon-phone:before {\n  content: \"\\e145\";\n}\n.glyphicon-pushpin:before {\n  content: \"\\e146\";\n}\n.glyphicon-usd:before {\n  content: \"\\e148\";\n}\n.glyphicon-gbp:before {\n  content: \"\\e149\";\n}\n.glyphicon-sort:before {\n  content: \"\\e150\";\n}\n.glyphicon-sort-by-alphabet:before {\n  content: \"\\e151\";\n}\n.glyphicon-sort-by-alphabet-alt:before {\n  content: \"\\e152\";\n}\n.glyphicon-sort-by-order:before {\n  content: \"\\e153\";\n}\n.glyphicon-sort-by-order-alt:before {\n  content: \"\\e154\";\n}\n.glyphicon-sort-by-attributes:before {\n  content: \"\\e155\";\n}\n.glyphicon-sort-by-attributes-alt:before {\n  content: \"\\e156\";\n}\n.glyphicon-unchecked:before {\n  content: \"\\e157\";\n}\n.glyphicon-expand:before {\n  content: \"\\e158\";\n}\n.glyphicon-collapse-down:before {\n  content: \"\\e159\";\n}\n.glyphicon-collapse-up:before {\n  content: \"\\e160\";\n}\n.glyphicon-log-in:before {\n  content: \"\\e161\";\n}\n.glyphicon-flash:before {\n  content: \"\\e162\";\n}\n.glyphicon-log-out:before {\n  content: \"\\e163\";\n}\n.glyphicon-new-window:before {\n  content: \"\\e164\";\n}\n.glyphicon-record:before {\n  content: \"\\e165\";\n}\n.glyphicon-save:before {\n  content: \"\\e166\";\n}\n.glyphicon-open:before {\n  content: \"\\e167\";\n}\n.glyphicon-saved:before {\n  content: \"\\e168\";\n}\n.glyphicon-import:before {\n  content: \"\\e169\";\n}\n.glyphicon-export:before {\n  content: \"\\e170\";\n}\n.glyphicon-send:before {\n  content: \"\\e171\";\n}\n.glyphicon-floppy-disk:before {\n  content: \"\\e172\";\n}\n.glyphicon-floppy-saved:before {\n  content: \"\\e173\";\n}\n.glyphicon-floppy-remove:before {\n  content: \"\\e174\";\n}\n.glyphicon-floppy-save:before {\n  content: \"\\e175\";\n}\n.glyphicon-floppy-open:before {\n  content: \"\\e176\";\n}\n.glyphicon-credit-card:before {\n  content: \"\\e177\";\n}\n.glyphicon-transfer:before {\n  content: \"\\e178\";\n}\n.glyphicon-cutlery:before {\n  content: \"\\e179\";\n}\n.glyphicon-header:before {\n  content: \"\\e180\";\n}\n.glyphicon-compressed:before {\n  content: \"\\e181\";\n}\n.glyphicon-earphone:before {\n  content: \"\\e182\";\n}\n.glyphicon-phone-alt:before {\n  content: \"\\e183\";\n}\n.glyphicon-tower:before {\n  content: \"\\e184\";\n}\n.glyphicon-stats:before {\n  content: \"\\e185\";\n}\n.glyphicon-sd-video:before {\n  content: \"\\e186\";\n}\n.glyphicon-hd-video:before {\n  content: \"\\e187\";\n}\n.glyphicon-subtitles:before {\n  content: \"\\e188\";\n}\n.glyphicon-sound-stereo:before {\n  content: \"\\e189\";\n}\n.glyphicon-sound-dolby:before {\n  content: \"\\e190\";\n}\n.glyphicon-sound-5-1:before {\n  content: \"\\e191\";\n}\n.glyphicon-sound-6-1:before {\n  content: \"\\e192\";\n}\n.glyphicon-sound-7-1:before {\n  content: \"\\e193\";\n}\n.glyphicon-copyright-mark:before {\n  content: \"\\e194\";\n}\n.glyphicon-registration-mark:before {\n  content: \"\\e195\";\n}\n.glyphicon-cloud-download:before {\n  content: \"\\e197\";\n}\n.glyphicon-cloud-upload:before {\n  content: \"\\e198\";\n}\n.glyphicon-tree-conifer:before {\n  content: \"\\e199\";\n}\n.glyphicon-tree-deciduous:before {\n  content: \"\\e200\";\n}\n.glyphicon-cd:before {\n  content: \"\\e201\";\n}\n.glyphicon-save-file:before {\n  content: \"\\e202\";\n}\n.glyphicon-open-file:before {\n  content: \"\\e203\";\n}\n.glyphicon-level-up:before {\n  content: \"\\e204\";\n}\n.glyphicon-copy:before {\n  content: \"\\e205\";\n}\n.glyphicon-paste:before {\n  content: \"\\e206\";\n}\n.glyphicon-alert:before {\n  content: \"\\e209\";\n}\n.glyphicon-equalizer:before {\n  content: \"\\e210\";\n}\n.glyphicon-king:before {\n  content: \"\\e211\";\n}\n.glyphicon-queen:before {\n  content: \"\\e212\";\n}\n.glyphicon-pawn:before {\n  content: \"\\e213\";\n}\n.glyphicon-bishop:before {\n  content: \"\\e214\";\n}\n.glyphicon-knight:before {\n  content: \"\\e215\";\n}\n.glyphicon-baby-formula:before {\n  content: \"\\e216\";\n}\n.glyphicon-tent:before {\n  content: \"\\26fa\";\n}\n.glyphicon-blackboard:before {\n  content: \"\\e218\";\n}\n.glyphicon-bed:before {\n  content: \"\\e219\";\n}\n.glyphicon-apple:before {\n  content: \"\\f8ff\";\n}\n.glyphicon-erase:before {\n  content: \"\\e221\";\n}\n.glyphicon-hourglass:before {\n  content: \"\\231b\";\n}\n.glyphicon-lamp:before {\n  content: \"\\e223\";\n}\n.glyphicon-duplicate:before {\n  content: \"\\e224\";\n}\n.glyphicon-piggy-bank:before {\n  content: \"\\e225\";\n}\n.glyphicon-scissors:before {\n  content: \"\\e226\";\n}\n.glyphicon-bitcoin:before {\n  content: \"\\e227\";\n}\n.glyphicon-btc:before {\n  content: \"\\e227\";\n}\n.glyphicon-xbt:before {\n  content: \"\\e227\";\n}\n.glyphicon-yen:before {\n  content: \"\\00a5\";\n}\n.glyphicon-jpy:before {\n  content: \"\\00a5\";\n}\n.glyphicon-ruble:before {\n  content: \"\\20bd\";\n}\n.glyphicon-rub:before {\n  content: \"\\20bd\";\n}\n.glyphicon-scale:before {\n  content: \"\\e230\";\n}\n.glyphicon-ice-lolly:before {\n  content: \"\\e231\";\n}\n.glyphicon-ice-lolly-tasted:before {\n  content: \"\\e232\";\n}\n.glyphicon-education:before {\n  content: \"\\e233\";\n}\n.glyphicon-option-horizontal:before {\n  content: \"\\e234\";\n}\n.glyphicon-option-vertical:before {\n  content: \"\\e235\";\n}\n.glyphicon-menu-hamburger:before {\n  content: \"\\e236\";\n}\n.glyphicon-modal-window:before {\n  content: \"\\e237\";\n}\n.glyphicon-oil:before {\n  content: \"\\e238\";\n}\n.glyphicon-grain:before {\n  content: \"\\e239\";\n}\n.glyphicon-sunglasses:before {\n  content: \"\\e240\";\n}\n.glyphicon-text-size:before {\n  content: \"\\e241\";\n}\n.glyphicon-text-color:before {\n  content: \"\\e242\";\n}\n.glyphicon-text-background:before {\n  content: \"\\e243\";\n}\n.glyphicon-object-align-top:before {\n  content: \"\\e244\";\n}\n.glyphicon-object-align-bottom:before {\n  content: \"\\e245\";\n}\n.glyphicon-object-align-horizontal:before {\n  content: \"\\e246\";\n}\n.glyphicon-object-align-left:before {\n  content: \"\\e247\";\n}\n.glyphicon-object-align-vertical:before {\n  content: \"\\e248\";\n}\n.glyphicon-object-align-right:before {\n  content: \"\\e249\";\n}\n.glyphicon-triangle-right:before {\n  content: \"\\e250\";\n}\n.glyphicon-triangle-left:before {\n  content: \"\\e251\";\n}\n.glyphicon-triangle-bottom:before {\n  content: \"\\e252\";\n}\n.glyphicon-triangle-top:before {\n  content: \"\\e253\";\n}\n.glyphicon-console:before {\n  content: \"\\e254\";\n}\n.glyphicon-superscript:before {\n  content: \"\\e255\";\n}\n.glyphicon-subscript:before {\n  content: \"\\e256\";\n}\n.glyphicon-menu-left:before {\n  content: \"\\e257\";\n}\n.glyphicon-menu-right:before {\n  content: \"\\e258\";\n}\n.glyphicon-menu-down:before {\n  content: \"\\e259\";\n}\n.glyphicon-menu-up:before {\n  content: \"\\e260\";\n}\n* {\n  -webkit-box-sizing: border-box;\n  -moz-box-sizing: border-box;\n  box-sizing: border-box;\n}\n*:before,\n*:after {\n  -webkit-box-sizing: border-box;\n  -moz-box-sizing: border-box;\n  box-sizing: border-box;\n}\nhtml {\n  font-size: 10px;\n  -webkit-tap-highlight-color: rgba(0, 0, 0, 0);\n}\nbody {\n  font-family: 'Source Sans Pro', sans-serif;\n  font-size: 16px;\n  line-height: 1.7;\n  color: #1e252f;\n  background-color: #1d252f;\n}\ninput,\nbutton,\nselect,\ntextarea {\n  font-family: inherit;\n  font-size: inherit;\n  line-height: inherit;\n}\na {\n  color: #2d7ef4;\n  text-decoration: none;\n}\na:hover,\na:focus {\n  color: #0b58ca;\n  text-decoration: underline;\n}\na:focus {\n  outline: 5px auto -webkit-focus-ring-color;\n  outline-offset: -2px;\n}\nfigure {\n  margin: 0;\n}\nimg {\n  vertical-align: middle;\n}\n.img-responsive,\n.thumbnail > img,\n.thumbnail a > img,\n.carousel-inner > .item > img,\n.carousel-inner > .item > a > img {\n  display: block;\n  max-width: 100%;\n  height: auto;\n}\n.img-rounded {\n  border-radius: 0;\n}\n.img-thumbnail {\n  padding: 4px;\n  line-height: 1.7;\n  background-color: #1d252f;\n  border: 1px solid #dddddd;\n  border-radius: 0;\n  -webkit-transition: all 0.2s ease-in-out;\n  -o-transition: all 0.2s ease-in-out;\n  transition: all 0.2s ease-in-out;\n  display: inline-block;\n  max-width: 100%;\n  height: auto;\n}\n.img-circle {\n  border-radius: 50%;\n}\nhr {\n  margin-top: 27px;\n  margin-bottom: 27px;\n  border: 0;\n  border-top: 1px solid #194c5f;\n}\n.sr-only {\n  position: absolute;\n  width: 1px;\n  height: 1px;\n  margin: -1px;\n  padding: 0;\n  overflow: hidden;\n  clip: rect(0, 0, 0, 0);\n  border: 0;\n}\n.sr-only-focusable:active,\n.sr-only-focusable:focus {\n  position: static;\n  width: auto;\n  height: auto;\n  margin: 0;\n  overflow: visible;\n  clip: auto;\n}\n[role=\"button\"] {\n  cursor: pointer;\n}\nh1,\nh2,\nh3,\nh4,\nh5,\nh6,\n.h1,\n.h2,\n.h3,\n.h4,\n.h5,\n.h6 {\n  font-family: inherit;\n  font-weight: 400;\n  line-height: 1.1;\n  color: #1e252f;\n}\nh1 small,\nh2 small,\nh3 small,\nh4 small,\nh5 small,\nh6 small,\n.h1 small,\n.h2 small,\n.h3 small,\n.h4 small,\n.h5 small,\n.h6 small,\nh1 .small,\nh2 .small,\nh3 .small,\nh4 .small,\nh5 .small,\nh6 .small,\n.h1 .small,\n.h2 .small,\n.h3 .small,\n.h4 .small,\n.h5 .small,\n.h6 .small {\n  font-weight: normal;\n  line-height: 1;\n  color: #757575;\n}\nh1,\n.h1,\nh2,\n.h2,\nh3,\n.h3 {\n  margin-top: 27px;\n  margin-bottom: 13.5px;\n}\nh1 small,\n.h1 small,\nh2 small,\n.h2 small,\nh3 small,\n.h3 small,\nh1 .small,\n.h1 .small,\nh2 .small,\n.h2 .small,\nh3 .small,\n.h3 .small {\n  font-size: 65%;\n}\nh4,\n.h4,\nh5,\n.h5,\nh6,\n.h6 {\n  margin-top: 13.5px;\n  margin-bottom: 13.5px;\n}\nh4 small,\n.h4 small,\nh5 small,\n.h5 small,\nh6 small,\n.h6 small,\nh4 .small,\n.h4 .small,\nh5 .small,\n.h5 .small,\nh6 .small,\n.h6 .small {\n  font-size: 75%;\n}\nh1,\n.h1 {\n  font-size: 57px;\n}\nh2,\n.h2 {\n  font-size: 51px;\n}\nh3,\n.h3 {\n  font-size: 45px;\n}\nh4,\n.h4 {\n  font-size: 39px;\n}\nh5,\n.h5 {\n  font-size: 29px;\n}\nh6,\n.h6 {\n  font-size: 20px;\n}\np {\n  margin: 0 0 13.5px;\n}\n.lead {\n  margin-bottom: 27px;\n  font-size: 18px;\n  font-weight: 300;\n  line-height: 1.4;\n}\n@media (min-width: 768px) {\n  .lead {\n    font-size: 24px;\n  }\n}\nsmall,\n.small {\n  font-size: 87%;\n}\nmark,\n.mark {\n  background-color: #fff4cc;\n  padding: .2em;\n}\n.text-left {\n  text-align: left;\n}\n.text-right {\n  text-align: right;\n}\n.text-center {\n  text-align: center;\n}\n.text-justify {\n  text-align: justify;\n}\n.text-nowrap {\n  white-space: nowrap;\n}\n.text-lowercase {\n  text-transform: lowercase;\n}\n.text-uppercase {\n  text-transform: uppercase;\n}\n.text-capitalize {\n  text-transform: capitalize;\n}\n.text-muted {\n  color: rgba(30, 37, 47, 0.6);\n}\n.text-primary {\n  color: #2d7ef4;\n}\na.text-primary:hover,\na.text-primary:focus {\n  color: #0c63e2;\n}\n.text-success {\n  color: #24b47e;\n}\na.text-success:hover,\na.text-success:focus {\n  color: #1c8a60;\n}\n.text-info {\n  color: #06a2ff;\n}\na.text-info:hover,\na.text-info:focus {\n  color: #0084d2;\n}\n.text-warning {\n  color: #de8e27;\n}\na.text-warning:hover,\na.text-warning:focus {\n  color: #b6721c;\n}\n.text-danger {\n  color: #ff2b67;\n}\na.text-danger:hover,\na.text-danger:focus {\n  color: #f70046;\n}\n.bg-primary {\n  color: #fff;\n  background-color: #2d7ef4;\n}\na.bg-primary:hover,\na.bg-primary:focus {\n  background-color: #0c63e2;\n}\n.bg-success {\n  background-color: #e3f1e4;\n}\na.bg-success:hover,\na.bg-success:focus {\n  background-color: #c1e0c3;\n}\n.bg-info {\n  background-color: #e5f3fa;\n}\na.bg-info:hover,\na.bg-info:focus {\n  background-color: #badff2;\n}\n.bg-warning {\n  background-color: #fff4cc;\n}\na.bg-warning:hover,\na.bg-warning:focus {\n  background-color: #ffe999;\n}\n.bg-danger {\n  background-color: #f9e9e9;\n}\na.bg-danger:hover,\na.bg-danger:focus {\n  background-color: #eec1c1;\n}\n.page-header {\n  padding-bottom: 12.5px;\n  margin: 54px 0 27px;\n  border-bottom: 1px solid #194c5f;\n}\nul,\nol {\n  margin-top: 0;\n  margin-bottom: 13.5px;\n}\nul ul,\nol ul,\nul ol,\nol ol {\n  margin-bottom: 0;\n}\n.list-unstyled {\n  padding-left: 0;\n  list-style: none;\n}\n.list-inline {\n  padding-left: 0;\n  list-style: none;\n  margin-left: -5px;\n}\n.list-inline > li {\n  display: inline-block;\n  padding-left: 5px;\n  padding-right: 5px;\n}\ndl {\n  margin-top: 0;\n  margin-bottom: 27px;\n}\ndt,\ndd {\n  line-height: 1.7;\n}\ndt {\n  font-weight: bold;\n}\ndd {\n  margin-left: 0;\n}\n@media (min-width: 992px) {\n  .dl-horizontal dt {\n    float: left;\n    width: 160px;\n    clear: left;\n    text-align: right;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n  }\n  .dl-horizontal dd {\n    margin-left: 180px;\n  }\n}\nabbr[title],\nabbr[data-original-title] {\n  cursor: help;\n  border-bottom: 1px dotted #e6e6e6;\n}\n.initialism {\n  font-size: 90%;\n  text-transform: uppercase;\n}\nblockquote {\n  padding: 13.5px 27px;\n  margin: 0 0 27px;\n  font-size: 20px;\n  border-left: 5px solid rgba(255, 255, 255, 0.14);\n}\nblockquote p:last-child,\nblockquote ul:last-child,\nblockquote ol:last-child {\n  margin-bottom: 0;\n}\nblockquote footer,\nblockquote small,\nblockquote .small {\n  display: block;\n  font-size: 80%;\n  line-height: 1.7;\n  color: rgba(30, 37, 47, 0.6);\n}\nblockquote footer:before,\nblockquote small:before,\nblockquote .small:before {\n  content: '\\2014 \\00A0';\n}\n.blockquote-reverse,\nblockquote.pull-right {\n  padding-right: 15px;\n  padding-left: 0;\n  border-right: 5px solid rgba(255, 255, 255, 0.14);\n  border-left: 0;\n  text-align: right;\n}\n.blockquote-reverse footer:before,\nblockquote.pull-right footer:before,\n.blockquote-reverse small:before,\nblockquote.pull-right small:before,\n.blockquote-reverse .small:before,\nblockquote.pull-right .small:before {\n  content: '';\n}\n.blockquote-reverse footer:after,\nblockquote.pull-right footer:after,\n.blockquote-reverse small:after,\nblockquote.pull-right small:after,\n.blockquote-reverse .small:after,\nblockquote.pull-right .small:after {\n  content: '\\00A0 \\2014';\n}\naddress {\n  margin-bottom: 27px;\n  font-style: normal;\n  line-height: 1.7;\n}\ncode,\nkbd,\npre,\nsamp {\n  font-family: Menlo, Monaco, Consolas, \"Courier New\", monospace;\n}\ncode {\n  padding: 2px 4px;\n  font-size: 90%;\n  color: #c7254e;\n  background-color: #f9f2f4;\n  border-radius: 0;\n}\nkbd {\n  padding: 2px 4px;\n  font-size: 90%;\n  color: #ffffff;\n  background-color: #333333;\n  border-radius: 0;\n  box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.25);\n}\nkbd kbd {\n  padding: 0;\n  font-size: 100%;\n  font-weight: bold;\n  box-shadow: none;\n}\npre {\n  display: block;\n  padding: 13px;\n  margin: 0 0 13.5px;\n  font-size: 15px;\n  line-height: 1.7;\n  word-break: break-all;\n  word-wrap: break-word;\n  color: #404040;\n  background-color: #f5f5f5;\n  border: 1px solid #cccccc;\n  border-radius: 0;\n}\npre code {\n  padding: 0;\n  font-size: inherit;\n  color: inherit;\n  white-space: pre-wrap;\n  background-color: transparent;\n  border-radius: 0;\n}\n.pre-scrollable {\n  max-height: 340px;\n  overflow-y: scroll;\n}\n.container {\n  margin-right: auto;\n  margin-left: auto;\n  padding-left: 15px;\n  padding-right: 15px;\n}\n@media (min-width: 768px) {\n  .container {\n    width: 750px;\n  }\n}\n@media (min-width: 992px) {\n  .container {\n    width: 970px;\n  }\n}\n@media (min-width: 992px) {\n  .container {\n    width: 970px;\n  }\n}\n.container-fluid {\n  margin-right: auto;\n  margin-left: auto;\n  padding-left: 15px;\n  padding-right: 15px;\n}\n.row {\n  margin-left: -15px;\n  margin-right: -15px;\n}\n.col-xs-1, .col-sm-1, .col-md-1, .col-lg-1, .col-xs-2, .col-sm-2, .col-md-2, .col-lg-2, .col-xs-3, .col-sm-3, .col-md-3, .col-lg-3, .col-xs-4, .col-sm-4, .col-md-4, .col-lg-4, .col-xs-5, .col-sm-5, .col-md-5, .col-lg-5, .col-xs-6, .col-sm-6, .col-md-6, .col-lg-6, .col-xs-7, .col-sm-7, .col-md-7, .col-lg-7, .col-xs-8, .col-sm-8, .col-md-8, .col-lg-8, .col-xs-9, .col-sm-9, .col-md-9, .col-lg-9, .col-xs-10, .col-sm-10, .col-md-10, .col-lg-10, .col-xs-11, .col-sm-11, .col-md-11, .col-lg-11, .col-xs-12, .col-sm-12, .col-md-12, .col-lg-12 {\n  position: relative;\n  min-height: 1px;\n  padding-left: 15px;\n  padding-right: 15px;\n}\n.col-xs-1, .col-xs-2, .col-xs-3, .col-xs-4, .col-xs-5, .col-xs-6, .col-xs-7, .col-xs-8, .col-xs-9, .col-xs-10, .col-xs-11, .col-xs-12 {\n  float: left;\n}\n.col-xs-12 {\n  width: 100%;\n}\n.col-xs-11 {\n  width: 91.66666667%;\n}\n.col-xs-10 {\n  width: 83.33333333%;\n}\n.col-xs-9 {\n  width: 75%;\n}\n.col-xs-8 {\n  width: 66.66666667%;\n}\n.col-xs-7 {\n  width: 58.33333333%;\n}\n.col-xs-6 {\n  width: 50%;\n}\n.col-xs-5 {\n  width: 41.66666667%;\n}\n.col-xs-4 {\n  width: 33.33333333%;\n}\n.col-xs-3 {\n  width: 25%;\n}\n.col-xs-2 {\n  width: 16.66666667%;\n}\n.col-xs-1 {\n  width: 8.33333333%;\n}\n.col-xs-pull-12 {\n  right: 100%;\n}\n.col-xs-pull-11 {\n  right: 91.66666667%;\n}\n.col-xs-pull-10 {\n  right: 83.33333333%;\n}\n.col-xs-pull-9 {\n  right: 75%;\n}\n.col-xs-pull-8 {\n  right: 66.66666667%;\n}\n.col-xs-pull-7 {\n  right: 58.33333333%;\n}\n.col-xs-pull-6 {\n  right: 50%;\n}\n.col-xs-pull-5 {\n  right: 41.66666667%;\n}\n.col-xs-pull-4 {\n  right: 33.33333333%;\n}\n.col-xs-pull-3 {\n  right: 25%;\n}\n.col-xs-pull-2 {\n  right: 16.66666667%;\n}\n.col-xs-pull-1 {\n  right: 8.33333333%;\n}\n.col-xs-pull-0 {\n  right: auto;\n}\n.col-xs-push-12 {\n  left: 100%;\n}\n.col-xs-push-11 {\n  left: 91.66666667%;\n}\n.col-xs-push-10 {\n  left: 83.33333333%;\n}\n.col-xs-push-9 {\n  left: 75%;\n}\n.col-xs-push-8 {\n  left: 66.66666667%;\n}\n.col-xs-push-7 {\n  left: 58.33333333%;\n}\n.col-xs-push-6 {\n  left: 50%;\n}\n.col-xs-push-5 {\n  left: 41.66666667%;\n}\n.col-xs-push-4 {\n  left: 33.33333333%;\n}\n.col-xs-push-3 {\n  left: 25%;\n}\n.col-xs-push-2 {\n  left: 16.66666667%;\n}\n.col-xs-push-1 {\n  left: 8.33333333%;\n}\n.col-xs-push-0 {\n  left: auto;\n}\n.col-xs-offset-12 {\n  margin-left: 100%;\n}\n.col-xs-offset-11 {\n  margin-left: 91.66666667%;\n}\n.col-xs-offset-10 {\n  margin-left: 83.33333333%;\n}\n.col-xs-offset-9 {\n  margin-left: 75%;\n}\n.col-xs-offset-8 {\n  margin-left: 66.66666667%;\n}\n.col-xs-offset-7 {\n  margin-left: 58.33333333%;\n}\n.col-xs-offset-6 {\n  margin-left: 50%;\n}\n.col-xs-offset-5 {\n  margin-left: 41.66666667%;\n}\n.col-xs-offset-4 {\n  margin-left: 33.33333333%;\n}\n.col-xs-offset-3 {\n  margin-left: 25%;\n}\n.col-xs-offset-2 {\n  margin-left: 16.66666667%;\n}\n.col-xs-offset-1 {\n  margin-left: 8.33333333%;\n}\n.col-xs-offset-0 {\n  margin-left: 0%;\n}\n@media (min-width: 768px) {\n  .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12 {\n    float: left;\n  }\n  .col-sm-12 {\n    width: 100%;\n  }\n  .col-sm-11 {\n    width: 91.66666667%;\n  }\n  .col-sm-10 {\n    width: 83.33333333%;\n  }\n  .col-sm-9 {\n    width: 75%;\n  }\n  .col-sm-8 {\n    width: 66.66666667%;\n  }\n  .col-sm-7 {\n    width: 58.33333333%;\n  }\n  .col-sm-6 {\n    width: 50%;\n  }\n  .col-sm-5 {\n    width: 41.66666667%;\n  }\n  .col-sm-4 {\n    width: 33.33333333%;\n  }\n  .col-sm-3 {\n    width: 25%;\n  }\n  .col-sm-2 {\n    width: 16.66666667%;\n  }\n  .col-sm-1 {\n    width: 8.33333333%;\n  }\n  .col-sm-pull-12 {\n    right: 100%;\n  }\n  .col-sm-pull-11 {\n    right: 91.66666667%;\n  }\n  .col-sm-pull-10 {\n    right: 83.33333333%;\n  }\n  .col-sm-pull-9 {\n    right: 75%;\n  }\n  .col-sm-pull-8 {\n    right: 66.66666667%;\n  }\n  .col-sm-pull-7 {\n    right: 58.33333333%;\n  }\n  .col-sm-pull-6 {\n    right: 50%;\n  }\n  .col-sm-pull-5 {\n    right: 41.66666667%;\n  }\n  .col-sm-pull-4 {\n    right: 33.33333333%;\n  }\n  .col-sm-pull-3 {\n    right: 25%;\n  }\n  .col-sm-pull-2 {\n    right: 16.66666667%;\n  }\n  .col-sm-pull-1 {\n    right: 8.33333333%;\n  }\n  .col-sm-pull-0 {\n    right: auto;\n  }\n  .col-sm-push-12 {\n    left: 100%;\n  }\n  .col-sm-push-11 {\n    left: 91.66666667%;\n  }\n  .col-sm-push-10 {\n    left: 83.33333333%;\n  }\n  .col-sm-push-9 {\n    left: 75%;\n  }\n  .col-sm-push-8 {\n    left: 66.66666667%;\n  }\n  .col-sm-push-7 {\n    left: 58.33333333%;\n  }\n  .col-sm-push-6 {\n    left: 50%;\n  }\n  .col-sm-push-5 {\n    left: 41.66666667%;\n  }\n  .col-sm-push-4 {\n    left: 33.33333333%;\n  }\n  .col-sm-push-3 {\n    left: 25%;\n  }\n  .col-sm-push-2 {\n    left: 16.66666667%;\n  }\n  .col-sm-push-1 {\n    left: 8.33333333%;\n  }\n  .col-sm-push-0 {\n    left: auto;\n  }\n  .col-sm-offset-12 {\n    margin-left: 100%;\n  }\n  .col-sm-offset-11 {\n    margin-left: 91.66666667%;\n  }\n  .col-sm-offset-10 {\n    margin-left: 83.33333333%;\n  }\n  .col-sm-offset-9 {\n    margin-left: 75%;\n  }\n  .col-sm-offset-8 {\n    margin-left: 66.66666667%;\n  }\n  .col-sm-offset-7 {\n    margin-left: 58.33333333%;\n  }\n  .col-sm-offset-6 {\n    margin-left: 50%;\n  }\n  .col-sm-offset-5 {\n    margin-left: 41.66666667%;\n  }\n  .col-sm-offset-4 {\n    margin-left: 33.33333333%;\n  }\n  .col-sm-offset-3 {\n    margin-left: 25%;\n  }\n  .col-sm-offset-2 {\n    margin-left: 16.66666667%;\n  }\n  .col-sm-offset-1 {\n    margin-left: 8.33333333%;\n  }\n  .col-sm-offset-0 {\n    margin-left: 0%;\n  }\n}\n@media (min-width: 992px) {\n  .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12 {\n    float: left;\n  }\n  .col-md-12 {\n    width: 100%;\n  }\n  .col-md-11 {\n    width: 91.66666667%;\n  }\n  .col-md-10 {\n    width: 83.33333333%;\n  }\n  .col-md-9 {\n    width: 75%;\n  }\n  .col-md-8 {\n    width: 66.66666667%;\n  }\n  .col-md-7 {\n    width: 58.33333333%;\n  }\n  .col-md-6 {\n    width: 50%;\n  }\n  .col-md-5 {\n    width: 41.66666667%;\n  }\n  .col-md-4 {\n    width: 33.33333333%;\n  }\n  .col-md-3 {\n    width: 25%;\n  }\n  .col-md-2 {\n    width: 16.66666667%;\n  }\n  .col-md-1 {\n    width: 8.33333333%;\n  }\n  .col-md-pull-12 {\n    right: 100%;\n  }\n  .col-md-pull-11 {\n    right: 91.66666667%;\n  }\n  .col-md-pull-10 {\n    right: 83.33333333%;\n  }\n  .col-md-pull-9 {\n    right: 75%;\n  }\n  .col-md-pull-8 {\n    right: 66.66666667%;\n  }\n  .col-md-pull-7 {\n    right: 58.33333333%;\n  }\n  .col-md-pull-6 {\n    right: 50%;\n  }\n  .col-md-pull-5 {\n    right: 41.66666667%;\n  }\n  .col-md-pull-4 {\n    right: 33.33333333%;\n  }\n  .col-md-pull-3 {\n    right: 25%;\n  }\n  .col-md-pull-2 {\n    right: 16.66666667%;\n  }\n  .col-md-pull-1 {\n    right: 8.33333333%;\n  }\n  .col-md-pull-0 {\n    right: auto;\n  }\n  .col-md-push-12 {\n    left: 100%;\n  }\n  .col-md-push-11 {\n    left: 91.66666667%;\n  }\n  .col-md-push-10 {\n    left: 83.33333333%;\n  }\n  .col-md-push-9 {\n    left: 75%;\n  }\n  .col-md-push-8 {\n    left: 66.66666667%;\n  }\n  .col-md-push-7 {\n    left: 58.33333333%;\n  }\n  .col-md-push-6 {\n    left: 50%;\n  }\n  .col-md-push-5 {\n    left: 41.66666667%;\n  }\n  .col-md-push-4 {\n    left: 33.33333333%;\n  }\n  .col-md-push-3 {\n    left: 25%;\n  }\n  .col-md-push-2 {\n    left: 16.66666667%;\n  }\n  .col-md-push-1 {\n    left: 8.33333333%;\n  }\n  .col-md-push-0 {\n    left: auto;\n  }\n  .col-md-offset-12 {\n    margin-left: 100%;\n  }\n  .col-md-offset-11 {\n    margin-left: 91.66666667%;\n  }\n  .col-md-offset-10 {\n    margin-left: 83.33333333%;\n  }\n  .col-md-offset-9 {\n    margin-left: 75%;\n  }\n  .col-md-offset-8 {\n    margin-left: 66.66666667%;\n  }\n  .col-md-offset-7 {\n    margin-left: 58.33333333%;\n  }\n  .col-md-offset-6 {\n    margin-left: 50%;\n  }\n  .col-md-offset-5 {\n    margin-left: 41.66666667%;\n  }\n  .col-md-offset-4 {\n    margin-left: 33.33333333%;\n  }\n  .col-md-offset-3 {\n    margin-left: 25%;\n  }\n  .col-md-offset-2 {\n    margin-left: 16.66666667%;\n  }\n  .col-md-offset-1 {\n    margin-left: 8.33333333%;\n  }\n  .col-md-offset-0 {\n    margin-left: 0%;\n  }\n}\n@media (min-width: 992px) {\n  .col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12 {\n    float: left;\n  }\n  .col-lg-12 {\n    width: 100%;\n  }\n  .col-lg-11 {\n    width: 91.66666667%;\n  }\n  .col-lg-10 {\n    width: 83.33333333%;\n  }\n  .col-lg-9 {\n    width: 75%;\n  }\n  .col-lg-8 {\n    width: 66.66666667%;\n  }\n  .col-lg-7 {\n    width: 58.33333333%;\n  }\n  .col-lg-6 {\n    width: 50%;\n  }\n  .col-lg-5 {\n    width: 41.66666667%;\n  }\n  .col-lg-4 {\n    width: 33.33333333%;\n  }\n  .col-lg-3 {\n    width: 25%;\n  }\n  .col-lg-2 {\n    width: 16.66666667%;\n  }\n  .col-lg-1 {\n    width: 8.33333333%;\n  }\n  .col-lg-pull-12 {\n    right: 100%;\n  }\n  .col-lg-pull-11 {\n    right: 91.66666667%;\n  }\n  .col-lg-pull-10 {\n    right: 83.33333333%;\n  }\n  .col-lg-pull-9 {\n    right: 75%;\n  }\n  .col-lg-pull-8 {\n    right: 66.66666667%;\n  }\n  .col-lg-pull-7 {\n    right: 58.33333333%;\n  }\n  .col-lg-pull-6 {\n    right: 50%;\n  }\n  .col-lg-pull-5 {\n    right: 41.66666667%;\n  }\n  .col-lg-pull-4 {\n    right: 33.33333333%;\n  }\n  .col-lg-pull-3 {\n    right: 25%;\n  }\n  .col-lg-pull-2 {\n    right: 16.66666667%;\n  }\n  .col-lg-pull-1 {\n    right: 8.33333333%;\n  }\n  .col-lg-pull-0 {\n    right: auto;\n  }\n  .col-lg-push-12 {\n    left: 100%;\n  }\n  .col-lg-push-11 {\n    left: 91.66666667%;\n  }\n  .col-lg-push-10 {\n    left: 83.33333333%;\n  }\n  .col-lg-push-9 {\n    left: 75%;\n  }\n  .col-lg-push-8 {\n    left: 66.66666667%;\n  }\n  .col-lg-push-7 {\n    left: 58.33333333%;\n  }\n  .col-lg-push-6 {\n    left: 50%;\n  }\n  .col-lg-push-5 {\n    left: 41.66666667%;\n  }\n  .col-lg-push-4 {\n    left: 33.33333333%;\n  }\n  .col-lg-push-3 {\n    left: 25%;\n  }\n  .col-lg-push-2 {\n    left: 16.66666667%;\n  }\n  .col-lg-push-1 {\n    left: 8.33333333%;\n  }\n  .col-lg-push-0 {\n    left: auto;\n  }\n  .col-lg-offset-12 {\n    margin-left: 100%;\n  }\n  .col-lg-offset-11 {\n    margin-left: 91.66666667%;\n  }\n  .col-lg-offset-10 {\n    margin-left: 83.33333333%;\n  }\n  .col-lg-offset-9 {\n    margin-left: 75%;\n  }\n  .col-lg-offset-8 {\n    margin-left: 66.66666667%;\n  }\n  .col-lg-offset-7 {\n    margin-left: 58.33333333%;\n  }\n  .col-lg-offset-6 {\n    margin-left: 50%;\n  }\n  .col-lg-offset-5 {\n    margin-left: 41.66666667%;\n  }\n  .col-lg-offset-4 {\n    margin-left: 33.33333333%;\n  }\n  .col-lg-offset-3 {\n    margin-left: 25%;\n  }\n  .col-lg-offset-2 {\n    margin-left: 16.66666667%;\n  }\n  .col-lg-offset-1 {\n    margin-left: 8.33333333%;\n  }\n  .col-lg-offset-0 {\n    margin-left: 0%;\n  }\n}\ntable {\n  background-color: transparent;\n}\ncaption {\n  padding-top: 15px;\n  padding-bottom: 15px;\n  color: rgba(30, 37, 47, 0.6);\n  text-align: left;\n}\nth {\n  text-align: left;\n}\n.table {\n  width: 100%;\n  max-width: 100%;\n  margin-bottom: 27px;\n}\n.table > thead > tr > th,\n.table > tbody > tr > th,\n.table > tfoot > tr > th,\n.table > thead > tr > td,\n.table > tbody > tr > td,\n.table > tfoot > tr > td {\n  padding: 15px;\n  line-height: 1.7;\n  vertical-align: top;\n  border-top: 1px solid #194c5f;\n}\n.table > thead > tr > th {\n  vertical-align: bottom;\n  border-bottom: 2px solid #194c5f;\n}\n.table > caption + thead > tr:first-child > th,\n.table > colgroup + thead > tr:first-child > th,\n.table > thead:first-child > tr:first-child > th,\n.table > caption + thead > tr:first-child > td,\n.table > colgroup + thead > tr:first-child > td,\n.table > thead:first-child > tr:first-child > td {\n  border-top: 0;\n}\n.table > tbody + tbody {\n  border-top: 2px solid #194c5f;\n}\n.table .table {\n  background-color: #1d252f;\n}\n.table-condensed > thead > tr > th,\n.table-condensed > tbody > tr > th,\n.table-condensed > tfoot > tr > th,\n.table-condensed > thead > tr > td,\n.table-condensed > tbody > tr > td,\n.table-condensed > tfoot > tr > td {\n  padding: 8px 15px;\n}\n.table-bordered {\n  border: 1px solid #194c5f;\n}\n.table-bordered > thead > tr > th,\n.table-bordered > tbody > tr > th,\n.table-bordered > tfoot > tr > th,\n.table-bordered > thead > tr > td,\n.table-bordered > tbody > tr > td,\n.table-bordered > tfoot > tr > td {\n  border: 1px solid #194c5f;\n}\n.table-bordered > thead > tr > th,\n.table-bordered > thead > tr > td {\n  border-bottom-width: 2px;\n}\n.table-striped > tbody > tr:nth-of-type(odd) {\n  background-color: #242e3b;\n}\n.table-hover > tbody > tr:hover {\n  background-color: #2d7ef4;\n}\ntable col[class*=\"col-\"] {\n  position: static;\n  float: none;\n  display: table-column;\n}\ntable td[class*=\"col-\"],\ntable th[class*=\"col-\"] {\n  position: static;\n  float: none;\n  display: table-cell;\n}\n.table > thead > tr > td.active,\n.table > tbody > tr > td.active,\n.table > tfoot > tr > td.active,\n.table > thead > tr > th.active,\n.table > tbody > tr > th.active,\n.table > tfoot > tr > th.active,\n.table > thead > tr.active > td,\n.table > tbody > tr.active > td,\n.table > tfoot > tr.active > td,\n.table > thead > tr.active > th,\n.table > tbody > tr.active > th,\n.table > tfoot > tr.active > th {\n  background-color: #2d7ef4;\n}\n.table-hover > tbody > tr > td.active:hover,\n.table-hover > tbody > tr > th.active:hover,\n.table-hover > tbody > tr.active:hover > td,\n.table-hover > tbody > tr:hover > .active,\n.table-hover > tbody > tr.active:hover > th {\n  background-color: #156ff3;\n}\n.table > thead > tr > td.success,\n.table > tbody > tr > td.success,\n.table > tfoot > tr > td.success,\n.table > thead > tr > th.success,\n.table > tbody > tr > th.success,\n.table > tfoot > tr > th.success,\n.table > thead > tr.success > td,\n.table > tbody > tr.success > td,\n.table > tfoot > tr.success > td,\n.table > thead > tr.success > th,\n.table > tbody > tr.success > th,\n.table > tfoot > tr.success > th {\n  background-color: #e3f1e4;\n}\n.table-hover > tbody > tr > td.success:hover,\n.table-hover > tbody > tr > th.success:hover,\n.table-hover > tbody > tr.success:hover > td,\n.table-hover > tbody > tr:hover > .success,\n.table-hover > tbody > tr.success:hover > th {\n  background-color: #d2e9d4;\n}\n.table > thead > tr > td.info,\n.table > tbody > tr > td.info,\n.table > tfoot > tr > td.info,\n.table > thead > tr > th.info,\n.table > tbody > tr > th.info,\n.table > tfoot > tr > th.info,\n.table > thead > tr.info > td,\n.table > tbody > tr.info > td,\n.table > tfoot > tr.info > td,\n.table > thead > tr.info > th,\n.table > tbody > tr.info > th,\n.table > tfoot > tr.info > th {\n  background-color: #e5f3fa;\n}\n.table-hover > tbody > tr > td.info:hover,\n.table-hover > tbody > tr > th.info:hover,\n.table-hover > tbody > tr.info:hover > td,\n.table-hover > tbody > tr:hover > .info,\n.table-hover > tbody > tr.info:hover > th {\n  background-color: #d0e9f6;\n}\n.table > thead > tr > td.warning,\n.table > tbody > tr > td.warning,\n.table > tfoot > tr > td.warning,\n.table > thead > tr > th.warning,\n.table > tbody > tr > th.warning,\n.table > tfoot > tr > th.warning,\n.table > thead > tr.warning > td,\n.table > tbody > tr.warning > td,\n.table > tfoot > tr.warning > td,\n.table > thead > tr.warning > th,\n.table > tbody > tr.warning > th,\n.table > tfoot > tr.warning > th {\n  background-color: #fff4cc;\n}\n.table-hover > tbody > tr > td.warning:hover,\n.table-hover > tbody > tr > th.warning:hover,\n.table-hover > tbody > tr.warning:hover > td,\n.table-hover > tbody > tr:hover > .warning,\n.table-hover > tbody > tr.warning:hover > th {\n  background-color: #ffefb3;\n}\n.table > thead > tr > td.danger,\n.table > tbody > tr > td.danger,\n.table > tfoot > tr > td.danger,\n.table > thead > tr > th.danger,\n.table > tbody > tr > th.danger,\n.table > tfoot > tr > th.danger,\n.table > thead > tr.danger > td,\n.table > tbody > tr.danger > td,\n.table > tfoot > tr.danger > td,\n.table > thead > tr.danger > th,\n.table > tbody > tr.danger > th,\n.table > tfoot > tr.danger > th {\n  background-color: #f9e9e9;\n}\n.table-hover > tbody > tr > td.danger:hover,\n.table-hover > tbody > tr > th.danger:hover,\n.table-hover > tbody > tr.danger:hover > td,\n.table-hover > tbody > tr:hover > .danger,\n.table-hover > tbody > tr.danger:hover > th {\n  background-color: #f4d5d5;\n}\n.table-responsive {\n  overflow-x: auto;\n  min-height: 0.01%;\n}\n@media screen and (max-width: 767px) {\n  .table-responsive {\n    width: 100%;\n    margin-bottom: 20.25px;\n    overflow-y: hidden;\n    -ms-overflow-style: -ms-autohiding-scrollbar;\n    border: 1px solid #194c5f;\n  }\n  .table-responsive > .table {\n    margin-bottom: 0;\n  }\n  .table-responsive > .table > thead > tr > th,\n  .table-responsive > .table > tbody > tr > th,\n  .table-responsive > .table > tfoot > tr > th,\n  .table-responsive > .table > thead > tr > td,\n  .table-responsive > .table > tbody > tr > td,\n  .table-responsive > .table > tfoot > tr > td {\n    white-space: nowrap;\n  }\n  .table-responsive > .table-bordered {\n    border: 0;\n  }\n  .table-responsive > .table-bordered > thead > tr > th:first-child,\n  .table-responsive > .table-bordered > tbody > tr > th:first-child,\n  .table-responsive > .table-bordered > tfoot > tr > th:first-child,\n  .table-responsive > .table-bordered > thead > tr > td:first-child,\n  .table-responsive > .table-bordered > tbody > tr > td:first-child,\n  .table-responsive > .table-bordered > tfoot > tr > td:first-child {\n    border-left: 0;\n  }\n  .table-responsive > .table-bordered > thead > tr > th:last-child,\n  .table-responsive > .table-bordered > tbody > tr > th:last-child,\n  .table-responsive > .table-bordered > tfoot > tr > th:last-child,\n  .table-responsive > .table-bordered > thead > tr > td:last-child,\n  .table-responsive > .table-bordered > tbody > tr > td:last-child,\n  .table-responsive > .table-bordered > tfoot > tr > td:last-child {\n    border-right: 0;\n  }\n  .table-responsive > .table-bordered > tbody > tr:last-child > th,\n  .table-responsive > .table-bordered > tfoot > tr:last-child > th,\n  .table-responsive > .table-bordered > tbody > tr:last-child > td,\n  .table-responsive > .table-bordered > tfoot > tr:last-child > td {\n    border-bottom: 0;\n  }\n}\nfieldset {\n  padding: 0;\n  margin: 0;\n  border: 0;\n  min-width: 0;\n}\nlegend {\n  display: block;\n  width: 100%;\n  padding: 0;\n  margin-bottom: 27px;\n  font-size: 24px;\n  line-height: inherit;\n  color: rgba(30, 37, 47, 0.6);\n  border: 0;\n  border-bottom: 1px solid rgba(255, 255, 255, 0.14);\n}\nlabel {\n  display: inline-block;\n  max-width: 100%;\n  margin-bottom: 5px;\n  font-weight: bold;\n}\ninput[type=\"search\"] {\n  -webkit-box-sizing: border-box;\n  -moz-box-sizing: border-box;\n  box-sizing: border-box;\n}\ninput[type=\"radio\"],\ninput[type=\"checkbox\"] {\n  margin: 4px 0 0;\n  margin-top: 1px \\9;\n  line-height: normal;\n}\ninput[type=\"file\"] {\n  display: block;\n}\ninput[type=\"range\"] {\n  display: block;\n  width: 100%;\n}\nselect[multiple],\nselect[size] {\n  height: auto;\n}\ninput[type=\"file\"]:focus,\ninput[type=\"radio\"]:focus,\ninput[type=\"checkbox\"]:focus {\n  outline: 5px auto -webkit-focus-ring-color;\n  outline-offset: -2px;\n}\noutput {\n  display: block;\n  padding-top: 7px;\n  font-size: 16px;\n  line-height: 1.7;\n  color: #1e252f;\n}\n.form-control {\n  display: block;\n  width: 100%;\n  height: 41px;\n  padding: 6px 12px;\n  font-size: 16px;\n  line-height: 1.7;\n  color: #1e252f;\n  background-color: #44576e;\n  background-image: none;\n  border: 1px solid #194c5f;\n  border-radius: 0;\n  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n  -webkit-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;\n  -o-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;\n  transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;\n}\n.form-control:focus {\n  border-color: #66afe9;\n  outline: 0;\n  -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, 0.6);\n  box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, 0.6);\n}\n.form-control::-moz-placeholder {\n  color: rgba(30, 37, 47, 0.6);\n  opacity: 1;\n}\n.form-control:-ms-input-placeholder {\n  color: rgba(30, 37, 47, 0.6);\n}\n.form-control::-webkit-input-placeholder {\n  color: rgba(30, 37, 47, 0.6);\n}\n.form-control::-ms-expand {\n  border: 0;\n  background-color: transparent;\n}\n.form-control[disabled],\n.form-control[readonly],\nfieldset[disabled] .form-control {\n  background-color: transparent;\n  opacity: 1;\n}\n.form-control[disabled],\nfieldset[disabled] .form-control {\n  cursor: not-allowed;\n}\ntextarea.form-control {\n  height: auto;\n}\ninput[type=\"search\"] {\n  -webkit-appearance: none;\n}\n@media screen and (-webkit-min-device-pixel-ratio: 0) {\n  input[type=\"date\"].form-control,\n  input[type=\"time\"].form-control,\n  input[type=\"datetime-local\"].form-control,\n  input[type=\"month\"].form-control {\n    line-height: 41px;\n  }\n  input[type=\"date\"].input-sm,\n  input[type=\"time\"].input-sm,\n  input[type=\"datetime-local\"].input-sm,\n  input[type=\"month\"].input-sm,\n  .input-group-sm input[type=\"date\"],\n  .input-group-sm input[type=\"time\"],\n  .input-group-sm input[type=\"datetime-local\"],\n  .input-group-sm input[type=\"month\"] {\n    line-height: 33px;\n  }\n  input[type=\"date\"].input-lg,\n  input[type=\"time\"].input-lg,\n  input[type=\"datetime-local\"].input-lg,\n  input[type=\"month\"].input-lg,\n  .input-group-lg input[type=\"date\"],\n  .input-group-lg input[type=\"time\"],\n  .input-group-lg input[type=\"datetime-local\"],\n  .input-group-lg input[type=\"month\"] {\n    line-height: 49px;\n  }\n}\n.form-group {\n  margin-bottom: 24px;\n}\n.radio,\n.checkbox {\n  position: relative;\n  display: block;\n  margin-top: 10px;\n  margin-bottom: 10px;\n}\n.radio label,\n.checkbox label {\n  min-height: 27px;\n  padding-left: 20px;\n  margin-bottom: 0;\n  font-weight: normal;\n  cursor: pointer;\n}\n.radio input[type=\"radio\"],\n.radio-inline input[type=\"radio\"],\n.checkbox input[type=\"checkbox\"],\n.checkbox-inline input[type=\"checkbox\"] {\n  position: absolute;\n  margin-left: -20px;\n  margin-top: 4px \\9;\n}\n.radio + .radio,\n.checkbox + .checkbox {\n  margin-top: -5px;\n}\n.radio-inline,\n.checkbox-inline {\n  position: relative;\n  display: inline-block;\n  padding-left: 20px;\n  margin-bottom: 0;\n  vertical-align: middle;\n  font-weight: normal;\n  cursor: pointer;\n}\n.radio-inline + .radio-inline,\n.checkbox-inline + .checkbox-inline {\n  margin-top: 0;\n  margin-left: 10px;\n}\ninput[type=\"radio\"][disabled],\ninput[type=\"checkbox\"][disabled],\ninput[type=\"radio\"].disabled,\ninput[type=\"checkbox\"].disabled,\nfieldset[disabled] input[type=\"radio\"],\nfieldset[disabled] input[type=\"checkbox\"] {\n  cursor: not-allowed;\n}\n.radio-inline.disabled,\n.checkbox-inline.disabled,\nfieldset[disabled] .radio-inline,\nfieldset[disabled] .checkbox-inline {\n  cursor: not-allowed;\n}\n.radio.disabled label,\n.checkbox.disabled label,\nfieldset[disabled] .radio label,\nfieldset[disabled] .checkbox label {\n  cursor: not-allowed;\n}\n.form-control-static {\n  padding-top: 7px;\n  padding-bottom: 7px;\n  margin-bottom: 0;\n  min-height: 43px;\n}\n.form-control-static.input-lg,\n.form-control-static.input-sm {\n  padding-left: 0;\n  padding-right: 0;\n}\n.input-sm {\n  height: 33px;\n  padding: 5px 10px;\n  font-size: 14px;\n  line-height: 1.5;\n  border-radius: 0;\n}\nselect.input-sm {\n  height: 33px;\n  line-height: 33px;\n}\ntextarea.input-sm,\nselect[multiple].input-sm {\n  height: auto;\n}\n.form-group-sm .form-control {\n  height: 33px;\n  padding: 5px 10px;\n  font-size: 14px;\n  line-height: 1.5;\n  border-radius: 0;\n}\n.form-group-sm select.form-control {\n  height: 33px;\n  line-height: 33px;\n}\n.form-group-sm textarea.form-control,\n.form-group-sm select[multiple].form-control {\n  height: auto;\n}\n.form-group-sm .form-control-static {\n  height: 33px;\n  min-height: 41px;\n  padding: 6px 10px;\n  font-size: 14px;\n  line-height: 1.5;\n}\n.input-lg {\n  height: 49px;\n  padding: 10px 16px;\n  font-size: 20px;\n  line-height: 1.3333333;\n  border-radius: 0;\n}\nselect.input-lg {\n  height: 49px;\n  line-height: 49px;\n}\ntextarea.input-lg,\nselect[multiple].input-lg {\n  height: auto;\n}\n.form-group-lg .form-control {\n  height: 49px;\n  padding: 10px 16px;\n  font-size: 20px;\n  line-height: 1.3333333;\n  border-radius: 0;\n}\n.form-group-lg select.form-control {\n  height: 49px;\n  line-height: 49px;\n}\n.form-group-lg textarea.form-control,\n.form-group-lg select[multiple].form-control {\n  height: auto;\n}\n.form-group-lg .form-control-static {\n  height: 49px;\n  min-height: 47px;\n  padding: 11px 16px;\n  font-size: 20px;\n  line-height: 1.3333333;\n}\n.has-feedback {\n  position: relative;\n}\n.has-feedback .form-control {\n  padding-right: 51.25px;\n}\n.form-control-feedback {\n  position: absolute;\n  top: 0;\n  right: 0;\n  z-index: 2;\n  display: block;\n  width: 41px;\n  height: 41px;\n  line-height: 41px;\n  text-align: center;\n  pointer-events: none;\n}\n.input-lg + .form-control-feedback,\n.input-group-lg + .form-control-feedback,\n.form-group-lg .form-control + .form-control-feedback {\n  width: 49px;\n  height: 49px;\n  line-height: 49px;\n}\n.input-sm + .form-control-feedback,\n.input-group-sm + .form-control-feedback,\n.form-group-sm .form-control + .form-control-feedback {\n  width: 33px;\n  height: 33px;\n  line-height: 33px;\n}\n.has-success .help-block,\n.has-success .control-label,\n.has-success .radio,\n.has-success .checkbox,\n.has-success .radio-inline,\n.has-success .checkbox-inline,\n.has-success.radio label,\n.has-success.checkbox label,\n.has-success.radio-inline label,\n.has-success.checkbox-inline label {\n  color: #24b47e;\n}\n.has-success .form-control {\n  border-color: #24b47e;\n  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n}\n.has-success .form-control:focus {\n  border-color: #1c8a60;\n  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #5fdfaf;\n  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #5fdfaf;\n}\n.has-success .input-group-addon {\n  color: #24b47e;\n  border-color: #24b47e;\n  background-color: #e3f1e4;\n}\n.has-success .form-control-feedback {\n  color: #24b47e;\n}\n.has-warning .help-block,\n.has-warning .control-label,\n.has-warning .radio,\n.has-warning .checkbox,\n.has-warning .radio-inline,\n.has-warning .checkbox-inline,\n.has-warning.radio label,\n.has-warning.checkbox label,\n.has-warning.radio-inline label,\n.has-warning.checkbox-inline label {\n  color: #de8e27;\n}\n.has-warning .form-control {\n  border-color: #de8e27;\n  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n}\n.has-warning .form-control:focus {\n  border-color: #b6721c;\n  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ebbc7f;\n  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ebbc7f;\n}\n.has-warning .input-group-addon {\n  color: #de8e27;\n  border-color: #de8e27;\n  background-color: #fff4cc;\n}\n.has-warning .form-control-feedback {\n  color: #de8e27;\n}\n.has-error .help-block,\n.has-error .control-label,\n.has-error .radio,\n.has-error .checkbox,\n.has-error .radio-inline,\n.has-error .checkbox-inline,\n.has-error.radio label,\n.has-error.checkbox label,\n.has-error.radio-inline label,\n.has-error.checkbox-inline label {\n  color: #ff2b67;\n}\n.has-error .form-control {\n  border-color: #ff2b67;\n  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n}\n.has-error .form-control:focus {\n  border-color: #f70046;\n  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ff91b0;\n  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ff91b0;\n}\n.has-error .input-group-addon {\n  color: #ff2b67;\n  border-color: #ff2b67;\n  background-color: #f9e9e9;\n}\n.has-error .form-control-feedback {\n  color: #ff2b67;\n}\n.has-feedback label ~ .form-control-feedback {\n  top: 32px;\n}\n.has-feedback label.sr-only ~ .form-control-feedback {\n  top: 0;\n}\n.help-block {\n  display: block;\n  margin-top: 5px;\n  margin-bottom: 10px;\n  color: #50627d;\n}\n@media (min-width: 768px) {\n  .form-inline .form-group {\n    display: inline-block;\n    margin-bottom: 0;\n    vertical-align: middle;\n  }\n  .form-inline .form-control {\n    display: inline-block;\n    width: auto;\n    vertical-align: middle;\n  }\n  .form-inline .form-control-static {\n    display: inline-block;\n  }\n  .form-inline .input-group {\n    display: inline-table;\n    vertical-align: middle;\n  }\n  .form-inline .input-group .input-group-addon,\n  .form-inline .input-group .input-group-btn,\n  .form-inline .input-group .form-control {\n    width: auto;\n  }\n  .form-inline .input-group > .form-control {\n    width: 100%;\n  }\n  .form-inline .control-label {\n    margin-bottom: 0;\n    vertical-align: middle;\n  }\n  .form-inline .radio,\n  .form-inline .checkbox {\n    display: inline-block;\n    margin-top: 0;\n    margin-bottom: 0;\n    vertical-align: middle;\n  }\n  .form-inline .radio label,\n  .form-inline .checkbox label {\n    padding-left: 0;\n  }\n  .form-inline .radio input[type=\"radio\"],\n  .form-inline .checkbox input[type=\"checkbox\"] {\n    position: relative;\n    margin-left: 0;\n  }\n  .form-inline .has-feedback .form-control-feedback {\n    top: 0;\n  }\n}\n.form-horizontal .radio,\n.form-horizontal .checkbox,\n.form-horizontal .radio-inline,\n.form-horizontal .checkbox-inline {\n  margin-top: 0;\n  margin-bottom: 0;\n  padding-top: 7px;\n}\n.form-horizontal .radio,\n.form-horizontal .checkbox {\n  min-height: 34px;\n}\n.form-horizontal .form-group {\n  margin-left: -15px;\n  margin-right: -15px;\n}\n@media (min-width: 768px) {\n  .form-horizontal .control-label {\n    text-align: right;\n    margin-bottom: 0;\n    padding-top: 7px;\n  }\n}\n.form-horizontal .has-feedback .form-control-feedback {\n  right: 15px;\n}\n@media (min-width: 768px) {\n  .form-horizontal .form-group-lg .control-label {\n    padding-top: 11px;\n    font-size: 20px;\n  }\n}\n@media (min-width: 768px) {\n  .form-horizontal .form-group-sm .control-label {\n    padding-top: 6px;\n    font-size: 14px;\n  }\n}\n.btn {\n  display: inline-block;\n  margin-bottom: 0;\n  font-weight: 700;\n  text-align: center;\n  vertical-align: middle;\n  touch-action: manipulation;\n  cursor: pointer;\n  background-image: none;\n  border: 1px solid transparent;\n  white-space: nowrap;\n  padding: 6px 12px;\n  font-size: 16px;\n  line-height: 1.7;\n  border-radius: 30px;\n  -webkit-user-select: none;\n  -moz-user-select: none;\n  -ms-user-select: none;\n  user-select: none;\n}\n.btn:focus,\n.btn:active:focus,\n.btn.active:focus,\n.btn.focus,\n.btn:active.focus,\n.btn.active.focus {\n  outline: 5px auto -webkit-focus-ring-color;\n  outline-offset: -2px;\n}\n.btn:hover,\n.btn:focus,\n.btn.focus {\n  color: #1e252f;\n  text-decoration: none;\n}\n.btn:active,\n.btn.active {\n  outline: 0;\n  background-image: none;\n  -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n  box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n}\n.btn.disabled,\n.btn[disabled],\nfieldset[disabled] .btn {\n  cursor: not-allowed;\n  opacity: 0.65;\n  filter: alpha(opacity=65);\n  -webkit-box-shadow: none;\n  box-shadow: none;\n}\na.btn.disabled,\nfieldset[disabled] a.btn {\n  pointer-events: none;\n}\n.btn-default {\n  color: #1e252f;\n  background-color: #90a3bb;\n  border-color: transparent;\n}\n.btn-default:focus,\n.btn-default.focus {\n  color: #1e252f;\n  background-color: #7189a7;\n  border-color: rgba(0, 0, 0, 0);\n}\n.btn-default:hover {\n  color: #1e252f;\n  background-color: #7189a7;\n  border-color: rgba(0, 0, 0, 0);\n}\n.btn-default:active,\n.btn-default.active,\n.open > .dropdown-toggle.btn-default {\n  color: #1e252f;\n  background-color: #7189a7;\n  border-color: rgba(0, 0, 0, 0);\n}\n.btn-default:active:hover,\n.btn-default.active:hover,\n.open > .dropdown-toggle.btn-default:hover,\n.btn-default:active:focus,\n.btn-default.active:focus,\n.open > .dropdown-toggle.btn-default:focus,\n.btn-default:active.focus,\n.btn-default.active.focus,\n.open > .dropdown-toggle.btn-default.focus {\n  color: #1e252f;\n  background-color: #5d7797;\n  border-color: rgba(0, 0, 0, 0);\n}\n.btn-default:active,\n.btn-default.active,\n.open > .dropdown-toggle.btn-default {\n  background-image: none;\n}\n.btn-default.disabled:hover,\n.btn-default[disabled]:hover,\nfieldset[disabled] .btn-default:hover,\n.btn-default.disabled:focus,\n.btn-default[disabled]:focus,\nfieldset[disabled] .btn-default:focus,\n.btn-default.disabled.focus,\n.btn-default[disabled].focus,\nfieldset[disabled] .btn-default.focus {\n  background-color: #90a3bb;\n  border-color: transparent;\n}\n.btn-default .badge {\n  color: #90a3bb;\n  background-color: #1e252f;\n}\n.btn-primary {\n  color: #ffffff;\n  background-color: #2d7ef4;\n  border-color: #156ff3;\n}\n.btn-primary:focus,\n.btn-primary.focus {\n  color: #ffffff;\n  background-color: #0c63e2;\n  border-color: #073981;\n}\n.btn-primary:hover {\n  color: #ffffff;\n  background-color: #0c63e2;\n  border-color: #0a54c0;\n}\n.btn-primary:active,\n.btn-primary.active,\n.open > .dropdown-toggle.btn-primary {\n  color: #ffffff;\n  background-color: #0c63e2;\n  border-color: #0a54c0;\n}\n.btn-primary:active:hover,\n.btn-primary.active:hover,\n.open > .dropdown-toggle.btn-primary:hover,\n.btn-primary:active:focus,\n.btn-primary.active:focus,\n.open > .dropdown-toggle.btn-primary:focus,\n.btn-primary:active.focus,\n.btn-primary.active.focus,\n.open > .dropdown-toggle.btn-primary.focus {\n  color: #ffffff;\n  background-color: #0a54c0;\n  border-color: #073981;\n}\n.btn-primary:active,\n.btn-primary.active,\n.open > .dropdown-toggle.btn-primary {\n  background-image: none;\n}\n.btn-primary.disabled:hover,\n.btn-primary[disabled]:hover,\nfieldset[disabled] .btn-primary:hover,\n.btn-primary.disabled:focus,\n.btn-primary[disabled]:focus,\nfieldset[disabled] .btn-primary:focus,\n.btn-primary.disabled.focus,\n.btn-primary[disabled].focus,\nfieldset[disabled] .btn-primary.focus {\n  background-color: #2d7ef4;\n  border-color: #156ff3;\n}\n.btn-primary .badge {\n  color: #2d7ef4;\n  background-color: #ffffff;\n}\n.btn-success {\n  color: #ffffff;\n  background-color: #24b47e;\n  border-color: #209f6f;\n}\n.btn-success:focus,\n.btn-success.focus {\n  color: #ffffff;\n  background-color: #1c8a60;\n  border-color: #0b3525;\n}\n.btn-success:hover {\n  color: #ffffff;\n  background-color: #1c8a60;\n  border-color: #166c4b;\n}\n.btn-success:active,\n.btn-success.active,\n.open > .dropdown-toggle.btn-success {\n  color: #ffffff;\n  background-color: #1c8a60;\n  border-color: #166c4b;\n}\n.btn-success:active:hover,\n.btn-success.active:hover,\n.open > .dropdown-toggle.btn-success:hover,\n.btn-success:active:focus,\n.btn-success.active:focus,\n.open > .dropdown-toggle.btn-success:focus,\n.btn-success:active.focus,\n.btn-success.active.focus,\n.open > .dropdown-toggle.btn-success.focus {\n  color: #ffffff;\n  background-color: #166c4b;\n  border-color: #0b3525;\n}\n.btn-success:active,\n.btn-success.active,\n.open > .dropdown-toggle.btn-success {\n  background-image: none;\n}\n.btn-success.disabled:hover,\n.btn-success[disabled]:hover,\nfieldset[disabled] .btn-success:hover,\n.btn-success.disabled:focus,\n.btn-success[disabled]:focus,\nfieldset[disabled] .btn-success:focus,\n.btn-success.disabled.focus,\n.btn-success[disabled].focus,\nfieldset[disabled] .btn-success.focus {\n  background-color: #24b47e;\n  border-color: #209f6f;\n}\n.btn-success .badge {\n  color: #24b47e;\n  background-color: #ffffff;\n}\n.btn-info {\n  color: #ffffff;\n  background-color: #06a2ff;\n  border-color: #0094eb;\n}\n.btn-info:focus,\n.btn-info.focus {\n  color: #ffffff;\n  background-color: #0084d2;\n  border-color: #00446c;\n}\n.btn-info:hover {\n  color: #ffffff;\n  background-color: #0084d2;\n  border-color: #006dae;\n}\n.btn-info:active,\n.btn-info.active,\n.open > .dropdown-toggle.btn-info {\n  color: #ffffff;\n  background-color: #0084d2;\n  border-color: #006dae;\n}\n.btn-info:active:hover,\n.btn-info.active:hover,\n.open > .dropdown-toggle.btn-info:hover,\n.btn-info:active:focus,\n.btn-info.active:focus,\n.open > .dropdown-toggle.btn-info:focus,\n.btn-info:active.focus,\n.btn-info.active.focus,\n.open > .dropdown-toggle.btn-info.focus {\n  color: #ffffff;\n  background-color: #006dae;\n  border-color: #00446c;\n}\n.btn-info:active,\n.btn-info.active,\n.open > .dropdown-toggle.btn-info {\n  background-image: none;\n}\n.btn-info.disabled:hover,\n.btn-info[disabled]:hover,\nfieldset[disabled] .btn-info:hover,\n.btn-info.disabled:focus,\n.btn-info[disabled]:focus,\nfieldset[disabled] .btn-info:focus,\n.btn-info.disabled.focus,\n.btn-info[disabled].focus,\nfieldset[disabled] .btn-info.focus {\n  background-color: #06a2ff;\n  border-color: #0094eb;\n}\n.btn-info .badge {\n  color: #06a2ff;\n  background-color: #ffffff;\n}\n.btn-warning {\n  color: #ffffff;\n  background-color: #e39f48;\n  border-color: #e09332;\n}\n.btn-warning:focus,\n.btn-warning.focus {\n  color: #ffffff;\n  background-color: #d78721;\n  border-color: #7f5013;\n}\n.btn-warning:hover {\n  color: #ffffff;\n  background-color: #d78721;\n  border-color: #b8741c;\n}\n.btn-warning:active,\n.btn-warning.active,\n.open > .dropdown-toggle.btn-warning {\n  color: #ffffff;\n  background-color: #d78721;\n  border-color: #b8741c;\n}\n.btn-warning:active:hover,\n.btn-warning.active:hover,\n.open > .dropdown-toggle.btn-warning:hover,\n.btn-warning:active:focus,\n.btn-warning.active:focus,\n.open > .dropdown-toggle.btn-warning:focus,\n.btn-warning:active.focus,\n.btn-warning.active.focus,\n.open > .dropdown-toggle.btn-warning.focus {\n  color: #ffffff;\n  background-color: #b8741c;\n  border-color: #7f5013;\n}\n.btn-warning:active,\n.btn-warning.active,\n.open > .dropdown-toggle.btn-warning {\n  background-image: none;\n}\n.btn-warning.disabled:hover,\n.btn-warning[disabled]:hover,\nfieldset[disabled] .btn-warning:hover,\n.btn-warning.disabled:focus,\n.btn-warning[disabled]:focus,\nfieldset[disabled] .btn-warning:focus,\n.btn-warning.disabled.focus,\n.btn-warning[disabled].focus,\nfieldset[disabled] .btn-warning.focus {\n  background-color: #e39f48;\n  border-color: #e09332;\n}\n.btn-warning .badge {\n  color: #e39f48;\n  background-color: #ffffff;\n}\n.btn-danger {\n  color: #ffffff;\n  background-color: #ff2b67;\n  border-color: #ff1255;\n}\n.btn-danger:focus,\n.btn-danger.focus {\n  color: #ffffff;\n  background-color: #f70046;\n  border-color: #910029;\n}\n.btn-danger:hover {\n  color: #ffffff;\n  background-color: #f70046;\n  border-color: #d3003c;\n}\n.btn-danger:active,\n.btn-danger.active,\n.open > .dropdown-toggle.btn-danger {\n  color: #ffffff;\n  background-color: #f70046;\n  border-color: #d3003c;\n}\n.btn-danger:active:hover,\n.btn-danger.active:hover,\n.open > .dropdown-toggle.btn-danger:hover,\n.btn-danger:active:focus,\n.btn-danger.active:focus,\n.open > .dropdown-toggle.btn-danger:focus,\n.btn-danger:active.focus,\n.btn-danger.active.focus,\n.open > .dropdown-toggle.btn-danger.focus {\n  color: #ffffff;\n  background-color: #d3003c;\n  border-color: #910029;\n}\n.btn-danger:active,\n.btn-danger.active,\n.open > .dropdown-toggle.btn-danger {\n  background-image: none;\n}\n.btn-danger.disabled:hover,\n.btn-danger[disabled]:hover,\nfieldset[disabled] .btn-danger:hover,\n.btn-danger.disabled:focus,\n.btn-danger[disabled]:focus,\nfieldset[disabled] .btn-danger:focus,\n.btn-danger.disabled.focus,\n.btn-danger[disabled].focus,\nfieldset[disabled] .btn-danger.focus {\n  background-color: #ff2b67;\n  border-color: #ff1255;\n}\n.btn-danger .badge {\n  color: #ff2b67;\n  background-color: #ffffff;\n}\n.btn-link {\n  color: #2d7ef4;\n  font-weight: normal;\n  border-radius: 0;\n}\n.btn-link,\n.btn-link:active,\n.btn-link.active,\n.btn-link[disabled],\nfieldset[disabled] .btn-link {\n  background-color: transparent;\n  -webkit-box-shadow: none;\n  box-shadow: none;\n}\n.btn-link,\n.btn-link:hover,\n.btn-link:focus,\n.btn-link:active {\n  border-color: transparent;\n}\n.btn-link:hover,\n.btn-link:focus {\n  color: #0b58ca;\n  text-decoration: underline;\n  background-color: transparent;\n}\n.btn-link[disabled]:hover,\nfieldset[disabled] .btn-link:hover,\n.btn-link[disabled]:focus,\nfieldset[disabled] .btn-link:focus {\n  color: #757575;\n  text-decoration: none;\n}\n.btn-lg,\n.btn-group-lg > .btn {\n  padding: 10px 16px;\n  font-size: 20px;\n  line-height: 1.3333333;\n  border-radius: 45px;\n}\n.btn-sm,\n.btn-group-sm > .btn {\n  padding: 5px 10px;\n  font-size: 14px;\n  line-height: 1.5;\n  border-radius: 20px;\n}\n.btn-xs,\n.btn-group-xs > .btn {\n  padding: 1px 5px;\n  font-size: 14px;\n  line-height: 1.5;\n  border-radius: 20px;\n}\n.btn-block {\n  display: block;\n  width: 100%;\n}\n.btn-block + .btn-block {\n  margin-top: 5px;\n}\ninput[type=\"submit\"].btn-block,\ninput[type=\"reset\"].btn-block,\ninput[type=\"button\"].btn-block {\n  width: 100%;\n}\n.fade {\n  opacity: 0;\n  -webkit-transition: opacity 0.15s linear;\n  -o-transition: opacity 0.15s linear;\n  transition: opacity 0.15s linear;\n}\n.fade.in {\n  opacity: 1;\n}\n.collapse {\n  display: none;\n}\n.collapse.in {\n  display: block;\n}\ntr.collapse.in {\n  display: table-row;\n}\ntbody.collapse.in {\n  display: table-row-group;\n}\n.collapsing {\n  position: relative;\n  height: 0;\n  overflow: hidden;\n  -webkit-transition-property: height, visibility;\n  transition-property: height, visibility;\n  -webkit-transition-duration: 0.35s;\n  transition-duration: 0.35s;\n  -webkit-transition-timing-function: ease;\n  transition-timing-function: ease;\n}\n.caret {\n  display: inline-block;\n  width: 0;\n  height: 0;\n  margin-left: 2px;\n  vertical-align: middle;\n  border-top: 4px dashed;\n  border-top: 4px solid \\9;\n  border-right: 4px solid transparent;\n  border-left: 4px solid transparent;\n}\n.dropup,\n.dropdown {\n  position: relative;\n}\n.dropdown-toggle:focus {\n  outline: 0;\n}\n.dropdown-menu {\n  position: absolute;\n  top: 100%;\n  left: 0;\n  z-index: 1000;\n  display: none;\n  float: left;\n  min-width: 160px;\n  padding: 5px 0;\n  margin: 2px 0 0;\n  list-style: none;\n  font-size: 16px;\n  text-align: left;\n  background-color: #1d252f;\n  border: 1px solid #cccccc;\n  border: 1px solid #ff2b67;\n  border-radius: 0;\n  -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);\n  box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);\n  background-clip: padding-box;\n}\n.dropdown-menu.pull-right {\n  right: 0;\n  left: auto;\n}\n.dropdown-menu .divider {\n  height: 1px;\n  margin: 12.5px 0;\n  overflow: hidden;\n  background-color: rgba(255, 255, 255, 0.1);\n}\n.dropdown-menu > li > a {\n  display: block;\n  padding: 3px 20px;\n  clear: both;\n  font-weight: normal;\n  line-height: 1.7;\n  color: #ffffff;\n  white-space: nowrap;\n}\n.dropdown-menu > li > a:hover,\n.dropdown-menu > li > a:focus {\n  text-decoration: none;\n  color: #ffffff;\n  background-color: #06a2ff;\n}\n.dropdown-menu > .active > a,\n.dropdown-menu > .active > a:hover,\n.dropdown-menu > .active > a:focus {\n  color: #ffffff;\n  text-decoration: none;\n  outline: 0;\n  background-color: #2d7ef4;\n}\n.dropdown-menu > .disabled > a,\n.dropdown-menu > .disabled > a:hover,\n.dropdown-menu > .disabled > a:focus {\n  color: rgba(30, 37, 47, 0.6);\n}\n.dropdown-menu > .disabled > a:hover,\n.dropdown-menu > .disabled > a:focus {\n  text-decoration: none;\n  background-color: transparent;\n  background-image: none;\n  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n  cursor: not-allowed;\n}\n.open > .dropdown-menu {\n  display: block;\n}\n.open > a {\n  outline: 0;\n}\n.dropdown-menu-right {\n  left: auto;\n  right: 0;\n}\n.dropdown-menu-left {\n  left: 0;\n  right: auto;\n}\n.dropdown-header {\n  display: block;\n  padding: 3px 20px;\n  font-size: 14px;\n  line-height: 1.7;\n  color: #1e252f;\n  white-space: nowrap;\n}\n.dropdown-backdrop {\n  position: fixed;\n  left: 0;\n  right: 0;\n  bottom: 0;\n  top: 0;\n  z-index: 990;\n}\n.pull-right > .dropdown-menu {\n  right: 0;\n  left: auto;\n}\n.dropup .caret,\n.navbar-fixed-bottom .dropdown .caret {\n  border-top: 0;\n  border-bottom: 4px dashed;\n  border-bottom: 4px solid \\9;\n  content: \"\";\n}\n.dropup .dropdown-menu,\n.navbar-fixed-bottom .dropdown .dropdown-menu {\n  top: auto;\n  bottom: 100%;\n  margin-bottom: 2px;\n}\n@media (min-width: 992px) {\n  .navbar-right .dropdown-menu {\n    left: auto;\n    right: 0;\n  }\n  .navbar-right .dropdown-menu-left {\n    left: 0;\n    right: auto;\n  }\n}\n.btn-group,\n.btn-group-vertical {\n  position: relative;\n  display: inline-block;\n  vertical-align: middle;\n}\n.btn-group > .btn,\n.btn-group-vertical > .btn {\n  position: relative;\n  float: left;\n}\n.btn-group > .btn:hover,\n.btn-group-vertical > .btn:hover,\n.btn-group > .btn:focus,\n.btn-group-vertical > .btn:focus,\n.btn-group > .btn:active,\n.btn-group-vertical > .btn:active,\n.btn-group > .btn.active,\n.btn-group-vertical > .btn.active {\n  z-index: 2;\n}\n.btn-group .btn + .btn,\n.btn-group .btn + .btn-group,\n.btn-group .btn-group + .btn,\n.btn-group .btn-group + .btn-group {\n  margin-left: -1px;\n}\n.btn-toolbar {\n  margin-left: -5px;\n}\n.btn-toolbar .btn,\n.btn-toolbar .btn-group,\n.btn-toolbar .input-group {\n  float: left;\n}\n.btn-toolbar > .btn,\n.btn-toolbar > .btn-group,\n.btn-toolbar > .input-group {\n  margin-left: 5px;\n}\n.btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle) {\n  border-radius: 0;\n}\n.btn-group > .btn:first-child {\n  margin-left: 0;\n}\n.btn-group > .btn:first-child:not(:last-child):not(.dropdown-toggle) {\n  border-bottom-right-radius: 0;\n  border-top-right-radius: 0;\n}\n.btn-group > .btn:last-child:not(:first-child),\n.btn-group > .dropdown-toggle:not(:first-child) {\n  border-bottom-left-radius: 0;\n  border-top-left-radius: 0;\n}\n.btn-group > .btn-group {\n  float: left;\n}\n.btn-group > .btn-group:not(:first-child):not(:last-child) > .btn {\n  border-radius: 0;\n}\n.btn-group > .btn-group:first-child:not(:last-child) > .btn:last-child,\n.btn-group > .btn-group:first-child:not(:last-child) > .dropdown-toggle {\n  border-bottom-right-radius: 0;\n  border-top-right-radius: 0;\n}\n.btn-group > .btn-group:last-child:not(:first-child) > .btn:first-child {\n  border-bottom-left-radius: 0;\n  border-top-left-radius: 0;\n}\n.btn-group .dropdown-toggle:active,\n.btn-group.open .dropdown-toggle {\n  outline: 0;\n}\n.btn-group > .btn + .dropdown-toggle {\n  padding-left: 8px;\n  padding-right: 8px;\n}\n.btn-group > .btn-lg + .dropdown-toggle {\n  padding-left: 12px;\n  padding-right: 12px;\n}\n.btn-group.open .dropdown-toggle {\n  -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n  box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n}\n.btn-group.open .dropdown-toggle.btn-link {\n  -webkit-box-shadow: none;\n  box-shadow: none;\n}\n.btn .caret {\n  margin-left: 0;\n}\n.btn-lg .caret {\n  border-width: 5px 5px 0;\n  border-bottom-width: 0;\n}\n.dropup .btn-lg .caret {\n  border-width: 0 5px 5px;\n}\n.btn-group-vertical > .btn,\n.btn-group-vertical > .btn-group,\n.btn-group-vertical > .btn-group > .btn {\n  display: block;\n  float: none;\n  width: 100%;\n  max-width: 100%;\n}\n.btn-group-vertical > .btn-group > .btn {\n  float: none;\n}\n.btn-group-vertical > .btn + .btn,\n.btn-group-vertical > .btn + .btn-group,\n.btn-group-vertical > .btn-group + .btn,\n.btn-group-vertical > .btn-group + .btn-group {\n  margin-top: -1px;\n  margin-left: 0;\n}\n.btn-group-vertical > .btn:not(:first-child):not(:last-child) {\n  border-radius: 0;\n}\n.btn-group-vertical > .btn:first-child:not(:last-child) {\n  border-top-right-radius: 30px;\n  border-top-left-radius: 30px;\n  border-bottom-right-radius: 0;\n  border-bottom-left-radius: 0;\n}\n.btn-group-vertical > .btn:last-child:not(:first-child) {\n  border-top-right-radius: 0;\n  border-top-left-radius: 0;\n  border-bottom-right-radius: 30px;\n  border-bottom-left-radius: 30px;\n}\n.btn-group-vertical > .btn-group:not(:first-child):not(:last-child) > .btn {\n  border-radius: 0;\n}\n.btn-group-vertical > .btn-group:first-child:not(:last-child) > .btn:last-child,\n.btn-group-vertical > .btn-group:first-child:not(:last-child) > .dropdown-toggle {\n  border-bottom-right-radius: 0;\n  border-bottom-left-radius: 0;\n}\n.btn-group-vertical > .btn-group:last-child:not(:first-child) > .btn:first-child {\n  border-top-right-radius: 0;\n  border-top-left-radius: 0;\n}\n.btn-group-justified {\n  display: table;\n  width: 100%;\n  table-layout: fixed;\n  border-collapse: separate;\n}\n.btn-group-justified > .btn,\n.btn-group-justified > .btn-group {\n  float: none;\n  display: table-cell;\n  width: 1%;\n}\n.btn-group-justified > .btn-group .btn {\n  width: 100%;\n}\n.btn-group-justified > .btn-group .dropdown-menu {\n  left: auto;\n}\n[data-toggle=\"buttons\"] > .btn input[type=\"radio\"],\n[data-toggle=\"buttons\"] > .btn-group > .btn input[type=\"radio\"],\n[data-toggle=\"buttons\"] > .btn input[type=\"checkbox\"],\n[data-toggle=\"buttons\"] > .btn-group > .btn input[type=\"checkbox\"] {\n  position: absolute;\n  clip: rect(0, 0, 0, 0);\n  pointer-events: none;\n}\n.input-group {\n  position: relative;\n  display: table;\n  border-collapse: separate;\n}\n.input-group[class*=\"col-\"] {\n  float: none;\n  padding-left: 0;\n  padding-right: 0;\n}\n.input-group .form-control {\n  position: relative;\n  z-index: 2;\n  float: left;\n  width: 100%;\n  margin-bottom: 0;\n}\n.input-group .form-control:focus {\n  z-index: 3;\n}\n.input-group-lg > .form-control,\n.input-group-lg > .input-group-addon,\n.input-group-lg > .input-group-btn > .btn {\n  height: 49px;\n  padding: 10px 16px;\n  font-size: 20px;\n  line-height: 1.3333333;\n  border-radius: 0;\n}\nselect.input-group-lg > .form-control,\nselect.input-group-lg > .input-group-addon,\nselect.input-group-lg > .input-group-btn > .btn {\n  height: 49px;\n  line-height: 49px;\n}\ntextarea.input-group-lg > .form-control,\ntextarea.input-group-lg > .input-group-addon,\ntextarea.input-group-lg > .input-group-btn > .btn,\nselect[multiple].input-group-lg > .form-control,\nselect[multiple].input-group-lg > .input-group-addon,\nselect[multiple].input-group-lg > .input-group-btn > .btn {\n  height: auto;\n}\n.input-group-sm > .form-control,\n.input-group-sm > .input-group-addon,\n.input-group-sm > .input-group-btn > .btn {\n  height: 33px;\n  padding: 5px 10px;\n  font-size: 14px;\n  line-height: 1.5;\n  border-radius: 0;\n}\nselect.input-group-sm > .form-control,\nselect.input-group-sm > .input-group-addon,\nselect.input-group-sm > .input-group-btn > .btn {\n  height: 33px;\n  line-height: 33px;\n}\ntextarea.input-group-sm > .form-control,\ntextarea.input-group-sm > .input-group-addon,\ntextarea.input-group-sm > .input-group-btn > .btn,\nselect[multiple].input-group-sm > .form-control,\nselect[multiple].input-group-sm > .input-group-addon,\nselect[multiple].input-group-sm > .input-group-btn > .btn {\n  height: auto;\n}\n.input-group-addon,\n.input-group-btn,\n.input-group .form-control {\n  display: table-cell;\n}\n.input-group-addon:not(:first-child):not(:last-child),\n.input-group-btn:not(:first-child):not(:last-child),\n.input-group .form-control:not(:first-child):not(:last-child) {\n  border-radius: 0;\n}\n.input-group-addon,\n.input-group-btn {\n  width: 1%;\n  white-space: nowrap;\n  vertical-align: middle;\n}\n.input-group-addon {\n  padding: 6px 12px;\n  font-size: 16px;\n  font-weight: normal;\n  line-height: 1;\n  color: #1e252f;\n  text-align: center;\n  background-color: #44576e;\n  border: 1px solid #194c5f;\n  border-radius: 0;\n}\n.input-group-addon.input-sm {\n  padding: 5px 10px;\n  font-size: 14px;\n  border-radius: 0;\n}\n.input-group-addon.input-lg {\n  padding: 10px 16px;\n  font-size: 20px;\n  border-radius: 0;\n}\n.input-group-addon input[type=\"radio\"],\n.input-group-addon input[type=\"checkbox\"] {\n  margin-top: 0;\n}\n.input-group .form-control:first-child,\n.input-group-addon:first-child,\n.input-group-btn:first-child > .btn,\n.input-group-btn:first-child > .btn-group > .btn,\n.input-group-btn:first-child > .dropdown-toggle,\n.input-group-btn:last-child > .btn:not(:last-child):not(.dropdown-toggle),\n.input-group-btn:last-child > .btn-group:not(:last-child) > .btn {\n  border-bottom-right-radius: 0;\n  border-top-right-radius: 0;\n}\n.input-group-addon:first-child {\n  border-right: 0;\n}\n.input-group .form-control:last-child,\n.input-group-addon:last-child,\n.input-group-btn:last-child > .btn,\n.input-group-btn:last-child > .btn-group > .btn,\n.input-group-btn:last-child > .dropdown-toggle,\n.input-group-btn:first-child > .btn:not(:first-child),\n.input-group-btn:first-child > .btn-group:not(:first-child) > .btn {\n  border-bottom-left-radius: 0;\n  border-top-left-radius: 0;\n}\n.input-group-addon:last-child {\n  border-left: 0;\n}\n.input-group-btn {\n  position: relative;\n  font-size: 0;\n  white-space: nowrap;\n}\n.input-group-btn > .btn {\n  position: relative;\n}\n.input-group-btn > .btn + .btn {\n  margin-left: -1px;\n}\n.input-group-btn > .btn:hover,\n.input-group-btn > .btn:focus,\n.input-group-btn > .btn:active {\n  z-index: 2;\n}\n.input-group-btn:first-child > .btn,\n.input-group-btn:first-child > .btn-group {\n  margin-right: -1px;\n}\n.input-group-btn:last-child > .btn,\n.input-group-btn:last-child > .btn-group {\n  z-index: 2;\n  margin-left: -1px;\n}\n.nav {\n  margin-bottom: 0;\n  padding-left: 0;\n  list-style: none;\n}\n.nav > li {\n  position: relative;\n  display: block;\n}\n.nav > li > a {\n  position: relative;\n  display: block;\n  padding: 10px 15px;\n}\n.nav > li > a:hover,\n.nav > li > a:focus {\n  text-decoration: none;\n  background-color: rgba(255, 255, 255, 0.06);\n}\n.nav > li.disabled > a {\n  color: rgba(30, 37, 47, 0.6);\n}\n.nav > li.disabled > a:hover,\n.nav > li.disabled > a:focus {\n  color: rgba(30, 37, 47, 0.6);\n  text-decoration: none;\n  background-color: transparent;\n  cursor: not-allowed;\n}\n.nav .open > a,\n.nav .open > a:hover,\n.nav .open > a:focus {\n  background-color: rgba(255, 255, 255, 0.06);\n  border-color: #2d7ef4;\n}\n.nav .nav-divider {\n  height: 1px;\n  margin: 12.5px 0;\n  overflow: hidden;\n  background-color: #e5e5e5;\n}\n.nav > li > a > img {\n  max-width: none;\n}\n.nav-tabs {\n  border-bottom: 1px solid rgba(255, 255, 255, 0.1);\n}\n.nav-tabs > li {\n  float: left;\n  margin-bottom: -1px;\n}\n.nav-tabs > li > a {\n  margin-right: 2px;\n  line-height: 1.7;\n  border: 1px solid transparent;\n  border-radius: 0 0 0 0;\n}\n.nav-tabs > li > a:hover {\n  border-color: rgba(255, 255, 255, 0.1) rgba(255, 255, 255, 0.1) rgba(255, 255, 255, 0.1);\n}\n.nav-tabs > li.active > a,\n.nav-tabs > li.active > a:hover,\n.nav-tabs > li.active > a:focus {\n  color: #1e252f;\n  background-color: #1d252f;\n  border: 1px solid rgba(255, 255, 255, 0.1);\n  border-bottom-color: transparent;\n  cursor: default;\n}\n.nav-tabs.nav-justified {\n  width: 100%;\n  border-bottom: 0;\n}\n.nav-tabs.nav-justified > li {\n  float: none;\n}\n.nav-tabs.nav-justified > li > a {\n  text-align: center;\n  margin-bottom: 5px;\n}\n.nav-tabs.nav-justified > .dropdown .dropdown-menu {\n  top: auto;\n  left: auto;\n}\n@media (min-width: 768px) {\n  .nav-tabs.nav-justified > li {\n    display: table-cell;\n    width: 1%;\n  }\n  .nav-tabs.nav-justified > li > a {\n    margin-bottom: 0;\n  }\n}\n.nav-tabs.nav-justified > li > a {\n  margin-right: 0;\n  border-radius: 0;\n}\n.nav-tabs.nav-justified > .active > a,\n.nav-tabs.nav-justified > .active > a:hover,\n.nav-tabs.nav-justified > .active > a:focus {\n  border: 1px solid #dddddd;\n}\n@media (min-width: 768px) {\n  .nav-tabs.nav-justified > li > a {\n    border-bottom: 1px solid #dddddd;\n    border-radius: 0 0 0 0;\n  }\n  .nav-tabs.nav-justified > .active > a,\n  .nav-tabs.nav-justified > .active > a:hover,\n  .nav-tabs.nav-justified > .active > a:focus {\n    border-bottom-color: #1d252f;\n  }\n}\n.nav-pills > li {\n  float: left;\n}\n.nav-pills > li > a {\n  border-radius: 0;\n}\n.nav-pills > li + li {\n  margin-left: 2px;\n}\n.nav-pills > li.active > a,\n.nav-pills > li.active > a:hover,\n.nav-pills > li.active > a:focus {\n  color: #ffffff;\n  background-color: #2d7ef4;\n}\n.nav-stacked > li {\n  float: none;\n}\n.nav-stacked > li + li {\n  margin-top: 2px;\n  margin-left: 0;\n}\n.nav-justified {\n  width: 100%;\n}\n.nav-justified > li {\n  float: none;\n}\n.nav-justified > li > a {\n  text-align: center;\n  margin-bottom: 5px;\n}\n.nav-justified > .dropdown .dropdown-menu {\n  top: auto;\n  left: auto;\n}\n@media (min-width: 768px) {\n  .nav-justified > li {\n    display: table-cell;\n    width: 1%;\n  }\n  .nav-justified > li > a {\n    margin-bottom: 0;\n  }\n}\n.nav-tabs-justified {\n  border-bottom: 0;\n}\n.nav-tabs-justified > li > a {\n  margin-right: 0;\n  border-radius: 0;\n}\n.nav-tabs-justified > .active > a,\n.nav-tabs-justified > .active > a:hover,\n.nav-tabs-justified > .active > a:focus {\n  border: 1px solid #dddddd;\n}\n@media (min-width: 768px) {\n  .nav-tabs-justified > li > a {\n    border-bottom: 1px solid #dddddd;\n    border-radius: 0 0 0 0;\n  }\n  .nav-tabs-justified > .active > a,\n  .nav-tabs-justified > .active > a:hover,\n  .nav-tabs-justified > .active > a:focus {\n    border-bottom-color: #1d252f;\n  }\n}\n.tab-content > .tab-pane {\n  display: none;\n}\n.tab-content > .active {\n  display: block;\n}\n.nav-tabs .dropdown-menu {\n  margin-top: -1px;\n  border-top-right-radius: 0;\n  border-top-left-radius: 0;\n}\n.navbar {\n  position: relative;\n  min-height: 80px;\n  margin-bottom: 0;\n  border: 1px solid transparent;\n}\n@media (min-width: 992px) {\n  .navbar {\n    border-radius: 0;\n  }\n}\n@media (min-width: 992px) {\n  .navbar-header {\n    float: left;\n  }\n}\n.navbar-collapse {\n  overflow-x: visible;\n  padding-right: 15px;\n  padding-left: 15px;\n  border-top: 1px solid transparent;\n  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1);\n  -webkit-overflow-scrolling: touch;\n}\n.navbar-collapse.in {\n  overflow-y: auto;\n}\n@media (min-width: 992px) {\n  .navbar-collapse {\n    width: auto;\n    border-top: 0;\n    box-shadow: none;\n  }\n  .navbar-collapse.collapse {\n    display: block !important;\n    height: auto !important;\n    padding-bottom: 0;\n    overflow: visible !important;\n  }\n  .navbar-collapse.in {\n    overflow-y: visible;\n  }\n  .navbar-fixed-top .navbar-collapse,\n  .navbar-static-top .navbar-collapse,\n  .navbar-fixed-bottom .navbar-collapse {\n    padding-left: 0;\n    padding-right: 0;\n  }\n}\n.navbar-fixed-top .navbar-collapse,\n.navbar-fixed-bottom .navbar-collapse {\n  max-height: 340px;\n}\n@media (max-device-width: 480px) and (orientation: landscape) {\n  .navbar-fixed-top .navbar-collapse,\n  .navbar-fixed-bottom .navbar-collapse {\n    max-height: 200px;\n  }\n}\n.container > .navbar-header,\n.container-fluid > .navbar-header,\n.container > .navbar-collapse,\n.container-fluid > .navbar-collapse {\n  margin-right: -15px;\n  margin-left: -15px;\n}\n@media (min-width: 992px) {\n  .container > .navbar-header,\n  .container-fluid > .navbar-header,\n  .container > .navbar-collapse,\n  .container-fluid > .navbar-collapse {\n    margin-right: 0;\n    margin-left: 0;\n  }\n}\n.navbar-static-top {\n  z-index: 1000;\n  border-width: 0 0 1px;\n}\n@media (min-width: 992px) {\n  .navbar-static-top {\n    border-radius: 0;\n  }\n}\n.navbar-fixed-top,\n.navbar-fixed-bottom {\n  position: fixed;\n  right: 0;\n  left: 0;\n  z-index: 1030;\n}\n@media (min-width: 992px) {\n  .navbar-fixed-top,\n  .navbar-fixed-bottom {\n    border-radius: 0;\n  }\n}\n.navbar-fixed-top {\n  top: 0;\n  border-width: 0 0 1px;\n}\n.navbar-fixed-bottom {\n  bottom: 0;\n  margin-bottom: 0;\n  border-width: 1px 0 0;\n}\n.navbar-brand {\n  float: left;\n  padding: 26.5px 15px;\n  font-size: 20px;\n  line-height: 27px;\n  height: 80px;\n}\n.navbar-brand:hover,\n.navbar-brand:focus {\n  text-decoration: none;\n}\n.navbar-brand > img {\n  display: block;\n}\n@media (min-width: 992px) {\n  .navbar > .container .navbar-brand,\n  .navbar > .container-fluid .navbar-brand {\n    margin-left: -15px;\n  }\n}\n.navbar-toggle {\n  position: relative;\n  float: right;\n  margin-right: 15px;\n  padding: 9px 10px;\n  margin-top: 23px;\n  margin-bottom: 23px;\n  background-color: transparent;\n  background-image: none;\n  border: 1px solid transparent;\n  border-radius: 0;\n}\n.navbar-toggle:focus {\n  outline: 0;\n}\n.navbar-toggle .icon-bar {\n  display: block;\n  width: 22px;\n  height: 2px;\n  border-radius: 1px;\n}\n.navbar-toggle .icon-bar + .icon-bar {\n  margin-top: 4px;\n}\n@media (min-width: 992px) {\n  .navbar-toggle {\n    display: none;\n  }\n}\n.navbar-nav {\n  margin: 13.25px -15px;\n}\n.navbar-nav > li > a {\n  padding-top: 10px;\n  padding-bottom: 10px;\n  line-height: 27px;\n}\n@media (max-width: 991px) {\n  .navbar-nav .open .dropdown-menu {\n    position: static;\n    float: none;\n    width: auto;\n    margin-top: 0;\n    background-color: transparent;\n    border: 0;\n    box-shadow: none;\n  }\n  .navbar-nav .open .dropdown-menu > li > a,\n  .navbar-nav .open .dropdown-menu .dropdown-header {\n    padding: 5px 15px 5px 25px;\n  }\n  .navbar-nav .open .dropdown-menu > li > a {\n    line-height: 27px;\n  }\n  .navbar-nav .open .dropdown-menu > li > a:hover,\n  .navbar-nav .open .dropdown-menu > li > a:focus {\n    background-image: none;\n  }\n}\n@media (min-width: 992px) {\n  .navbar-nav {\n    float: left;\n    margin: 0;\n  }\n  .navbar-nav > li {\n    float: left;\n  }\n  .navbar-nav > li > a {\n    padding-top: 26.5px;\n    padding-bottom: 26.5px;\n  }\n}\n.navbar-form {\n  margin-left: -15px;\n  margin-right: -15px;\n  padding: 10px 15px;\n  border-top: 1px solid transparent;\n  border-bottom: 1px solid transparent;\n  -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1);\n  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1);\n  margin-top: 19.5px;\n  margin-bottom: 19.5px;\n}\n@media (min-width: 768px) {\n  .navbar-form .form-group {\n    display: inline-block;\n    margin-bottom: 0;\n    vertical-align: middle;\n  }\n  .navbar-form .form-control {\n    display: inline-block;\n    width: auto;\n    vertical-align: middle;\n  }\n  .navbar-form .form-control-static {\n    display: inline-block;\n  }\n  .navbar-form .input-group {\n    display: inline-table;\n    vertical-align: middle;\n  }\n  .navbar-form .input-group .input-group-addon,\n  .navbar-form .input-group .input-group-btn,\n  .navbar-form .input-group .form-control {\n    width: auto;\n  }\n  .navbar-form .input-group > .form-control {\n    width: 100%;\n  }\n  .navbar-form .control-label {\n    margin-bottom: 0;\n    vertical-align: middle;\n  }\n  .navbar-form .radio,\n  .navbar-form .checkbox {\n    display: inline-block;\n    margin-top: 0;\n    margin-bottom: 0;\n    vertical-align: middle;\n  }\n  .navbar-form .radio label,\n  .navbar-form .checkbox label {\n    padding-left: 0;\n  }\n  .navbar-form .radio input[type=\"radio\"],\n  .navbar-form .checkbox input[type=\"checkbox\"] {\n    position: relative;\n    margin-left: 0;\n  }\n  .navbar-form .has-feedback .form-control-feedback {\n    top: 0;\n  }\n}\n@media (max-width: 991px) {\n  .navbar-form .form-group {\n    margin-bottom: 5px;\n  }\n  .navbar-form .form-group:last-child {\n    margin-bottom: 0;\n  }\n}\n@media (min-width: 992px) {\n  .navbar-form {\n    width: auto;\n    border: 0;\n    margin-left: 0;\n    margin-right: 0;\n    padding-top: 0;\n    padding-bottom: 0;\n    -webkit-box-shadow: none;\n    box-shadow: none;\n  }\n}\n.navbar-nav > li > .dropdown-menu {\n  margin-top: 0;\n  border-top-right-radius: 0;\n  border-top-left-radius: 0;\n}\n.navbar-fixed-bottom .navbar-nav > li > .dropdown-menu {\n  margin-bottom: 0;\n  border-top-right-radius: 0;\n  border-top-left-radius: 0;\n  border-bottom-right-radius: 0;\n  border-bottom-left-radius: 0;\n}\n.navbar-btn {\n  margin-top: 19.5px;\n  margin-bottom: 19.5px;\n}\n.navbar-btn.btn-sm {\n  margin-top: 23.5px;\n  margin-bottom: 23.5px;\n}\n.navbar-btn.btn-xs {\n  margin-top: 29px;\n  margin-bottom: 29px;\n}\n.navbar-text {\n  margin-top: 26.5px;\n  margin-bottom: 26.5px;\n}\n@media (min-width: 992px) {\n  .navbar-text {\n    float: left;\n    margin-left: 15px;\n    margin-right: 15px;\n  }\n}\n@media (min-width: 992px) {\n  .navbar-left {\n    float: left !important;\n  }\n  .navbar-right {\n    float: right !important;\n    margin-right: -15px;\n  }\n  .navbar-right ~ .navbar-right {\n    margin-right: 0;\n  }\n}\n.navbar-default {\n  background-color: transparent;\n  border-color: transparent;\n}\n.navbar-default .navbar-brand {\n  color: #ffffff;\n}\n.navbar-default .navbar-brand:hover,\n.navbar-default .navbar-brand:focus {\n  color: rgba(255, 255, 255, 0.7);\n  background-color: transparent;\n}\n.navbar-default .navbar-text {\n  color: #ffffff;\n}\n.navbar-default .navbar-nav > li > a {\n  color: #ffffff;\n}\n.navbar-default .navbar-nav > li > a:hover,\n.navbar-default .navbar-nav > li > a:focus {\n  color: rgba(255, 255, 255, 0.7);\n  background-color: transparent;\n}\n.navbar-default .navbar-nav > .active > a,\n.navbar-default .navbar-nav > .active > a:hover,\n.navbar-default .navbar-nav > .active > a:focus {\n  color: rgba(255, 255, 255, 0.7);\n  background-color: transparent;\n}\n.navbar-default .navbar-nav > .disabled > a,\n.navbar-default .navbar-nav > .disabled > a:hover,\n.navbar-default .navbar-nav > .disabled > a:focus {\n  color: rgba(255, 255, 255, 0.5);\n  background-color: transparent;\n}\n.navbar-default .navbar-toggle {\n  border-color: transparent;\n}\n.navbar-default .navbar-toggle:hover,\n.navbar-default .navbar-toggle:focus {\n  background-color: rgba(255, 255, 255, 0.14);\n}\n.navbar-default .navbar-toggle .icon-bar {\n  background-color: #ffffff;\n}\n.navbar-default .navbar-collapse,\n.navbar-default .navbar-form {\n  border-color: transparent;\n}\n.navbar-default .navbar-nav > .open > a,\n.navbar-default .navbar-nav > .open > a:hover,\n.navbar-default .navbar-nav > .open > a:focus {\n  background-color: transparent;\n  color: rgba(255, 255, 255, 0.7);\n}\n@media (max-width: 991px) {\n  .navbar-default .navbar-nav .open .dropdown-menu > li > a {\n    color: #ffffff;\n  }\n  .navbar-default .navbar-nav .open .dropdown-menu > li > a:hover,\n  .navbar-default .navbar-nav .open .dropdown-menu > li > a:focus {\n    color: rgba(255, 255, 255, 0.7);\n    background-color: transparent;\n  }\n  .navbar-default .navbar-nav .open .dropdown-menu > .active > a,\n  .navbar-default .navbar-nav .open .dropdown-menu > .active > a:hover,\n  .navbar-default .navbar-nav .open .dropdown-menu > .active > a:focus {\n    color: rgba(255, 255, 255, 0.7);\n    background-color: transparent;\n  }\n  .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a,\n  .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:hover,\n  .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:focus {\n    color: rgba(255, 255, 255, 0.5);\n    background-color: transparent;\n  }\n}\n.navbar-default .navbar-link {\n  color: #ffffff;\n}\n.navbar-default .navbar-link:hover {\n  color: rgba(255, 255, 255, 0.7);\n}\n.navbar-default .btn-link {\n  color: #ffffff;\n}\n.navbar-default .btn-link:hover,\n.navbar-default .btn-link:focus {\n  color: rgba(255, 255, 255, 0.7);\n}\n.navbar-default .btn-link[disabled]:hover,\nfieldset[disabled] .navbar-default .btn-link:hover,\n.navbar-default .btn-link[disabled]:focus,\nfieldset[disabled] .navbar-default .btn-link:focus {\n  color: rgba(255, 255, 255, 0.5);\n}\n.navbar-inverse {\n  background-color: #222222;\n  border-color: #080808;\n}\n.navbar-inverse .navbar-brand {\n  color: #9c9c9c;\n}\n.navbar-inverse .navbar-brand:hover,\n.navbar-inverse .navbar-brand:focus {\n  color: #ffffff;\n  background-color: transparent;\n}\n.navbar-inverse .navbar-text {\n  color: #9c9c9c;\n}\n.navbar-inverse .navbar-nav > li > a {\n  color: #9c9c9c;\n}\n.navbar-inverse .navbar-nav > li > a:hover,\n.navbar-inverse .navbar-nav > li > a:focus {\n  color: #ffffff;\n  background-color: transparent;\n}\n.navbar-inverse .navbar-nav > .active > a,\n.navbar-inverse .navbar-nav > .active > a:hover,\n.navbar-inverse .navbar-nav > .active > a:focus {\n  color: #ffffff;\n  background-color: #080808;\n}\n.navbar-inverse .navbar-nav > .disabled > a,\n.navbar-inverse .navbar-nav > .disabled > a:hover,\n.navbar-inverse .navbar-nav > .disabled > a:focus {\n  color: #444444;\n  background-color: transparent;\n}\n.navbar-inverse .navbar-toggle {\n  border-color: #333333;\n}\n.navbar-inverse .navbar-toggle:hover,\n.navbar-inverse .navbar-toggle:focus {\n  background-color: #333333;\n}\n.navbar-inverse .navbar-toggle .icon-bar {\n  background-color: #ffffff;\n}\n.navbar-inverse .navbar-collapse,\n.navbar-inverse .navbar-form {\n  border-color: #101010;\n}\n.navbar-inverse .navbar-nav > .open > a,\n.navbar-inverse .navbar-nav > .open > a:hover,\n.navbar-inverse .navbar-nav > .open > a:focus {\n  background-color: #080808;\n  color: #ffffff;\n}\n@media (max-width: 991px) {\n  .navbar-inverse .navbar-nav .open .dropdown-menu > .dropdown-header {\n    border-color: #080808;\n  }\n  .navbar-inverse .navbar-nav .open .dropdown-menu .divider {\n    background-color: #080808;\n  }\n  .navbar-inverse .navbar-nav .open .dropdown-menu > li > a {\n    color: #9c9c9c;\n  }\n  .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:hover,\n  .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:focus {\n    color: #ffffff;\n    background-color: transparent;\n  }\n  .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a,\n  .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:hover,\n  .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:focus {\n    color: #ffffff;\n    background-color: #080808;\n  }\n  .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a,\n  .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:hover,\n  .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:focus {\n    color: #444444;\n    background-color: transparent;\n  }\n}\n.navbar-inverse .navbar-link {\n  color: #9c9c9c;\n}\n.navbar-inverse .navbar-link:hover {\n  color: #ffffff;\n}\n.navbar-inverse .btn-link {\n  color: #9c9c9c;\n}\n.navbar-inverse .btn-link:hover,\n.navbar-inverse .btn-link:focus {\n  color: #ffffff;\n}\n.navbar-inverse .btn-link[disabled]:hover,\nfieldset[disabled] .navbar-inverse .btn-link:hover,\n.navbar-inverse .btn-link[disabled]:focus,\nfieldset[disabled] .navbar-inverse .btn-link:focus {\n  color: #444444;\n}\n.breadcrumb {\n  padding: 8px 15px;\n  margin-bottom: 27px;\n  list-style: none;\n  background-color: #27313f;\n  border-radius: 0;\n}\n.breadcrumb > li {\n  display: inline-block;\n}\n.breadcrumb > li + li:before {\n  content: \"/\\00a0\";\n  padding: 0 5px;\n  color: rgba(30, 37, 47, 0.6);\n}\n.breadcrumb > .active {\n  color: rgba(30, 37, 47, 0.6);\n}\n.pagination {\n  display: inline-block;\n  padding-left: 0;\n  margin: 27px 0;\n  border-radius: 0;\n}\n.pagination > li {\n  display: inline;\n}\n.pagination > li > a,\n.pagination > li > span {\n  position: relative;\n  float: left;\n  padding: 6px 12px;\n  line-height: 1.7;\n  text-decoration: none;\n  color: #2d7ef4;\n  background-color: #ffffff;\n  border: 1px solid #dddddd;\n  margin-left: -1px;\n}\n.pagination > li:first-child > a,\n.pagination > li:first-child > span {\n  margin-left: 0;\n  border-bottom-left-radius: 0;\n  border-top-left-radius: 0;\n}\n.pagination > li:last-child > a,\n.pagination > li:last-child > span {\n  border-bottom-right-radius: 0;\n  border-top-right-radius: 0;\n}\n.pagination > li > a:hover,\n.pagination > li > span:hover,\n.pagination > li > a:focus,\n.pagination > li > span:focus {\n  z-index: 2;\n  color: #0b58ca;\n  background-color: #e6e6e6;\n  border-color: #dddddd;\n}\n.pagination > .active > a,\n.pagination > .active > span,\n.pagination > .active > a:hover,\n.pagination > .active > span:hover,\n.pagination > .active > a:focus,\n.pagination > .active > span:focus {\n  z-index: 3;\n  color: #ffffff;\n  background-color: #2d7ef4;\n  border-color: #2d7ef4;\n  cursor: default;\n}\n.pagination > .disabled > span,\n.pagination > .disabled > span:hover,\n.pagination > .disabled > span:focus,\n.pagination > .disabled > a,\n.pagination > .disabled > a:hover,\n.pagination > .disabled > a:focus {\n  color: #757575;\n  background-color: #ffffff;\n  border-color: #dddddd;\n  cursor: not-allowed;\n}\n.pagination-lg > li > a,\n.pagination-lg > li > span {\n  padding: 10px 16px;\n  font-size: 20px;\n  line-height: 1.3333333;\n}\n.pagination-lg > li:first-child > a,\n.pagination-lg > li:first-child > span {\n  border-bottom-left-radius: 0;\n  border-top-left-radius: 0;\n}\n.pagination-lg > li:last-child > a,\n.pagination-lg > li:last-child > span {\n  border-bottom-right-radius: 0;\n  border-top-right-radius: 0;\n}\n.pagination-sm > li > a,\n.pagination-sm > li > span {\n  padding: 5px 10px;\n  font-size: 14px;\n  line-height: 1.5;\n}\n.pagination-sm > li:first-child > a,\n.pagination-sm > li:first-child > span {\n  border-bottom-left-radius: 0;\n  border-top-left-radius: 0;\n}\n.pagination-sm > li:last-child > a,\n.pagination-sm > li:last-child > span {\n  border-bottom-right-radius: 0;\n  border-top-right-radius: 0;\n}\n.pager {\n  padding-left: 0;\n  margin: 27px 0;\n  list-style: none;\n  text-align: center;\n}\n.pager li {\n  display: inline;\n}\n.pager li > a,\n.pager li > span {\n  display: inline-block;\n  padding: 5px 14px;\n  background-color: #ffffff;\n  border: 1px solid #dddddd;\n  border-radius: 15px;\n}\n.pager li > a:hover,\n.pager li > a:focus {\n  text-decoration: none;\n  background-color: #e6e6e6;\n}\n.pager .next > a,\n.pager .next > span {\n  float: right;\n}\n.pager .previous > a,\n.pager .previous > span {\n  float: left;\n}\n.pager .disabled > a,\n.pager .disabled > a:hover,\n.pager .disabled > a:focus,\n.pager .disabled > span {\n  color: #757575;\n  background-color: #ffffff;\n  cursor: not-allowed;\n}\n.label {\n  display: inline;\n  padding: .2em .6em .3em;\n  font-size: 75%;\n  font-weight: bold;\n  line-height: 1;\n  color: #ffffff;\n  text-align: center;\n  white-space: nowrap;\n  vertical-align: baseline;\n  border-radius: .25em;\n}\na.label:hover,\na.label:focus {\n  color: #ffffff;\n  text-decoration: none;\n  cursor: pointer;\n}\n.label:empty {\n  display: none;\n}\n.btn .label {\n  position: relative;\n  top: -1px;\n}\n.label-default {\n  background-color: #90a3bb;\n}\n.label-default[href]:hover,\n.label-default[href]:focus {\n  background-color: #7189a7;\n}\n.label-primary {\n  background-color: #2d7ef4;\n}\n.label-primary[href]:hover,\n.label-primary[href]:focus {\n  background-color: #0c63e2;\n}\n.label-success {\n  background-color: #24b47e;\n}\n.label-success[href]:hover,\n.label-success[href]:focus {\n  background-color: #1c8a60;\n}\n.label-info {\n  background-color: #06a2ff;\n}\n.label-info[href]:hover,\n.label-info[href]:focus {\n  background-color: #0084d2;\n}\n.label-warning {\n  background-color: #e39f48;\n}\n.label-warning[href]:hover,\n.label-warning[href]:focus {\n  background-color: #d78721;\n}\n.label-danger {\n  background-color: #ff2b67;\n}\n.label-danger[href]:hover,\n.label-danger[href]:focus {\n  background-color: #f70046;\n}\n.badge {\n  display: inline-block;\n  min-width: 10px;\n  padding: 3px 7px;\n  font-size: 14px;\n  font-weight: bold;\n  color: #ffffff;\n  line-height: 1;\n  vertical-align: middle;\n  white-space: nowrap;\n  text-align: center;\n  background-color: #757575;\n  border-radius: 10px;\n}\n.badge:empty {\n  display: none;\n}\n.btn .badge {\n  position: relative;\n  top: -1px;\n}\n.btn-xs .badge,\n.btn-group-xs > .btn .badge {\n  top: 0;\n  padding: 1px 5px;\n}\na.badge:hover,\na.badge:focus {\n  color: #ffffff;\n  text-decoration: none;\n  cursor: pointer;\n}\n.list-group-item.active > .badge,\n.nav-pills > .active > a > .badge {\n  color: #2d7ef4;\n  background-color: #ffffff;\n}\n.list-group-item > .badge {\n  float: right;\n}\n.list-group-item > .badge + .badge {\n  margin-right: 5px;\n}\n.nav-pills > li > a > .badge {\n  margin-left: 3px;\n}\n.jumbotron {\n  padding-top: 30px;\n  padding-bottom: 30px;\n  margin-bottom: 30px;\n  color: inherit;\n  background-color: rgba(255, 255, 255, 0.1);\n}\n.jumbotron h1,\n.jumbotron .h1 {\n  color: inherit;\n}\n.jumbotron p {\n  margin-bottom: 15px;\n  font-size: 24px;\n  font-weight: 200;\n}\n.jumbotron > hr {\n  border-top-color: rgba(230, 230, 230, 0.1);\n}\n.container .jumbotron,\n.container-fluid .jumbotron {\n  border-radius: 0;\n  padding-left: 15px;\n  padding-right: 15px;\n}\n.jumbotron .container {\n  max-width: 100%;\n}\n@media screen and (min-width: 768px) {\n  .jumbotron {\n    padding-top: 48px;\n    padding-bottom: 48px;\n  }\n  .container .jumbotron,\n  .container-fluid .jumbotron {\n    padding-left: 60px;\n    padding-right: 60px;\n  }\n  .jumbotron h1,\n  .jumbotron .h1 {\n    font-size: 72px;\n  }\n}\n.thumbnail {\n  display: block;\n  padding: 4px;\n  margin-bottom: 27px;\n  line-height: 1.7;\n  background-color: #1d252f;\n  border: 1px solid #dddddd;\n  border-radius: 0;\n  -webkit-transition: border 0.2s ease-in-out;\n  -o-transition: border 0.2s ease-in-out;\n  transition: border 0.2s ease-in-out;\n}\n.thumbnail > img,\n.thumbnail a > img {\n  margin-left: auto;\n  margin-right: auto;\n}\na.thumbnail:hover,\na.thumbnail:focus,\na.thumbnail.active {\n  border-color: #2d7ef4;\n}\n.thumbnail .caption {\n  padding: 9px;\n  color: #1e252f;\n}\n.alert {\n  padding: 15px;\n  margin-bottom: 27px;\n  border: 1px solid transparent;\n  border-radius: 0;\n}\n.alert h4 {\n  margin-top: 0;\n  color: inherit;\n}\n.alert .alert-link {\n  font-weight: bold;\n}\n.alert > p,\n.alert > ul {\n  margin-bottom: 0;\n}\n.alert > p + p {\n  margin-top: 5px;\n}\n.alert-dismissable,\n.alert-dismissible {\n  padding-right: 35px;\n}\n.alert-dismissable .close,\n.alert-dismissible .close {\n  position: relative;\n  top: -2px;\n  right: -21px;\n  color: inherit;\n}\n.alert-success {\n  background-color: #e3f1e4;\n  border-color: #bddebf;\n  color: #24b47e;\n}\n.alert-success hr {\n  border-top-color: #acd5af;\n}\n.alert-success .alert-link {\n  color: #1c8a60;\n}\n.alert-info {\n  background-color: #e5f3fa;\n  border-color: #b3dbf1;\n  color: #06a2ff;\n}\n.alert-info hr {\n  border-top-color: #9dd1ed;\n}\n.alert-info .alert-link {\n  color: #0084d2;\n}\n.alert-warning {\n  background-color: #fff4cc;\n  border-color: #ffe071;\n  color: #de8e27;\n}\n.alert-warning hr {\n  border-top-color: #ffda58;\n}\n.alert-warning .alert-link {\n  color: #b6721c;\n}\n.alert-danger {\n  background-color: #f9e9e9;\n  border-color: #f1c9c9;\n  color: #ff2b67;\n}\n.alert-danger hr {\n  border-top-color: #ecb5b5;\n}\n.alert-danger .alert-link {\n  color: #f70046;\n}\n@-webkit-keyframes progress-bar-stripes {\n  from {\n    background-position: 40px 0;\n  }\n  to {\n    background-position: 0 0;\n  }\n}\n@keyframes progress-bar-stripes {\n  from {\n    background-position: 40px 0;\n  }\n  to {\n    background-position: 0 0;\n  }\n}\n.progress {\n  overflow: hidden;\n  height: 27px;\n  margin-bottom: 27px;\n  background-color: rgba(255, 255, 255, 0.06);\n  border-radius: 0;\n  -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);\n  box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);\n}\n.progress-bar {\n  float: left;\n  width: 0%;\n  height: 100%;\n  font-size: 14px;\n  line-height: 27px;\n  color: #ffffff;\n  text-align: center;\n  background-color: #2d7ef4;\n  -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);\n  box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);\n  -webkit-transition: width 0.6s ease;\n  -o-transition: width 0.6s ease;\n  transition: width 0.6s ease;\n}\n.progress-striped .progress-bar,\n.progress-bar-striped {\n  background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n  background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n  background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n  background-size: 40px 40px;\n}\n.progress.active .progress-bar,\n.progress-bar.active {\n  -webkit-animation: progress-bar-stripes 2s linear infinite;\n  -o-animation: progress-bar-stripes 2s linear infinite;\n  animation: progress-bar-stripes 2s linear infinite;\n}\n.progress-bar-success {\n  background-color: #24b47e;\n}\n.progress-striped .progress-bar-success {\n  background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n  background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n  background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n}\n.progress-bar-info {\n  background-color: #06a2ff;\n}\n.progress-striped .progress-bar-info {\n  background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n  background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n  background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n}\n.progress-bar-warning {\n  background-color: #e39f48;\n}\n.progress-striped .progress-bar-warning {\n  background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n  background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n  background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n}\n.progress-bar-danger {\n  background-color: #ff2b67;\n}\n.progress-striped .progress-bar-danger {\n  background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n  background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n  background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n}\n.media {\n  margin-top: 15px;\n}\n.media:first-child {\n  margin-top: 0;\n}\n.media,\n.media-body {\n  zoom: 1;\n  overflow: hidden;\n}\n.media-body {\n  width: 10000px;\n}\n.media-object {\n  display: block;\n}\n.media-object.img-thumbnail {\n  max-width: none;\n}\n.media-right,\n.media > .pull-right {\n  padding-left: 10px;\n}\n.media-left,\n.media > .pull-left {\n  padding-right: 10px;\n}\n.media-left,\n.media-right,\n.media-body {\n  display: table-cell;\n  vertical-align: top;\n}\n.media-middle {\n  vertical-align: middle;\n}\n.media-bottom {\n  vertical-align: bottom;\n}\n.media-heading {\n  margin-top: 0;\n  margin-bottom: 5px;\n}\n.media-list {\n  padding-left: 0;\n  list-style: none;\n}\n.list-group {\n  margin-bottom: 20px;\n  padding-left: 0;\n}\n.list-group-item {\n  position: relative;\n  display: block;\n  padding: 10px 15px;\n  margin-bottom: -1px;\n  background-color: transparent;\n  border: 1px solid rgba(255, 255, 255, 0.1);\n}\n.list-group-item:first-child {\n  border-top-right-radius: 0;\n  border-top-left-radius: 0;\n}\n.list-group-item:last-child {\n  margin-bottom: 0;\n  border-bottom-right-radius: 0;\n  border-bottom-left-radius: 0;\n}\na.list-group-item,\nbutton.list-group-item {\n  color: #1e252f;\n}\na.list-group-item .list-group-item-heading,\nbutton.list-group-item .list-group-item-heading {\n  color: #1e252f;\n}\na.list-group-item:hover,\nbutton.list-group-item:hover,\na.list-group-item:focus,\nbutton.list-group-item:focus {\n  text-decoration: none;\n  color: #1e252f;\n  background-color: rgba(255, 255, 255, 0.06);\n}\nbutton.list-group-item {\n  width: 100%;\n  text-align: left;\n}\n.list-group-item.disabled,\n.list-group-item.disabled:hover,\n.list-group-item.disabled:focus {\n  background-color: transparent;\n  color: rgba(255, 255, 255, 0.22);\n  cursor: not-allowed;\n}\n.list-group-item.disabled .list-group-item-heading,\n.list-group-item.disabled:hover .list-group-item-heading,\n.list-group-item.disabled:focus .list-group-item-heading {\n  color: inherit;\n}\n.list-group-item.disabled .list-group-item-text,\n.list-group-item.disabled:hover .list-group-item-text,\n.list-group-item.disabled:focus .list-group-item-text {\n  color: rgba(30, 37, 47, 0.6);\n}\n.list-group-item.active,\n.list-group-item.active:hover,\n.list-group-item.active:focus {\n  z-index: 2;\n  color: #ffffff;\n  background-color: #2d7ef4;\n  border-color: #2d7ef4;\n}\n.list-group-item.active .list-group-item-heading,\n.list-group-item.active:hover .list-group-item-heading,\n.list-group-item.active:focus .list-group-item-heading,\n.list-group-item.active .list-group-item-heading > small,\n.list-group-item.active:hover .list-group-item-heading > small,\n.list-group-item.active:focus .list-group-item-heading > small,\n.list-group-item.active .list-group-item-heading > .small,\n.list-group-item.active:hover .list-group-item-heading > .small,\n.list-group-item.active:focus .list-group-item-heading > .small {\n  color: inherit;\n}\n.list-group-item.active .list-group-item-text,\n.list-group-item.active:hover .list-group-item-text,\n.list-group-item.active:focus .list-group-item-text {\n  color: #eff5fe;\n}\n.list-group-item-success {\n  color: #24b47e;\n  background-color: #e3f1e4;\n}\na.list-group-item-success,\nbutton.list-group-item-success {\n  color: #24b47e;\n}\na.list-group-item-success .list-group-item-heading,\nbutton.list-group-item-success .list-group-item-heading {\n  color: inherit;\n}\na.list-group-item-success:hover,\nbutton.list-group-item-success:hover,\na.list-group-item-success:focus,\nbutton.list-group-item-success:focus {\n  color: #24b47e;\n  background-color: #d2e9d4;\n}\na.list-group-item-success.active,\nbutton.list-group-item-success.active,\na.list-group-item-success.active:hover,\nbutton.list-group-item-success.active:hover,\na.list-group-item-success.active:focus,\nbutton.list-group-item-success.active:focus {\n  color: #fff;\n  background-color: #24b47e;\n  border-color: #24b47e;\n}\n.list-group-item-info {\n  color: #06a2ff;\n  background-color: #e5f3fa;\n}\na.list-group-item-info,\nbutton.list-group-item-info {\n  color: #06a2ff;\n}\na.list-group-item-info .list-group-item-heading,\nbutton.list-group-item-info .list-group-item-heading {\n  color: inherit;\n}\na.list-group-item-info:hover,\nbutton.list-group-item-info:hover,\na.list-group-item-info:focus,\nbutton.list-group-item-info:focus {\n  color: #06a2ff;\n  background-color: #d0e9f6;\n}\na.list-group-item-info.active,\nbutton.list-group-item-info.active,\na.list-group-item-info.active:hover,\nbutton.list-group-item-info.active:hover,\na.list-group-item-info.active:focus,\nbutton.list-group-item-info.active:focus {\n  color: #fff;\n  background-color: #06a2ff;\n  border-color: #06a2ff;\n}\n.list-group-item-warning {\n  color: #de8e27;\n  background-color: #fff4cc;\n}\na.list-group-item-warning,\nbutton.list-group-item-warning {\n  color: #de8e27;\n}\na.list-group-item-warning .list-group-item-heading,\nbutton.list-group-item-warning .list-group-item-heading {\n  color: inherit;\n}\na.list-group-item-warning:hover,\nbutton.list-group-item-warning:hover,\na.list-group-item-warning:focus,\nbutton.list-group-item-warning:focus {\n  color: #de8e27;\n  background-color: #ffefb3;\n}\na.list-group-item-warning.active,\nbutton.list-group-item-warning.active,\na.list-group-item-warning.active:hover,\nbutton.list-group-item-warning.active:hover,\na.list-group-item-warning.active:focus,\nbutton.list-group-item-warning.active:focus {\n  color: #fff;\n  background-color: #de8e27;\n  border-color: #de8e27;\n}\n.list-group-item-danger {\n  color: #ff2b67;\n  background-color: #f9e9e9;\n}\na.list-group-item-danger,\nbutton.list-group-item-danger {\n  color: #ff2b67;\n}\na.list-group-item-danger .list-group-item-heading,\nbutton.list-group-item-danger .list-group-item-heading {\n  color: inherit;\n}\na.list-group-item-danger:hover,\nbutton.list-group-item-danger:hover,\na.list-group-item-danger:focus,\nbutton.list-group-item-danger:focus {\n  color: #ff2b67;\n  background-color: #f4d5d5;\n}\na.list-group-item-danger.active,\nbutton.list-group-item-danger.active,\na.list-group-item-danger.active:hover,\nbutton.list-group-item-danger.active:hover,\na.list-group-item-danger.active:focus,\nbutton.list-group-item-danger.active:focus {\n  color: #fff;\n  background-color: #ff2b67;\n  border-color: #ff2b67;\n}\n.list-group-item-heading {\n  margin-top: 0;\n  margin-bottom: 5px;\n}\n.list-group-item-text {\n  margin-bottom: 0;\n  line-height: 1.3;\n}\n.panel {\n  margin-bottom: 27px;\n  background-color: transparent;\n  border: 1px solid transparent;\n  border-radius: 0;\n  -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);\n  box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);\n}\n.panel-body {\n  padding: 23px 20px 20px 20px;\n}\n.panel-heading {\n  padding: 20px;\n  border-bottom: 1px solid transparent;\n  border-top-right-radius: -1;\n  border-top-left-radius: -1;\n}\n.panel-heading > .dropdown .dropdown-toggle {\n  color: inherit;\n}\n.panel-title {\n  margin-top: 0;\n  margin-bottom: 0;\n  font-size: 18px;\n  color: inherit;\n}\n.panel-title > a,\n.panel-title > small,\n.panel-title > .small,\n.panel-title > small > a,\n.panel-title > .small > a {\n  color: inherit;\n}\n.panel-footer {\n  padding: 20px;\n  background-color: rgba(255, 255, 255, 0.06);\n  border-top: 1px solid rgba(255, 255, 255, 0.1);\n  border-bottom-right-radius: -1;\n  border-bottom-left-radius: -1;\n}\n.panel > .list-group,\n.panel > .panel-collapse > .list-group {\n  margin-bottom: 0;\n}\n.panel > .list-group .list-group-item,\n.panel > .panel-collapse > .list-group .list-group-item {\n  border-width: 1px 0;\n  border-radius: 0;\n}\n.panel > .list-group:first-child .list-group-item:first-child,\n.panel > .panel-collapse > .list-group:first-child .list-group-item:first-child {\n  border-top: 0;\n  border-top-right-radius: -1;\n  border-top-left-radius: -1;\n}\n.panel > .list-group:last-child .list-group-item:last-child,\n.panel > .panel-collapse > .list-group:last-child .list-group-item:last-child {\n  border-bottom: 0;\n  border-bottom-right-radius: -1;\n  border-bottom-left-radius: -1;\n}\n.panel > .panel-heading + .panel-collapse > .list-group .list-group-item:first-child {\n  border-top-right-radius: 0;\n  border-top-left-radius: 0;\n}\n.panel-heading + .list-group .list-group-item:first-child {\n  border-top-width: 0;\n}\n.list-group + .panel-footer {\n  border-top-width: 0;\n}\n.panel > .table,\n.panel > .table-responsive > .table,\n.panel > .panel-collapse > .table {\n  margin-bottom: 0;\n}\n.panel > .table caption,\n.panel > .table-responsive > .table caption,\n.panel > .panel-collapse > .table caption {\n  padding-left: 23px 20px 20px 20px;\n  padding-right: 23px 20px 20px 20px;\n}\n.panel > .table:first-child,\n.panel > .table-responsive:first-child > .table:first-child {\n  border-top-right-radius: -1;\n  border-top-left-radius: -1;\n}\n.panel > .table:first-child > thead:first-child > tr:first-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child {\n  border-top-left-radius: -1;\n  border-top-right-radius: -1;\n}\n.panel > .table:first-child > thead:first-child > tr:first-child td:first-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:first-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child td:first-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:first-child,\n.panel > .table:first-child > thead:first-child > tr:first-child th:first-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:first-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child th:first-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:first-child {\n  border-top-left-radius: -1;\n}\n.panel > .table:first-child > thead:first-child > tr:first-child td:last-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:last-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child td:last-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:last-child,\n.panel > .table:first-child > thead:first-child > tr:first-child th:last-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:last-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child th:last-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:last-child {\n  border-top-right-radius: -1;\n}\n.panel > .table:last-child,\n.panel > .table-responsive:last-child > .table:last-child {\n  border-bottom-right-radius: -1;\n  border-bottom-left-radius: -1;\n}\n.panel > .table:last-child > tbody:last-child > tr:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child {\n  border-bottom-left-radius: -1;\n  border-bottom-right-radius: -1;\n}\n.panel > .table:last-child > tbody:last-child > tr:last-child td:first-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:first-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child td:first-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:first-child,\n.panel > .table:last-child > tbody:last-child > tr:last-child th:first-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:first-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child th:first-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:first-child {\n  border-bottom-left-radius: -1;\n}\n.panel > .table:last-child > tbody:last-child > tr:last-child td:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:last-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child td:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:last-child,\n.panel > .table:last-child > tbody:last-child > tr:last-child th:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:last-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child th:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:last-child {\n  border-bottom-right-radius: -1;\n}\n.panel > .panel-body + .table,\n.panel > .panel-body + .table-responsive,\n.panel > .table + .panel-body,\n.panel > .table-responsive + .panel-body {\n  border-top: 1px solid #194c5f;\n}\n.panel > .table > tbody:first-child > tr:first-child th,\n.panel > .table > tbody:first-child > tr:first-child td {\n  border-top: 0;\n}\n.panel > .table-bordered,\n.panel > .table-responsive > .table-bordered {\n  border: 0;\n}\n.panel > .table-bordered > thead > tr > th:first-child,\n.panel > .table-responsive > .table-bordered > thead > tr > th:first-child,\n.panel > .table-bordered > tbody > tr > th:first-child,\n.panel > .table-responsive > .table-bordered > tbody > tr > th:first-child,\n.panel > .table-bordered > tfoot > tr > th:first-child,\n.panel > .table-responsive > .table-bordered > tfoot > tr > th:first-child,\n.panel > .table-bordered > thead > tr > td:first-child,\n.panel > .table-responsive > .table-bordered > thead > tr > td:first-child,\n.panel > .table-bordered > tbody > tr > td:first-child,\n.panel > .table-responsive > .table-bordered > tbody > tr > td:first-child,\n.panel > .table-bordered > tfoot > tr > td:first-child,\n.panel > .table-responsive > .table-bordered > tfoot > tr > td:first-child {\n  border-left: 0;\n}\n.panel > .table-bordered > thead > tr > th:last-child,\n.panel > .table-responsive > .table-bordered > thead > tr > th:last-child,\n.panel > .table-bordered > tbody > tr > th:last-child,\n.panel > .table-responsive > .table-bordered > tbody > tr > th:last-child,\n.panel > .table-bordered > tfoot > tr > th:last-child,\n.panel > .table-responsive > .table-bordered > tfoot > tr > th:last-child,\n.panel > .table-bordered > thead > tr > td:last-child,\n.panel > .table-responsive > .table-bordered > thead > tr > td:last-child,\n.panel > .table-bordered > tbody > tr > td:last-child,\n.panel > .table-responsive > .table-bordered > tbody > tr > td:last-child,\n.panel > .table-bordered > tfoot > tr > td:last-child,\n.panel > .table-responsive > .table-bordered > tfoot > tr > td:last-child {\n  border-right: 0;\n}\n.panel > .table-bordered > thead > tr:first-child > td,\n.panel > .table-responsive > .table-bordered > thead > tr:first-child > td,\n.panel > .table-bordered > tbody > tr:first-child > td,\n.panel > .table-responsive > .table-bordered > tbody > tr:first-child > td,\n.panel > .table-bordered > thead > tr:first-child > th,\n.panel > .table-responsive > .table-bordered > thead > tr:first-child > th,\n.panel > .table-bordered > tbody > tr:first-child > th,\n.panel > .table-responsive > .table-bordered > tbody > tr:first-child > th {\n  border-bottom: 0;\n}\n.panel > .table-bordered > tbody > tr:last-child > td,\n.panel > .table-responsive > .table-bordered > tbody > tr:last-child > td,\n.panel > .table-bordered > tfoot > tr:last-child > td,\n.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > td,\n.panel > .table-bordered > tbody > tr:last-child > th,\n.panel > .table-responsive > .table-bordered > tbody > tr:last-child > th,\n.panel > .table-bordered > tfoot > tr:last-child > th,\n.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > th {\n  border-bottom: 0;\n}\n.panel > .table-responsive {\n  border: 0;\n  margin-bottom: 0;\n}\n.panel-group {\n  margin-bottom: 27px;\n}\n.panel-group .panel {\n  margin-bottom: 0;\n  border-radius: 0;\n}\n.panel-group .panel + .panel {\n  margin-top: 5px;\n}\n.panel-group .panel-heading {\n  border-bottom: 0;\n}\n.panel-group .panel-heading + .panel-collapse > .panel-body,\n.panel-group .panel-heading + .panel-collapse > .list-group {\n  border-top: 1px solid rgba(255, 255, 255, 0.1);\n}\n.panel-group .panel-footer {\n  border-top: 0;\n}\n.panel-group .panel-footer + .panel-collapse .panel-body {\n  border-bottom: 1px solid rgba(255, 255, 255, 0.1);\n}\n.panel-default {\n  border-color: #194c5f;\n}\n.panel-default > .panel-heading {\n  color: #1e252f;\n  background-color: #242e3b;\n  border-color: #194c5f;\n}\n.panel-default > .panel-heading + .panel-collapse > .panel-body {\n  border-top-color: #194c5f;\n}\n.panel-default > .panel-heading .badge {\n  color: #242e3b;\n  background-color: #1e252f;\n}\n.panel-default > .panel-footer + .panel-collapse > .panel-body {\n  border-bottom-color: #194c5f;\n}\n.panel-primary {\n  border-color: #2d7ef4;\n}\n.panel-primary > .panel-heading {\n  color: #ffffff;\n  background-color: #2d7ef4;\n  border-color: #2d7ef4;\n}\n.panel-primary > .panel-heading + .panel-collapse > .panel-body {\n  border-top-color: #2d7ef4;\n}\n.panel-primary > .panel-heading .badge {\n  color: #2d7ef4;\n  background-color: #ffffff;\n}\n.panel-primary > .panel-footer + .panel-collapse > .panel-body {\n  border-bottom-color: #2d7ef4;\n}\n.panel-success {\n  border-color: #bddebf;\n}\n.panel-success > .panel-heading {\n  color: #24b47e;\n  background-color: #e3f1e4;\n  border-color: #bddebf;\n}\n.panel-success > .panel-heading + .panel-collapse > .panel-body {\n  border-top-color: #bddebf;\n}\n.panel-success > .panel-heading .badge {\n  color: #e3f1e4;\n  background-color: #24b47e;\n}\n.panel-success > .panel-footer + .panel-collapse > .panel-body {\n  border-bottom-color: #bddebf;\n}\n.panel-info {\n  border-color: #b3dbf1;\n}\n.panel-info > .panel-heading {\n  color: #06a2ff;\n  background-color: #e5f3fa;\n  border-color: #b3dbf1;\n}\n.panel-info > .panel-heading + .panel-collapse > .panel-body {\n  border-top-color: #b3dbf1;\n}\n.panel-info > .panel-heading .badge {\n  color: #e5f3fa;\n  background-color: #06a2ff;\n}\n.panel-info > .panel-footer + .panel-collapse > .panel-body {\n  border-bottom-color: #b3dbf1;\n}\n.panel-warning {\n  border-color: #ffe071;\n}\n.panel-warning > .panel-heading {\n  color: #de8e27;\n  background-color: #fff4cc;\n  border-color: #ffe071;\n}\n.panel-warning > .panel-heading + .panel-collapse > .panel-body {\n  border-top-color: #ffe071;\n}\n.panel-warning > .panel-heading .badge {\n  color: #fff4cc;\n  background-color: #de8e27;\n}\n.panel-warning > .panel-footer + .panel-collapse > .panel-body {\n  border-bottom-color: #ffe071;\n}\n.panel-danger {\n  border-color: #f1c9c9;\n}\n.panel-danger > .panel-heading {\n  color: #ff2b67;\n  background-color: #f9e9e9;\n  border-color: #f1c9c9;\n}\n.panel-danger > .panel-heading + .panel-collapse > .panel-body {\n  border-top-color: #f1c9c9;\n}\n.panel-danger > .panel-heading .badge {\n  color: #f9e9e9;\n  background-color: #ff2b67;\n}\n.panel-danger > .panel-footer + .panel-collapse > .panel-body {\n  border-bottom-color: #f1c9c9;\n}\n.embed-responsive {\n  position: relative;\n  display: block;\n  height: 0;\n  padding: 0;\n  overflow: hidden;\n}\n.embed-responsive .embed-responsive-item,\n.embed-responsive iframe,\n.embed-responsive embed,\n.embed-responsive object,\n.embed-responsive video {\n  position: absolute;\n  top: 0;\n  left: 0;\n  bottom: 0;\n  height: 100%;\n  width: 100%;\n  border: 0;\n}\n.embed-responsive-16by9 {\n  padding-bottom: 56.25%;\n}\n.embed-responsive-4by3 {\n  padding-bottom: 75%;\n}\n.well {\n  min-height: 20px;\n  padding: 19px;\n  margin-bottom: 20px;\n  background-color: #242e3b;\n  border: 1px solid transparent;\n  border-radius: 0;\n  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05);\n  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05);\n}\n.well blockquote {\n  border-color: #ddd;\n  border-color: rgba(0, 0, 0, 0.15);\n}\n.well-lg {\n  padding: 24px;\n  border-radius: 0;\n}\n.well-sm {\n  padding: 9px;\n  border-radius: 0;\n}\n.close {\n  float: right;\n  font-size: 24px;\n  font-weight: bold;\n  line-height: 1;\n  color: #ffffff;\n  text-shadow: 0 1px 0 #1d252f;\n  opacity: 0.2;\n  filter: alpha(opacity=20);\n}\n.close:hover,\n.close:focus {\n  color: #ffffff;\n  text-decoration: none;\n  cursor: pointer;\n  opacity: 0.5;\n  filter: alpha(opacity=50);\n}\nbutton.close {\n  padding: 0;\n  cursor: pointer;\n  background: transparent;\n  border: 0;\n  -webkit-appearance: none;\n}\n.modal-open {\n  overflow: hidden;\n}\n.modal {\n  display: none;\n  overflow: hidden;\n  position: fixed;\n  top: 0;\n  right: 0;\n  bottom: 0;\n  left: 0;\n  z-index: 1050;\n  -webkit-overflow-scrolling: touch;\n  outline: 0;\n}\n.modal.fade .modal-dialog {\n  -webkit-transform: translate(0, -25%);\n  -ms-transform: translate(0, -25%);\n  -o-transform: translate(0, -25%);\n  transform: translate(0, -25%);\n  -webkit-transition: -webkit-transform 0.3s ease-out;\n  -moz-transition: -moz-transform 0.3s ease-out;\n  -o-transition: -o-transform 0.3s ease-out;\n  transition: transform 0.3s ease-out;\n}\n.modal.in .modal-dialog {\n  -webkit-transform: translate(0, 0);\n  -ms-transform: translate(0, 0);\n  -o-transform: translate(0, 0);\n  transform: translate(0, 0);\n}\n.modal-open .modal {\n  overflow-x: hidden;\n  overflow-y: auto;\n}\n.modal-dialog {\n  position: relative;\n  width: auto;\n  margin: 10px;\n}\n.modal-content {\n  position: relative;\n  background-color: #1d252f;\n  border: 1px solid #999999;\n  border: 1px solid transparent;\n  border-radius: 0;\n  -webkit-box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5);\n  box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5);\n  background-clip: padding-box;\n  outline: 0;\n}\n.modal-backdrop {\n  position: fixed;\n  top: 0;\n  right: 0;\n  bottom: 0;\n  left: 0;\n  z-index: 1040;\n  background-color: #2d7ef4;\n}\n.modal-backdrop.fade {\n  opacity: 0;\n  filter: alpha(opacity=0);\n}\n.modal-backdrop.in {\n  opacity: 0.4;\n  filter: alpha(opacity=40);\n}\n.modal-header {\n  padding: 15px 20px 11px 20px;\n  border-bottom: 1px solid transparent;\n}\n.modal-header .close {\n  margin-top: -2px;\n}\n.modal-title {\n  margin: 0;\n  line-height: 1.7;\n}\n.modal-body {\n  position: relative;\n  padding: 15px 20px;\n}\n.modal-footer {\n  padding: 15px 20px;\n  text-align: right;\n  border-top: 1px solid transparent;\n}\n.modal-footer .btn + .btn {\n  margin-left: 5px;\n  margin-bottom: 0;\n}\n.modal-footer .btn-group .btn + .btn {\n  margin-left: -1px;\n}\n.modal-footer .btn-block + .btn-block {\n  margin-left: 0;\n}\n.modal-scrollbar-measure {\n  position: absolute;\n  top: -9999px;\n  width: 50px;\n  height: 50px;\n  overflow: scroll;\n}\n@media (min-width: 768px) {\n  .modal-dialog {\n    width: 768px;\n    margin: 30px auto;\n  }\n  .modal-content {\n    -webkit-box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);\n    box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);\n  }\n  .modal-sm {\n    width: 300px;\n  }\n}\n@media (min-width: 992px) {\n  .modal-lg {\n    width: 900px;\n  }\n}\n.tooltip {\n  position: absolute;\n  z-index: 1070;\n  display: block;\n  font-family: 'Source Sans Pro', sans-serif;\n  font-style: normal;\n  font-weight: normal;\n  letter-spacing: normal;\n  line-break: auto;\n  line-height: 1.7;\n  text-align: left;\n  text-align: start;\n  text-decoration: none;\n  text-shadow: none;\n  text-transform: none;\n  white-space: normal;\n  word-break: normal;\n  word-spacing: normal;\n  word-wrap: normal;\n  font-size: 14px;\n  opacity: 0;\n  filter: alpha(opacity=0);\n}\n.tooltip.in {\n  opacity: 0.9;\n  filter: alpha(opacity=90);\n}\n.tooltip.top {\n  margin-top: -3px;\n  padding: 5px 0;\n}\n.tooltip.right {\n  margin-left: 3px;\n  padding: 0 5px;\n}\n.tooltip.bottom {\n  margin-top: 3px;\n  padding: 5px 0;\n}\n.tooltip.left {\n  margin-left: -3px;\n  padding: 0 5px;\n}\n.tooltip-inner {\n  max-width: 200px;\n  padding: 3px 8px;\n  color: #ffffff;\n  text-align: center;\n  background-color: #000000;\n  border-radius: 0;\n}\n.tooltip-arrow {\n  position: absolute;\n  width: 0;\n  height: 0;\n  border-color: transparent;\n  border-style: solid;\n}\n.tooltip.top .tooltip-arrow {\n  bottom: 0;\n  left: 50%;\n  margin-left: -5px;\n  border-width: 5px 5px 0;\n  border-top-color: #000000;\n}\n.tooltip.top-left .tooltip-arrow {\n  bottom: 0;\n  right: 5px;\n  margin-bottom: -5px;\n  border-width: 5px 5px 0;\n  border-top-color: #000000;\n}\n.tooltip.top-right .tooltip-arrow {\n  bottom: 0;\n  left: 5px;\n  margin-bottom: -5px;\n  border-width: 5px 5px 0;\n  border-top-color: #000000;\n}\n.tooltip.right .tooltip-arrow {\n  top: 50%;\n  left: 0;\n  margin-top: -5px;\n  border-width: 5px 5px 5px 0;\n  border-right-color: #000000;\n}\n.tooltip.left .tooltip-arrow {\n  top: 50%;\n  right: 0;\n  margin-top: -5px;\n  border-width: 5px 0 5px 5px;\n  border-left-color: #000000;\n}\n.tooltip.bottom .tooltip-arrow {\n  top: 0;\n  left: 50%;\n  margin-left: -5px;\n  border-width: 0 5px 5px;\n  border-bottom-color: #000000;\n}\n.tooltip.bottom-left .tooltip-arrow {\n  top: 0;\n  right: 5px;\n  margin-top: -5px;\n  border-width: 0 5px 5px;\n  border-bottom-color: #000000;\n}\n.tooltip.bottom-right .tooltip-arrow {\n  top: 0;\n  left: 5px;\n  margin-top: -5px;\n  border-width: 0 5px 5px;\n  border-bottom-color: #000000;\n}\n.popover {\n  position: absolute;\n  top: 0;\n  left: 0;\n  z-index: 1060;\n  display: none;\n  max-width: 276px;\n  padding: 1px;\n  font-family: 'Source Sans Pro', sans-serif;\n  font-style: normal;\n  font-weight: normal;\n  letter-spacing: normal;\n  line-break: auto;\n  line-height: 1.7;\n  text-align: left;\n  text-align: start;\n  text-decoration: none;\n  text-shadow: none;\n  text-transform: none;\n  white-space: normal;\n  word-break: normal;\n  word-spacing: normal;\n  word-wrap: normal;\n  font-size: 16px;\n  background-color: #ffffff;\n  background-clip: padding-box;\n  border: 1px solid #cccccc;\n  border: 1px solid rgba(0, 0, 0, 0.2);\n  border-radius: 0;\n  -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);\n  box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);\n}\n.popover.top {\n  margin-top: -10px;\n}\n.popover.right {\n  margin-left: 10px;\n}\n.popover.bottom {\n  margin-top: 10px;\n}\n.popover.left {\n  margin-left: -10px;\n}\n.popover-title {\n  margin: 0;\n  padding: 8px 14px;\n  font-size: 16px;\n  background-color: #f7f7f7;\n  border-bottom: 1px solid #ebebeb;\n  border-radius: -1 -1 0 0;\n}\n.popover-content {\n  padding: 9px 14px;\n}\n.popover > .arrow,\n.popover > .arrow:after {\n  position: absolute;\n  display: block;\n  width: 0;\n  height: 0;\n  border-color: transparent;\n  border-style: solid;\n}\n.popover > .arrow {\n  border-width: 11px;\n}\n.popover > .arrow:after {\n  border-width: 10px;\n  content: \"\";\n}\n.popover.top > .arrow {\n  left: 50%;\n  margin-left: -11px;\n  border-bottom-width: 0;\n  border-top-color: #999999;\n  border-top-color: rgba(0, 0, 0, 0.25);\n  bottom: -11px;\n}\n.popover.top > .arrow:after {\n  content: \" \";\n  bottom: 1px;\n  margin-left: -10px;\n  border-bottom-width: 0;\n  border-top-color: #ffffff;\n}\n.popover.right > .arrow {\n  top: 50%;\n  left: -11px;\n  margin-top: -11px;\n  border-left-width: 0;\n  border-right-color: #999999;\n  border-right-color: rgba(0, 0, 0, 0.25);\n}\n.popover.right > .arrow:after {\n  content: \" \";\n  left: 1px;\n  bottom: -10px;\n  border-left-width: 0;\n  border-right-color: #ffffff;\n}\n.popover.bottom > .arrow {\n  left: 50%;\n  margin-left: -11px;\n  border-top-width: 0;\n  border-bottom-color: #999999;\n  border-bottom-color: rgba(0, 0, 0, 0.25);\n  top: -11px;\n}\n.popover.bottom > .arrow:after {\n  content: \" \";\n  top: 1px;\n  margin-left: -10px;\n  border-top-width: 0;\n  border-bottom-color: #ffffff;\n}\n.popover.left > .arrow {\n  top: 50%;\n  right: -11px;\n  margin-top: -11px;\n  border-right-width: 0;\n  border-left-color: #999999;\n  border-left-color: rgba(0, 0, 0, 0.25);\n}\n.popover.left > .arrow:after {\n  content: \" \";\n  right: 1px;\n  border-right-width: 0;\n  border-left-color: #ffffff;\n  bottom: -10px;\n}\n.carousel {\n  position: relative;\n}\n.carousel-inner {\n  position: relative;\n  overflow: hidden;\n  width: 100%;\n}\n.carousel-inner > .item {\n  display: none;\n  position: relative;\n  -webkit-transition: 0.6s ease-in-out left;\n  -o-transition: 0.6s ease-in-out left;\n  transition: 0.6s ease-in-out left;\n}\n.carousel-inner > .item > img,\n.carousel-inner > .item > a > img {\n  line-height: 1;\n}\n@media all and (transform-3d), (-webkit-transform-3d) {\n  .carousel-inner > .item {\n    -webkit-transition: -webkit-transform 0.6s ease-in-out;\n    -moz-transition: -moz-transform 0.6s ease-in-out;\n    -o-transition: -o-transform 0.6s ease-in-out;\n    transition: transform 0.6s ease-in-out;\n    -webkit-backface-visibility: hidden;\n    -moz-backface-visibility: hidden;\n    backface-visibility: hidden;\n    -webkit-perspective: 1000px;\n    -moz-perspective: 1000px;\n    perspective: 1000px;\n  }\n  .carousel-inner > .item.next,\n  .carousel-inner > .item.active.right {\n    -webkit-transform: translate3d(100%, 0, 0);\n    transform: translate3d(100%, 0, 0);\n    left: 0;\n  }\n  .carousel-inner > .item.prev,\n  .carousel-inner > .item.active.left {\n    -webkit-transform: translate3d(-100%, 0, 0);\n    transform: translate3d(-100%, 0, 0);\n    left: 0;\n  }\n  .carousel-inner > .item.next.left,\n  .carousel-inner > .item.prev.right,\n  .carousel-inner > .item.active {\n    -webkit-transform: translate3d(0, 0, 0);\n    transform: translate3d(0, 0, 0);\n    left: 0;\n  }\n}\n.carousel-inner > .active,\n.carousel-inner > .next,\n.carousel-inner > .prev {\n  display: block;\n}\n.carousel-inner > .active {\n  left: 0;\n}\n.carousel-inner > .next,\n.carousel-inner > .prev {\n  position: absolute;\n  top: 0;\n  width: 100%;\n}\n.carousel-inner > .next {\n  left: 100%;\n}\n.carousel-inner > .prev {\n  left: -100%;\n}\n.carousel-inner > .next.left,\n.carousel-inner > .prev.right {\n  left: 0;\n}\n.carousel-inner > .active.left {\n  left: -100%;\n}\n.carousel-inner > .active.right {\n  left: 100%;\n}\n.carousel-control {\n  position: absolute;\n  top: 0;\n  left: 0;\n  bottom: 0;\n  width: 15%;\n  opacity: 0.5;\n  filter: alpha(opacity=50);\n  font-size: 20px;\n  color: #ffffff;\n  text-align: center;\n  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);\n  background-color: rgba(0, 0, 0, 0);\n}\n.carousel-control.left {\n  background-image: -webkit-linear-gradient(left, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.0001) 100%);\n  background-image: -o-linear-gradient(left, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.0001) 100%);\n  background-image: linear-gradient(to right, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.0001) 100%);\n  background-repeat: repeat-x;\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1);\n}\n.carousel-control.right {\n  left: auto;\n  right: 0;\n  background-image: -webkit-linear-gradient(left, rgba(0, 0, 0, 0.0001) 0%, rgba(0, 0, 0, 0.5) 100%);\n  background-image: -o-linear-gradient(left, rgba(0, 0, 0, 0.0001) 0%, rgba(0, 0, 0, 0.5) 100%);\n  background-image: linear-gradient(to right, rgba(0, 0, 0, 0.0001) 0%, rgba(0, 0, 0, 0.5) 100%);\n  background-repeat: repeat-x;\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1);\n}\n.carousel-control:hover,\n.carousel-control:focus {\n  outline: 0;\n  color: #ffffff;\n  text-decoration: none;\n  opacity: 0.9;\n  filter: alpha(opacity=90);\n}\n.carousel-control .icon-prev,\n.carousel-control .icon-next,\n.carousel-control .glyphicon-chevron-left,\n.carousel-control .glyphicon-chevron-right {\n  position: absolute;\n  top: 50%;\n  margin-top: -10px;\n  z-index: 5;\n  display: inline-block;\n}\n.carousel-control .icon-prev,\n.carousel-control .glyphicon-chevron-left {\n  left: 50%;\n  margin-left: -10px;\n}\n.carousel-control .icon-next,\n.carousel-control .glyphicon-chevron-right {\n  right: 50%;\n  margin-right: -10px;\n}\n.carousel-control .icon-prev,\n.carousel-control .icon-next {\n  width: 20px;\n  height: 20px;\n  line-height: 1;\n  font-family: serif;\n}\n.carousel-control .icon-prev:before {\n  content: '\\2039';\n}\n.carousel-control .icon-next:before {\n  content: '\\203a';\n}\n.carousel-indicators {\n  position: absolute;\n  bottom: 10px;\n  left: 50%;\n  z-index: 15;\n  width: 60%;\n  margin-left: -30%;\n  padding-left: 0;\n  list-style: none;\n  text-align: center;\n}\n.carousel-indicators li {\n  display: inline-block;\n  width: 10px;\n  height: 10px;\n  margin: 1px;\n  text-indent: -999px;\n  border: 1px solid #ffffff;\n  border-radius: 10px;\n  cursor: pointer;\n  background-color: #000 \\9;\n  background-color: rgba(0, 0, 0, 0);\n}\n.carousel-indicators .active {\n  margin: 0;\n  width: 12px;\n  height: 12px;\n  background-color: #ffffff;\n}\n.carousel-caption {\n  position: absolute;\n  left: 15%;\n  right: 15%;\n  bottom: 20px;\n  z-index: 10;\n  padding-top: 20px;\n  padding-bottom: 20px;\n  color: #ffffff;\n  text-align: center;\n  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);\n}\n.carousel-caption .btn {\n  text-shadow: none;\n}\n@media screen and (min-width: 768px) {\n  .carousel-control .glyphicon-chevron-left,\n  .carousel-control .glyphicon-chevron-right,\n  .carousel-control .icon-prev,\n  .carousel-control .icon-next {\n    width: 30px;\n    height: 30px;\n    margin-top: -10px;\n    font-size: 30px;\n  }\n  .carousel-control .glyphicon-chevron-left,\n  .carousel-control .icon-prev {\n    margin-left: -10px;\n  }\n  .carousel-control .glyphicon-chevron-right,\n  .carousel-control .icon-next {\n    margin-right: -10px;\n  }\n  .carousel-caption {\n    left: 20%;\n    right: 20%;\n    padding-bottom: 30px;\n  }\n  .carousel-indicators {\n    bottom: 20px;\n  }\n}\n.clearfix:before,\n.clearfix:after,\n.dl-horizontal dd:before,\n.dl-horizontal dd:after,\n.container:before,\n.container:after,\n.container-fluid:before,\n.container-fluid:after,\n.row:before,\n.row:after,\n.form-horizontal .form-group:before,\n.form-horizontal .form-group:after,\n.btn-toolbar:before,\n.btn-toolbar:after,\n.btn-group-vertical > .btn-group:before,\n.btn-group-vertical > .btn-group:after,\n.nav:before,\n.nav:after,\n.navbar:before,\n.navbar:after,\n.navbar-header:before,\n.navbar-header:after,\n.navbar-collapse:before,\n.navbar-collapse:after,\n.pager:before,\n.pager:after,\n.panel-body:before,\n.panel-body:after,\n.modal-header:before,\n.modal-header:after,\n.modal-footer:before,\n.modal-footer:after {\n  content: \" \";\n  display: table;\n}\n.clearfix:after,\n.dl-horizontal dd:after,\n.container:after,\n.container-fluid:after,\n.row:after,\n.form-horizontal .form-group:after,\n.btn-toolbar:after,\n.btn-group-vertical > .btn-group:after,\n.nav:after,\n.navbar:after,\n.navbar-header:after,\n.navbar-collapse:after,\n.pager:after,\n.panel-body:after,\n.modal-header:after,\n.modal-footer:after {\n  clear: both;\n}\n.center-block {\n  display: block;\n  margin-left: auto;\n  margin-right: auto;\n}\n.pull-right {\n  float: right !important;\n}\n.pull-left {\n  float: left !important;\n}\n.hide {\n  display: none !important;\n}\n.show {\n  display: block !important;\n}\n.invisible {\n  visibility: hidden;\n}\n.text-hide {\n  font: 0/0 a;\n  color: transparent;\n  text-shadow: none;\n  background-color: transparent;\n  border: 0;\n}\n.hidden {\n  display: none !important;\n}\n.affix {\n  position: fixed;\n}\n@-ms-viewport {\n  width: device-width;\n}\n.visible-xs,\n.visible-sm,\n.visible-md,\n.visible-lg {\n  display: none !important;\n}\n.visible-xs-block,\n.visible-xs-inline,\n.visible-xs-inline-block,\n.visible-sm-block,\n.visible-sm-inline,\n.visible-sm-inline-block,\n.visible-md-block,\n.visible-md-inline,\n.visible-md-inline-block,\n.visible-lg-block,\n.visible-lg-inline,\n.visible-lg-inline-block {\n  display: none !important;\n}\n@media (max-width: 767px) {\n  .visible-xs {\n    display: block !important;\n  }\n  table.visible-xs {\n    display: table !important;\n  }\n  tr.visible-xs {\n    display: table-row !important;\n  }\n  th.visible-xs,\n  td.visible-xs {\n    display: table-cell !important;\n  }\n}\n@media (max-width: 767px) {\n  .visible-xs-block {\n    display: block !important;\n  }\n}\n@media (max-width: 767px) {\n  .visible-xs-inline {\n    display: inline !important;\n  }\n}\n@media (max-width: 767px) {\n  .visible-xs-inline-block {\n    display: inline-block !important;\n  }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n  .visible-sm {\n    display: block !important;\n  }\n  table.visible-sm {\n    display: table !important;\n  }\n  tr.visible-sm {\n    display: table-row !important;\n  }\n  th.visible-sm,\n  td.visible-sm {\n    display: table-cell !important;\n  }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n  .visible-sm-block {\n    display: block !important;\n  }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n  .visible-sm-inline {\n    display: inline !important;\n  }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n  .visible-sm-inline-block {\n    display: inline-block !important;\n  }\n}\n@media (min-width: 992px) and (max-width: 991px) {\n  .visible-md {\n    display: block !important;\n  }\n  table.visible-md {\n    display: table !important;\n  }\n  tr.visible-md {\n    display: table-row !important;\n  }\n  th.visible-md,\n  td.visible-md {\n    display: table-cell !important;\n  }\n}\n@media (min-width: 992px) and (max-width: 991px) {\n  .visible-md-block {\n    display: block !important;\n  }\n}\n@media (min-width: 992px) and (max-width: 991px) {\n  .visible-md-inline {\n    display: inline !important;\n  }\n}\n@media (min-width: 992px) and (max-width: 991px) {\n  .visible-md-inline-block {\n    display: inline-block !important;\n  }\n}\n@media (min-width: 992px) {\n  .visible-lg {\n    display: block !important;\n  }\n  table.visible-lg {\n    display: table !important;\n  }\n  tr.visible-lg {\n    display: table-row !important;\n  }\n  th.visible-lg,\n  td.visible-lg {\n    display: table-cell !important;\n  }\n}\n@media (min-width: 992px) {\n  .visible-lg-block {\n    display: block !important;\n  }\n}\n@media (min-width: 992px) {\n  .visible-lg-inline {\n    display: inline !important;\n  }\n}\n@media (min-width: 992px) {\n  .visible-lg-inline-block {\n    display: inline-block !important;\n  }\n}\n@media (max-width: 767px) {\n  .hidden-xs {\n    display: none !important;\n  }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n  .hidden-sm {\n    display: none !important;\n  }\n}\n@media (min-width: 992px) and (max-width: 991px) {\n  .hidden-md {\n    display: none !important;\n  }\n}\n@media (min-width: 992px) {\n  .hidden-lg {\n    display: none !important;\n  }\n}\n.visible-print {\n  display: none !important;\n}\n@media print {\n  .visible-print {\n    display: block !important;\n  }\n  table.visible-print {\n    display: table !important;\n  }\n  tr.visible-print {\n    display: table-row !important;\n  }\n  th.visible-print,\n  td.visible-print {\n    display: table-cell !important;\n  }\n}\n.visible-print-block {\n  display: none !important;\n}\n@media print {\n  .visible-print-block {\n    display: block !important;\n  }\n}\n.visible-print-inline {\n  display: none !important;\n}\n@media print {\n  .visible-print-inline {\n    display: inline !important;\n  }\n}\n.visible-print-inline-block {\n  display: none !important;\n}\n@media print {\n  .visible-print-inline-block {\n    display: inline-block !important;\n  }\n}\n@media print {\n  .hidden-print {\n    display: none !important;\n  }\n}\n"
  },
  {
    "path": "docs/assets/css/collection_browser.scss",
    "content": ".collection-browser-doc.subpage .subpage__header {\r\n  padding-top: 5px;\r\n  padding-bottom: 5px;\r\n  overflow: hidden;\r\n\r\n  &.subpage__header--min {\r\n    overflow: visible;\r\n\r\n    .header-shapes-top {\r\n      position: absolute;\r\n      top: -20px;\r\n      width: 80%;\r\n      max-width: 800px;\r\n      left: auto;\r\n      right: 510px;\r\n\r\n      @media all and (max-width: 1200px) {\r\n        right: 410px;\r\n      }\r\n      @media all and (max-width: 991px) {\r\n        right: auto;\r\n        width: 600px;\r\n        top: -5px;\r\n        left: 50%;\r\n        margin-left: -300px;\r\n      }\r\n      @media all and (max-width: 767px) {\r\n        display: none;\r\n      }\r\n    }\r\n  }\r\n}\r\n\r\n.container-cb-lg {\r\n  max-width: 1600px;\r\n  margin: auto;\r\n  padding-left: 15px;\r\n  padding-right: 15px;\r\n\r\n  .col-md-2-5 {\r\n    width: 24.9%;\r\n    max-width: 400px;\r\n  }\r\n\r\n  #toc {\r\n    font-family: 'Source Sans Pro', sans-serif;\r\n    max-width: 350px;\r\n  }\r\n}\r\n\r\n@media all and (min-width: 1200px) {\r\n  .collection-browser .container-cb-lg .cb-doc-content, .collection-browser-doc .container-cb-lg .cb-doc-content {\r\n    margin-left: 0;\r\n  }\r\n}\r\n\r\n@media all and (max-width: 991px) {\r\n  .collection-browser .container-cb-lg .col-md-2-5, .collection-browser-doc .container-cb-lg .col-md-2-5 {\r\n    max-width: 100vw;\r\n    padding-left: 0px;\r\n    padding-right: 0px;\r\n  }\r\n\r\n  .collection-browser .container-cb-lg .col-md-2-5 #toc, .collection-browser-doc .container-cb-lg .col-md-2-5 #toc {\r\n    padding-left: 15px;\r\n    padding-right: 15px;\r\n    width: 100%;\r\n    max-width: 992px;\r\n    top: 0;\r\n  }\r\n\r\n  .collection-browser .container-cb-lg .col-md-2-5 #toc.fixed, .collection-browser-doc .container-cb-lg .col-md-2-5 #toc.fixed {\r\n    position: static;\r\n  }\r\n}\r\n\r\n.cb-doc-listing {\r\n  max-width: 800px;\r\n  margin-left: auto;\r\n  margin-right: auto;\r\n\r\n  @media all and (min-width: 1200px) {\r\n    & {\r\n      max-width: 800px;\r\n      margin-left: 0;\r\n      margin-right: auto;\r\n    }\r\n  }\r\n}\r\n\r\n.collection-browser-doc {\r\n  h1 {\r\n    margin: 20px auto;\r\n    font-size: 35px;\r\n    line-height: normal;\r\n    font-weight: normal;\r\n    text-align: center;\r\n    color: #1e252f;\r\n  }\r\n\r\n  .cb-doc-content {\r\n    max-width: 800px;\r\n    margin-left: auto;\r\n    margin-right: auto;\r\n\r\n    img {\r\n      max-width: 100%;\r\n    }\r\n  }\r\n\r\n  .cb-doc-detail {\r\n    margin-bottom: 150px;\r\n\r\n    .cb-doc-header {\r\n      margin-bottom: 50px;\r\n    }\r\n\r\n    h2 {\r\n      margin-top: 50px;\r\n      margin-bottom: 20px;\r\n      border-bottom: solid 2px #5b4de5;\r\n      font-weight: bold;\r\n      font-size: 28px;\r\n\r\n      &:first-child {\r\n        margin-top: 0px;\r\n      }\r\n    }\r\n\r\n    h3 {\r\n      margin-top: 50px;\r\n      font-weight: normal;\r\n      font-size: 28px;\r\n    }\r\n\r\n    h4 {\r\n      margin-top: 50px;\r\n      font-weight: normal;\r\n      font-size: 22px;\r\n    }\r\n\r\n    p, .listingblock {\r\n      margin-bottom: 20px;\r\n    }\r\n  }\r\n\r\n  .cb-doc-title-image {\r\n    padding: 0px;\r\n    max-height: 60px;\r\n    max-width: 130px;\r\n    margin: 30px auto;\r\n  }\r\n\r\n  .cb-doc-title {\r\n    color: #000000;\r\n    font-size: 35px;\r\n    line-height: normal;\r\n    text-align: center;\r\n    max-width: 760px;\r\n    margin: 0 auto;\r\n\r\n    p {\r\n      font-weight: 600;\r\n      margin-bottom: 0px;\r\n      opacity: 0.5;\r\n      font-size: 14px;\r\n      letter-spacing: 0.39px;\r\n    }\r\n\r\n    h1, h2, h3, h4, h5, h6 {\r\n      color: #1e252f;\r\n    }\r\n  }\r\n\r\n  .breadcrumb {\r\n    font-size: 14px;\r\n    margin-bottom: 0;\r\n    letter-spacing: -0.2px;\r\n    background-color: white;\r\n    font-weight: normal;\r\n    margin-top: 30px;\r\n\r\n    > a {\r\n      color: #5b4de5;\r\n      text-decoration: none;\r\n\r\n      &:last-child span {\r\n        border-bottom: 1px solid #5b4de5;\r\n        text-shadow: 0.5px 0 0;\r\n\r\n        &:hover {\r\n          border-bottom: 2px solid #5b4de5;\r\n          text-shadow: 0.5px 0 0;\r\n        }\r\n      }\r\n\r\n      &:first-child {\r\n        &:hover, &:active {\r\n          border-bottom: 2px solid #5b4de5;\r\n          text-shadow: 0.5px 0 0;\r\n        }\r\n      }\r\n\r\n      span {\r\n        &:hover, &:active {\r\n          border-bottom: 2px solid #5b4de5;\r\n          text-shadow: 0.5px 0 0;\r\n        }\r\n      }\r\n    }\r\n  }\r\n\r\n  .imageblock {\r\n    margin-bottom: 30px;\r\n\r\n    .title {\r\n      text-align: center;\r\n      color: #bbbdc0;\r\n    }\r\n  }\r\n}\r\n\r\n#toc.cb-doc-sidebar {\r\n  padding: 15px 5px;\r\n  padding-top: 20px;\r\n  font-size: 14px;\r\n\r\n  a {\r\n    color: #bbbdc0;\r\n  }\r\n\r\n  .nav-doc-collection-link {\r\n    border-bottom: 1px solid #1e252f;\r\n    width: 100%;\r\n    padding: 5px 0;\r\n    text-transform: capitalize;\r\n    font-weight: bold;\r\n    color: #1e252f;\r\n\r\n    a {\r\n      text-transform: capitalize;\r\n      font-weight: bold;\r\n      color: #1e252f;\r\n    }\r\n  }\r\n\r\n  .arrow-icon {\r\n    font-size: 13px;\r\n    line-height: 13px;\r\n    padding-right: 4px;\r\n    padding-left: 2px;\r\n    margin-right: 5px;\r\n  }\r\n\r\n  ul {\r\n    list-style: none;\r\n    padding-left: 15px;\r\n\r\n    li:last-child {\r\n      border-bottom-width: 0px;\r\n    }\r\n  }\r\n\r\n  > ul {\r\n    padding: 5px 0 5px 15px;\r\n\r\n    > li {\r\n      a {\r\n        text-transform: none;\r\n        font-size: 14px;\r\n        color: #bbbdc0;\r\n      }\r\n\r\n      border-bottom: 1px solid #bbbdc0;\r\n\r\n      > ul > li {\r\n        border-bottom: 1px solid #bbbdc0;\r\n      }\r\n    }\r\n\r\n    padding-left: 0px;\r\n  }\r\n\r\n  .arrow-icon {\r\n    color: #bbbdc0;\r\n  }\r\n\r\n  .no-collapsable .arrow-icon {\r\n    font-size: 10px;\r\n  }\r\n\r\n  [data-toggle=\"collapse\"] {\r\n    .arrow-icon, + a {\r\n      color: #5b4de5;\r\n    }\r\n\r\n    &.collapsed {\r\n      .arrow-icon {\r\n        color: #bbbdc0;\r\n        -webkit-transform: rotate(-90deg);\r\n        -moz-transform: rotate(-90deg);\r\n        -o-transform: rotate(-90deg);\r\n        -ms-transform: rotate(-90deg);\r\n        transform: rotate(-90deg);\r\n      }\r\n\r\n      + a {\r\n        color: #bbbdc0;\r\n      }\r\n    }\r\n\r\n    &.active {\r\n      .arrow-icon {\r\n        color: #5b4de5;\r\n      }\r\n\r\n      + a {\r\n        color: #5b4de5;\r\n      }\r\n    }\r\n  }\r\n}\r\n\r\n.cb-input-placeholder {\r\n  font-size: 14px;\r\n  font-style: normal;\r\n  opacity: 1 !important;\r\n  font-weight: 600;\r\n  letter-spacing: -0.23px;\r\n  color: #0000;\r\n}\r\n\r\n.remove-visibility {\r\n  display: none;\r\n  clear: both;\r\n}\r\n\r\n.no-padding {\r\n  padding: 0 !important;\r\n  margin: 0 !important;\r\n}\r\n\r\n.no-transform {\r\n  transform: none;\r\n  box-shadow: none;\r\n  outline: none !important;\r\n}\r\n\r\n.active-button {\r\n  background-color: #fff !important;\r\n  position: relative;\r\n  border-radius: 8px !important;\r\n  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);\r\n  cursor: pointer;\r\n  border: 1px solid #dedede !important;\r\n  outline-color: #fff;\r\n}\r\n\r\n.cb-outline {\r\n  padding: 0;\r\n\r\n  a {\r\n    color: #999999;\r\n\r\n    &:active, &:hover {\r\n      text-decoration: none;\r\n      color: #06a6fd;\r\n    }\r\n  }\r\n\r\n  ul {\r\n    font-size: 16px;\r\n    font-weight: bold;\r\n    letter-spacing: -0.2px;\r\n  }\r\n}\r\n\r\n.col-md-2-5 {\r\n  position: relative;\r\n  min-height: 1px;\r\n  padding-right: 15px;\r\n  padding-left: 15px;\r\n  max-height: 100vh;\r\n  overflow-y: auto;\r\n  width: 25%;\r\n}\r\n\r\n@media (min-width: 992px) {\r\n  .col-md-2-5 {\r\n    float: left;\r\n    width: 20.66666667%;\r\n  }\r\n}\r\n\r\n@media all and (max-width: 991px) {\r\n  .collection-browser .col-md-2-5, .collection-browser-doc .col-md-2-5 {\r\n    width: 100vw;\r\n    height: 100vh;\r\n    background: #fff;\r\n    position: fixed;\r\n    left: -120vw;\r\n    top: 0;\r\n    z-index: 100;\r\n    transition: all 0.2s ease;\r\n    overflow: initial;\r\n  }\r\n\r\n  .collection-browser .col-md-2-5 #toc.cd-doc-sidebar.fixed, .collection-browser-doc .col-md-2-5 #toc.cd-doc-sidebar.fixed {\r\n    position: static;\r\n  }\r\n\r\n  .collection-browser .col-md-2-5.opened, .collection-browser-doc .col-md-2-5.opened {\r\n    left: 0;\r\n  }\r\n\r\n  .collection-browser .col-md-2-5.opened #toc-toggle-open, .collection-browser-doc .col-md-2-5.opened #toc-toggle-open {\r\n    display: none;\r\n  }\r\n\r\n  .collection-browser .col-md-2-5 #toc.cb-doc-sidebar > ul > li a, .collection-browser-doc .col-md-2-5 #toc.cb-doc-sidebar > ul > li a {\r\n    font-size: 18px;\r\n  }\r\n\r\n  .collection-browser .col-md-2-5 #toc.cd-doc-sidebar > ul > li .arrow-icon, .collection-browser-doc .col-md-2-5 #toc.cd-doc-sidebar > ul > li .arrow-icon {\r\n    font-size: 15px;\r\n    line-height: 15px;\r\n  }\r\n}\r\n\r\n.toc-toggle {\r\n  width: 100%;\r\n  padding: 5px;\r\n\r\n  .toc-toggle__button {\r\n    background: #fff;\r\n    width: 40px;\r\n    height: 40px;\r\n    border-radius: 50%;\r\n    border: 1px solid #bbbdc0;\r\n    display: flex;\r\n    align-items: center;\r\n    justify-content: center;\r\n\r\n    &:hover {\r\n      cursor: pointer;\r\n      color: #5b4de5;\r\n    }\r\n  }\r\n\r\n  #toc-toggle-open {\r\n    position: fixed;\r\n    bottom: 15px;\r\n    left: 15px;\r\n    z-index: 10;\r\n  }\r\n\r\n  #toc-toggle-close {\r\n    margin-right: 0;\r\n    margin-left: auto;\r\n    position: relative;\r\n    z-index: 101;\r\n  }\r\n}\r\n\r\n.collection-browser {\r\n  .cb-doc-listing h3 {\r\n    max-width: 90%;\r\n    margin-left: auto;\r\n    margin-right: auto;\r\n    margin-bottom: 10px;\r\n  }\r\n\r\n  .cb-doc-card {\r\n    max-width: 90%;\r\n    margin-left: auto;\r\n    margin-right: auto;\r\n  }\r\n\r\n  #toc {\r\n    max-height: 100vh;\r\n    overflow-y: auto;\r\n\r\n    ul {\r\n      margin-top: 0;\r\n    }\r\n  }\r\n}\r\n\r\n#toc {\r\n  padding: 0;\r\n\r\n  &.fixed {\r\n    width: inherit;\r\n    padding-right: 16px !important;\r\n  }\r\n\r\n  a {\r\n    font-weight: normal;\r\n    text-transform: uppercase;\r\n    color: #1e252f;\r\n    color: #999999;\r\n\r\n    &:active, &:hover {\r\n      text-decoration: none;\r\n      color: #06a6fd;\r\n    }\r\n  }\r\n\r\n  ul {\r\n    font-size: 16px;\r\n    font-weight: bold;\r\n    letter-spacing: -0.2px;\r\n  }\r\n\r\n  .section-nav, .sectlevel1 {\r\n    display: block;\r\n    z-index: 2;\r\n    padding: 30px 5px;\r\n    overflow-y: auto;\r\n    max-height: 100vh;\r\n    overflow-wrap: break-word;\r\n  }\r\n\r\n  .section-nav li, .sectlevel1 li {\r\n    list-style-type: none;\r\n  }\r\n\r\n  .section-nav li:hover, .sectlevel1 li:hover {\r\n    cursor: pointer;\r\n  }\r\n\r\n  .sectlevel2 {\r\n    padding-left: 20px !important;\r\n\r\n    li {\r\n      line-height: 16px;\r\n      margin-bottom: 8px;\r\n    }\r\n  }\r\n\r\n  a {\r\n    &.selected, &:hover, &:active {\r\n      color: #5b4de5 !important;\r\n      text-decoration: none;\r\n      font-weight: bold;\r\n    }\r\n  }\r\n}\r\n\r\n@media (max-width: 991px) {\r\n  #toc {\r\n    .section-nav, .sectlevel1 {\r\n      display: none;\r\n      clear: both;\r\n    }\r\n  }\r\n}\r\n\r\nbody .cb .cb-section-white {\r\n  font-family: 'Open Sans', sans-serif;\r\n  color: #1e252f;\r\n}\r\n\r\n.cb-doc-listing {\r\n  .category-head {\r\n    font-size: 30px;\r\n    font-weight: 600;\r\n    line-height: normal;\r\n    letter-spacing: 0.84px;\r\n    color: #1e252f;\r\n    margin-top: 40px;\r\n    margin-bottom: 11px;\r\n\r\n    &:first-of-type {\r\n      margin-top: 1em;\r\n    }\r\n\r\n    &:hover > a.anchorjs-link {\r\n      text-decoration: none;\r\n      display: none;\r\n    }\r\n  }\r\n\r\n  .card {\r\n    color: #1e252f;\r\n    cursor: pointer;\r\n    min-height: 100px;\r\n    overflow: hidden;\r\n    position: relative;\r\n    display: flex;\r\n    align-items: center;\r\n\r\n    & > .row {\r\n      width: 100%;\r\n    }\r\n\r\n    .card-title {\r\n      margin-top: 0;\r\n      margin-bottom: 8px;\r\n      font-size: 20px;\r\n      letter-spacing: 0.56px;\r\n\r\n      &:hover > a.anchorjs-link {\r\n        color: #FFFFFF;\r\n        text-decoration: none;\r\n        display: none;\r\n      }\r\n    }\r\n\r\n    a.card-title__link:hover {\r\n      color: $primary-color;\r\n      a, b, h5 {\r\n        color: $primary-color;\r\n      }\r\n    }\r\n\r\n    a {\r\n      text-decoration: none;\r\n    }\r\n\r\n    .card-body {\r\n      padding: 25px 30px 25px 30px;\r\n    }\r\n\r\n    .img-inner {\r\n      position: absolute;\r\n      top: 50%;\r\n      -ms-transform: translateY(-50%);\r\n      transform: translateY(-50%);\r\n    }\r\n\r\n    img {\r\n      max-width: 100%;\r\n      display: block;\r\n      margin: auto auto;\r\n      padding: 11px 20px 11px 30px;\r\n      max-height: 85px;\r\n    }\r\n\r\n    p {\r\n      font-size: 13px;\r\n      font-weight: inherit;\r\n      line-height: 1.5;\r\n      letter-spacing: 0.34px;\r\n      margin: 0;\r\n    }\r\n\r\n    h6, h5, h4, h3, h2, a {\r\n      color: #1e252f;\r\n    }\r\n\r\n    code {\r\n      color: #c7254e !important;\r\n    }\r\n  }\r\n\r\n  .cb-doc-card {\r\n    margin-bottom: 15px;\r\n    border: solid 1px #dedede;\r\n    border-radius: 10px;\r\n    transition: 0.2s all ease;\r\n  }\r\n\r\n  .float-right {\r\n    float: right;\r\n  }\r\n\r\n  .card-shadow:hover {\r\n    -webkit-box-shadow: 0 5px 20px 0 rgba(0, 0, 0, 0.1);\r\n\r\n    /* Safari, Android, iOS */\r\n    -moz-box-shadow: 0 5px 20px 0 rgba(0, 0, 0, 0.1);\r\n\r\n    /* Firefox */\r\n    box-shadow: 0 5px 20px 0 rgba(0, 0, 0, 0.1);\r\n    border: none;\r\n    transform: scale(1.1) translateZ(0);\r\n  }\r\n}\r\n\r\n@media all and (max-width: 768px) {\r\n  .cb-doc-listing .card .card-body {\r\n    padding: 10px 15px 10px 15px;\r\n  }\r\n}\r\n\r\n.cb .section-hero {\r\n\r\n  .container {\r\n    margin-bottom: 20px;\r\n  }\r\n\r\n  h1 {\r\n    font-size: 40px;\r\n    letter-spacing: 1.13px;\r\n    margin-bottom: 11px;\r\n  }\r\n\r\n  p {\r\n    font-size: 20px;\r\n    font-weight: inherit;\r\n    line-height: 1.5;\r\n    letter-spacing: 0.56px;\r\n  }\r\n}\r\n\r\n.section-hero-blue {\r\n  height: 409px;\r\n  color: #fff;\r\n\r\n  label {\r\n    margin-bottom: 29px;\r\n    font-size: 35px;\r\n    line-height: normal;\r\n    letter-spacing: 0.98px;\r\n    text-align: center;\r\n    color: #ffffff;\r\n  }\r\n\r\n  img {\r\n    margin: -1%;\r\n    position: relative;\r\n    height: 231px;\r\n    width: 112px;\r\n    margin-bottom: 23px;\r\n  }\r\n\r\n  a {\r\n    padding: 14px 39px;\r\n    margin-bottom: 49px;\r\n    color: #08a9fb;\r\n    font-size: 14px;\r\n    background-color: white;\r\n\r\n    &:active, &:hover {\r\n      color: #08a9fb;\r\n      cursor: pointer;\r\n      transform: none;\r\n      box-shadow: none;\r\n      outline: none !important;\r\n    }\r\n  }\r\n}\r\n\r\n.searchrow {\r\n  margin-top: 20px;\r\n  padding: 0;\r\n\r\n  .input-group {\r\n    width: 100%;\r\n    padding-right: 19px;\r\n    z-index: 0;\r\n  }\r\n\r\n  button {\r\n    border-bottom-left-radius: 8px;\r\n    border-top-left-radius: 8px;\r\n    min-height: 56px;\r\n    height: 56px;\r\n\r\n    &:hover, &:focus {\r\n      transform: none;\r\n      box-shadow: none;\r\n      outline: none !important;\r\n    }\r\n  }\r\n\r\n  input {\r\n    background: #f5f9fa;\r\n    border-top-right-radius: 8px;\r\n    border-bottom-right-radius: 8px;\r\n    min-height: 56px;\r\n    height: 56px;\r\n    border: none;\r\n    color: #1e252f;\r\n    transform: none;\r\n    box-shadow: none;\r\n    outline: none !important;\r\n\r\n    &:active, &:focus {\r\n      transform: none;\r\n      box-shadow: none;\r\n      outline: none !important;\r\n    }\r\n  }\r\n}\r\n\r\n.cb-input-box {\r\n  &::-webkit-input-placeholder, &::-moz-placeholder, &::-ms-placeholder, &::placeholder {\r\n    font-size: 14px;\r\n    font-style: normal;\r\n    opacity: 1 !important;\r\n    font-weight: 600;\r\n    letter-spacing: -0.23px;\r\n    color: #0000;\r\n  }\r\n}\r\n\r\n.cloud-filter {\r\n  display: flex;\r\n  justify-content: center;\r\n  margin-bottom: 11px;\r\n\r\n  .filter {\r\n    width: 109px;\r\n    height: 76px;\r\n    cursor: pointer;\r\n    float: left;\r\n    border-radius: 8px;\r\n    border: none;\r\n    background-color: #f5f9fa;\r\n    text-align: center;\r\n\r\n    &:hover, &:active {\r\n      background-color: #fff !important;\r\n      position: relative;\r\n      border-radius: 8px !important;\r\n      box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);\r\n      cursor: pointer;\r\n      border: 1px solid #dedede !important;\r\n      outline-color: #fff;\r\n    }\r\n\r\n    img {\r\n      vertical-align: middle;\r\n      max-height: 55px;\r\n      padding: 8px;\r\n    }\r\n\r\n    .helper {\r\n      display: inline-block;\r\n      height: 100%;\r\n      vertical-align: middle;\r\n    }\r\n\r\n    &:nth-last-of-type(2) {\r\n      border-radius: 0;\r\n    }\r\n\r\n    &:last-child {\r\n      border-top-left-radius: 0;\r\n      border-bottom-left-radius: 0;\r\n    }\r\n\r\n    &:first-child {\r\n      border-top-right-radius: 0;\r\n      border-bottom-right-radius: 0;\r\n    }\r\n  }\r\n}\r\n\r\n.no-azure-results {\r\n  display: none;\r\n\r\n  h2 {\r\n    color: #1e252f;\r\n    font-size: 30px;\r\n    line-height: normal;\r\n    letter-spacing: 0.84px;\r\n    margin-bottom: 8px;\r\n    margin-top: 91px;\r\n\r\n    &:hover > a.anchorjs-link {\r\n      color: #FFFFFF;\r\n      text-decoration: none;\r\n      display: none;\r\n    }\r\n  }\r\n\r\n  p {\r\n    font-size: 20px;\r\n    opacity: 0.7;\r\n    line-height: 1.5;\r\n    letter-spacing: 0.56px;\r\n    color: #000000;\r\n    margin-bottom: 24px;\r\n  }\r\n\r\n  .btn {\r\n    border-radius: 23.5px;\r\n    background-image: linear-gradient(to bottom, #5cbde7, #569cea);\r\n\r\n    &:hover, &:active {\r\n      outline: none;\r\n      transform: none;\r\n      box-shadow: none;\r\n      border-radius: 23.5px;\r\n      background-image: linear-gradient(to bottom, #5cbde7, #569cea);\r\n    }\r\n  }\r\n}\r\n\r\n#tags-filter {\r\n  display: flex;\r\n  justify-content: flex-end;\r\n\r\n  button {\r\n    border-radius: 8px;\r\n    background: rgba(0, 185, 248, 0.2);\r\n    padding: 0 15px;\r\n    box-shadow: none;\r\n    border: none;\r\n    font-size: 18px;\r\n    font-weight: 600;\r\n    letter-spacing: 0.51px;\r\n    text-align: center;\r\n    color: #06a6fd;\r\n\r\n    &:hover, &:focus, &:active {\r\n      transform: none;\r\n      box-shadow: none;\r\n      outline: none !important;\r\n      background: rgba(0, 185, 248, 0.1);\r\n      color: #06a6fd;\r\n    }\r\n  }\r\n}\r\n\r\n.filter-options {\r\n  padding: 0;\r\n  margin: 16px;\r\n\r\n  .card {\r\n    padding: 30px 109px;\r\n    border-radius: 8px;\r\n    background-color: #f5f9fa;\r\n    min-height: 193px;\r\n    max-height: 423px;\r\n\r\n    .tags {\r\n      display: flex;\r\n      flex-wrap: wrap;\r\n      flex-direction: column;\r\n      max-height: 333px;\r\n    }\r\n\r\n    .checkbox {\r\n      padding-left: 20px;\r\n      position: relative;\r\n      display: block;\r\n      margin-bottom: 0;\r\n\r\n      input {\r\n        opacity: 0;\r\n        position: absolute;\r\n        z-index: 1;\r\n        cursor: pointer;\r\n        margin-left: -20px;\r\n\r\n        &:checked + label {\r\n          &::before {\r\n            border: 2px solid #06a6fd;\r\n          }\r\n\r\n          &::after {\r\n            content: '';\r\n            display: inline-block;\r\n            position: absolute;\r\n            width: 13px;\r\n            height: 13px;\r\n            left: 2px;\r\n            top: 4px;\r\n            margin-left: -18px;\r\n            border: 2px solid #06a6fd;\r\n            border-radius: 2px;\r\n            background-color: #06a6fd;\r\n          }\r\n        }\r\n      }\r\n\r\n      label {\r\n        display: inline-block;\r\n        position: relative;\r\n        padding-left: 10px;\r\n\r\n        &::before {\r\n          content: '';\r\n          display: inline-block;\r\n          position: absolute;\r\n          width: 18px;\r\n          height: 18px;\r\n          border-radius: 2px;\r\n          border: 2px solid #939596;\r\n          background-color: #fff;\r\n          left: -18px;\r\n        }\r\n      }\r\n\r\n      + .checkbox {\r\n        margin-top: 10px !important;\r\n      }\r\n\r\n      input:checked + label {\r\n        &::before {\r\n          background-color: #06a6fd;\r\n        }\r\n\r\n        &::after {\r\n          content: '';\r\n          position: absolute;\r\n          width: 10px;\r\n          height: 7px;\r\n          background: #06a6fd;\r\n          left: 4px;\r\n          border: 2px solid #fff;\r\n          border-top: none;\r\n          border-right: none;\r\n          -webkit-transform: rotate(-45deg);\r\n          -moz-transform: rotate(-45deg);\r\n          -o-transform: rotate(-45deg);\r\n          -ms-transform: rotate(-45deg);\r\n          transform: rotate(-45deg);\r\n        }\r\n      }\r\n\r\n      label::before {\r\n        border-radius: 2px;\r\n      }\r\n    }\r\n\r\n    p {\r\n      letter-spacing: -0.2px;\r\n      color: #8f9fa7;\r\n      text-transform: uppercase;\r\n      font-size: 14px;\r\n      font-weight: bold;\r\n      margin-bottom: 2px;\r\n    }\r\n\r\n    label {\r\n      font-size: 16px;\r\n      font-weight: 600;\r\n      letter-spacing: -0.23px;\r\n      color: #939596;\r\n      text-transform: capitalize;\r\n      line-height: 1.3em;\r\n    }\r\n  }\r\n}\r\n\r\n.fixed {\r\n  position: fixed;\r\n  top: 0;\r\n}\r\n\r\n.bottom {\r\n  position: absolute;\r\n  bottom: 0;\r\n  top: auto;\r\n}\r\n\r\n.row.equal {\r\n  display: flex;\r\n  display: -webkit-flex;\r\n  flex-wrap: wrap;\r\n}\r\n\r\n#toc .sectlevel1 .expanded ul {\r\n  display: block;\r\n  font-size: 14px;\r\n  font-weight: normal;\r\n}\r\n\r\n.no-results-space {\r\n  &:after {\r\n    margin-bottom: 40%;\r\n  }\r\n\r\n  width: 100%;\r\n}\r\n\r\n@media (max-width: 991px) {\r\n  .cb .section-hero {\r\n    padding-top: 0;\r\n  }\r\n\r\n  .filter-options .card {\r\n    padding: 20px 40px;\r\n  }\r\n\r\n  .categories {\r\n    display: none !important;\r\n  }\r\n\r\n  .cb-doc-listing .card-shadow {\r\n    &:hover, &:active {\r\n      transform: scale(1.05) translateZ(0);\r\n    }\r\n\r\n    .card img {\r\n      padding: 11px;\r\n    }\r\n  }\r\n\r\n  .breadcrumb {\r\n    margin-bottom: 0;\r\n  }\r\n}\r\n\r\n@media only screen and (max-width: 767px) {\r\n  .cb .section-hero {\r\n    h1 {\r\n      font-size: 29px;\r\n    }\r\n\r\n    p {\r\n      font-size: 14px;\r\n    }\r\n  }\r\n\r\n  .post-detail {\r\n    h1 {\r\n      font-size: 29px;\r\n    }\r\n\r\n    .post-title {\r\n      padding: 0 !important;\r\n    }\r\n  }\r\n\r\n  .post-content {\r\n    h2 {\r\n      margin: 0 !important;\r\n      font-size: 28px !important;\r\n    }\r\n\r\n    h3 {\r\n      font-size: 22px !important;\r\n    }\r\n  }\r\n\r\n  .searchrow, .filter-options {\r\n    display: none !important;\r\n  }\r\n\r\n  .cloud-filter .filter {\r\n    height: 60px;\r\n    width: 90px;\r\n  }\r\n\r\n  #listings {\r\n    display: flex;\r\n    justify-content: center;\r\n  }\r\n\r\n  .cb-doc-listing {\r\n    .category-head {\r\n      font-size: 20px;\r\n    }\r\n\r\n    .card .card-title {\r\n      font-size: 16px;\r\n    }\r\n  }\r\n\r\n  .card-shadow:hover {\r\n    -webkit-box-shadow: 0 5px 20px 0 rgba(0, 0, 0, 0.1);\r\n\r\n    /* Safari, Android, iOS */\r\n    -moz-box-shadow: 0 5px 20px 0 rgba(0, 0, 0, 0.1);\r\n\r\n    /* Firefox */\r\n    box-shadow: 0 5px 20px 0 rgba(0, 0, 0, 0.1);\r\n    border: none;\r\n  }\r\n\r\n  .cb-newsletter {\r\n    padding: 0 !important;\r\n\r\n    label {\r\n      font-size: 22px !important;\r\n    }\r\n\r\n    p {\r\n      font-size: 16px;\r\n    }\r\n\r\n    .btn {\r\n      padding: 6px 20px !important;\r\n    }\r\n  }\r\n\r\n  .section-hero-blue {\r\n    label {\r\n      font-size: 25px;\r\n    }\r\n\r\n    a {\r\n      padding: 6px 20px;\r\n    }\r\n  }\r\n\r\n  p, pre {\r\n    font-size: 14px;\r\n  }\r\n\r\n  code {\r\n    font-size: 14px;\r\n\r\n    &[class*=\"language-\"] {\r\n      font-size: 14px;\r\n    }\r\n  }\r\n\r\n  pre[class*=\"language-\"] {\r\n    font-size: 14px;\r\n  }\r\n}\r\n\r\n.cb-newsletter, .no-results {\r\n  padding: 5em 6em;\r\n  font-size: 16px;\r\n  color: #1e252f;\r\n  display: inline-block;\r\n}\r\n\r\n.cb-newsletter .modal-box, .no-results .modal-box {\r\n  padding-bottom: 3em;\r\n}\r\n\r\n.cb-newsletter .cb-input-box, .no-results .cb-input-box {\r\n  background-color: #f5f9fa;\r\n  border: none;\r\n  border-top-left-radius: 20px;\r\n  border-bottom-left-radius: 20px;\r\n  font-size: 16px;\r\n  color: black;\r\n  height: 42px;\r\n  z-index: 1;\r\n  overflow: hidden;\r\n  padding-right: 16px;\r\n  box-shadow: none;\r\n}\r\n\r\n.cb-newsletter .cb-input-box:hover, .no-results .cb-input-box:hover, .cb-newsletter .cb-input-box:active, .no-results .cb-input-box:active, .cb-newsletter .cb-input-box:focus, .no-results .cb-input-box:focus {\r\n  border: 1px solid #07a7fc;\r\n  z-index: 1 !important;\r\n  padding: 12px;\r\n  transform: none;\r\n  box-shadow: none;\r\n  outline: none !important;\r\n}\r\n\r\n.cb-newsletter .cb-input-box:invalid, .no-results .cb-input-box:invalid {\r\n  border: 1px solid #ef5a73;\r\n}\r\n\r\n.cb-newsletter label, .no-results label {\r\n  font-size: 28px;\r\n  line-height: 40px;\r\n  letter-spacing: 0.79px;\r\n  font-weight: 600;\r\n  color: #1e252f;\r\n}\r\n\r\n.cb-newsletter img, .no-results img {\r\n  margin-bottom: 2%;\r\n}\r\n\r\n.cb-newsletter .btn, .no-results .btn {\r\n  text-decoration: none;\r\n  background: #07a7fc;\r\n  font-size: 15px;\r\n  margin-top: -2px;\r\n  border-radius: 25px !important;\r\n  margin-left: -13px;\r\n  padding: 11px 24px;\r\n  position: relative;\r\n  z-index: 2;\r\n}\r\n\r\n.cb-newsletter .btn:hover, .no-results .btn:hover, .cb-newsletter .btn:focus, .no-results .btn:focus {\r\n  background: #06a6fd;\r\n  transform: none;\r\n  box-shadow: none;\r\n  outline: none !important;\r\n}\r\n\r\n@media (min-width: 768px) {\r\n  #newsletter-success .modal-dialog {\r\n    margin: 30px auto;\r\n    width: auto;\r\n  }\r\n}\r\n\r\n#newsletter-success {\r\n  label {\r\n    font-size: 28px;\r\n    font-weight: 600;\r\n    line-height: 1.43;\r\n    letter-spacing: 0.79px;\r\n    color: #1e252f;\r\n    margin-bottom: 10px;\r\n  }\r\n\r\n  p {\r\n    color: #1e252f;\r\n    font-size: 16px;\r\n    line-height: 22px;\r\n    letter-spacing: 0.45px;\r\n  }\r\n\r\n  .modal-content {\r\n    background: #fff;\r\n    max-width: 468px;\r\n    height: 320px;\r\n    border-radius: 8px;\r\n\r\n    .modal-body {\r\n      padding: 50px 39px;\r\n\r\n      .img {\r\n        margin-bottom: 19px;\r\n      }\r\n    }\r\n  }\r\n\r\n  .btn {\r\n    width: 131px;\r\n    height: 47px;\r\n    border-radius: 23.5px;\r\n    background-color: #06a6fd;\r\n    color: #ffffff;\r\n    margin-top: 20px;\r\n\r\n    &:hover, &:focus {\r\n      background: #06a6fd;\r\n      outline: none;\r\n      transform: none;\r\n      box-shadow: none;\r\n      outline: none !important;\r\n    }\r\n  }\r\n}\r\n\r\n.cb-cta-card {\r\n  background-color: white;\r\n  color: black;\r\n  box-shadow: 3px 4px #c5c2c2;\r\n  border-radius: 5px;\r\n  padding: 0 24px 0;\r\n  margin-top: 10px;\r\n\r\n  p {\r\n    color: black;\r\n    font-size: 14px;\r\n    margin-top: -20%;\r\n  }\r\n\r\n  .deploy {\r\n    margin-top: 6%;\r\n    width: 100%;\r\n    border-radius: 6px;\r\n    font-size: 14px;\r\n    background-color: #06a6fd;\r\n    color: white !important;\r\n  }\r\n\r\n  .dismiss-cta {\r\n    color: #06a6fd;\r\n\r\n    &:hover, &:focus {\r\n      text-decoration: none;\r\n      transform: none;\r\n      box-shadow: none;\r\n      outline: none !important;\r\n    }\r\n  }\r\n\r\n  img {\r\n    max-width: 100%;\r\n  }\r\n}\r\n\r\n@media (max-width: 992px) {\r\n  .cb-cta-card {\r\n    display: none;\r\n    clear: both;\r\n  }\r\n}\r\n\r\n.cb-section-white {\r\n  background-color: #fff;\r\n  position: relative;\r\n  color: #1e252f;\r\n  padding: 20px 25px 0 25px;\r\n  font-family: 'Open Sans', sans-serif;\r\n  min-height: 400px;\r\n\r\n  .container-fluid {\r\n    max-width: 1200px;\r\n  }\r\n}\r\n\r\n.img-center {\r\n  max-width: 184px;\r\n  max-height: 184px;\r\n  padding: 15px;\r\n  display: block;\r\n  margin: 0 auto;\r\n}\r\n\r\n.post-bg {\r\n  background-color: #f6f8fa;\r\n}\r\n\r\n.post-detail {\r\n  h1 {\r\n    margin: 20px auto;\r\n    font-size: 35px;\r\n    line-height: normal;\r\n    letter-spacing: 0.98px;\r\n    text-align: center;\r\n    color: #1e252f;\r\n  }\r\n\r\n  .post-title-image {\r\n    padding: 0px;\r\n    max-height: 110px;\r\n  }\r\n\r\n  .post-title {\r\n    color: #000000;\r\n    padding: 1em 2em 0 2em;\r\n    letter-spacing: 0.98px;\r\n    font-size: 35px;\r\n    line-height: normal;\r\n    text-align: center;\r\n    max-width: 760px;\r\n    margin: 0 auto;\r\n\r\n    p {\r\n      font-weight: 600;\r\n      margin-bottom: 0px;\r\n      opacity: 0.5;\r\n      font-size: 14px;\r\n      letter-spacing: 0.39px;\r\n    }\r\n\r\n    h1, h2, h3, h4, h5, h6 {\r\n      color: #1e252f;\r\n    }\r\n  }\r\n\r\n  .breadcrumb {\r\n    font-size: 14px;\r\n    margin-bottom: 0;\r\n    letter-spacing: -0.2px;\r\n    background-color: white;\r\n\r\n    > a {\r\n      color: #06a6fd;\r\n      text-decoration: none;\r\n\r\n      &:last-child span {\r\n        border-bottom: 1px solid #06a6fd;\r\n        text-shadow: 0.5px 0 0;\r\n\r\n        &:hover {\r\n          border-bottom: 2px solid #06a6fd;\r\n          text-shadow: 0.5px 0 0;\r\n        }\r\n      }\r\n\r\n      &:first-child {\r\n        &:hover, &:active {\r\n          border-bottom: 2px solid #06a6fd;\r\n          text-shadow: 0.5px 0 0;\r\n        }\r\n      }\r\n\r\n      span {\r\n        &:hover, &:active {\r\n          border-bottom: 2px solid #06a6fd;\r\n          text-shadow: 0.5px 0 0;\r\n        }\r\n      }\r\n    }\r\n  }\r\n\r\n  h2 {\r\n    border-bottom: solid 1px #07a7fc;\r\n  }\r\n}\r\n\r\n.post-content {\r\n  padding: 0 0 50px 0;\r\n  color: #1e252f;\r\n  line-height: 1.38;\r\n  max-width: 760px;\r\n  margin: 0 auto;\r\n\r\n  .imageblock > .title {\r\n    font-size: 12px;\r\n    margin: 5px 0 15px 0;\r\n    color: rgba(0, 0, 0, 0.54);\r\n    text-align: center;\r\n  }\r\n\r\n  .sect2 {\r\n    .paragraph, dd {\r\n      margin-bottom: 10px;\r\n    }\r\n\r\n    dl {\r\n      margin: 0;\r\n    }\r\n\r\n    .listingblock {\r\n      margin: 20px 0;\r\n\r\n      &.title {\r\n        font-size: 12px;\r\n        margin: 15px 0 0 0;\r\n        color: rgba(0, 0, 0, 0.54);\r\n        text-align: left;\r\n      }\r\n    }\r\n\r\n    .hdlist1 a {\r\n      text-decoration: none;\r\n      color: #1e252f;\r\n    }\r\n\r\n    .dlist, .ulist {\r\n      padding: 0;\r\n      margin: 20px 0;\r\n    }\r\n\r\n    .dlist dt, .ulist dt {\r\n      margin-top: 15px;\r\n    }\r\n\r\n    pre {\r\n      &.highlight > code, &:not(.highlight) {\r\n        padding: 15px;\r\n        background: rgba(245, 245, 245, 0.3);\r\n        border: 1px solid #efefef;\r\n      }\r\n    }\r\n\r\n    .token {\r\n      &.operator, &.entity, &.url, &.variable {\r\n        background: transparent !important;\r\n      }\r\n    }\r\n  }\r\n\r\n  p, li {\r\n    font-size: 16px;\r\n    letter-spacing: 0.45px;\r\n    margin: 0;\r\n  }\r\n\r\n  p em, li em {\r\n    font-style: italic;\r\n  }\r\n\r\n  p code, li code {\r\n    background-color: #e6f6fe;\r\n    color: #000;\r\n  }\r\n\r\n  p > {\r\n    a, em > a {\r\n      color: #06a6fd;\r\n      text-decoration: none;\r\n      font-weight: bold;\r\n    }\r\n  }\r\n\r\n  img {\r\n    max-width: 100%;\r\n    padding-top: 10px;\r\n    display: block;\r\n    margin: 0 auto;\r\n\r\n    &:hover {\r\n      cursor: zoom-in;\r\n    }\r\n  }\r\n\r\n  pre {\r\n    padding: 0;\r\n    border: none;\r\n    font-size: 14px;\r\n    overflow-y: scroll;\r\n    text-overflow: initial;\r\n    max-height: 100vh;\r\n    margin: 10px 0;\r\n\r\n    &[class*=\"language-\"] {\r\n      padding: 0;\r\n      border: none;\r\n      font-size: 14px;\r\n      overflow-y: scroll;\r\n      text-overflow: initial;\r\n      max-height: 100vh;\r\n      margin: 10px 0;\r\n    }\r\n  }\r\n\r\n  h2 {\r\n    font-size: 38px;\r\n    line-height: 1.43;\r\n    letter-spacing: 0.79px;\r\n    margin: 70px 0 28px 0;\r\n  }\r\n\r\n  h3 {\r\n    font-size: 28px;\r\n    line-height: 1.43;\r\n    letter-spacing: 0.79px;\r\n    margin: 40px 0 18px 0;\r\n  }\r\n\r\n  h1, h2, h3, h4, h5, h6 {\r\n    color: #1e252f;\r\n  }\r\n\r\n  h1 a:link, h2 a:link, h3 a:link, h4 a:link, h5 a:link, h6 a:link {\r\n    color: #1e252f;\r\n    font-size: .9em !important;\r\n    margin-left: -0.8em !important;\r\n    padding-right: 0.1em !important;\r\n    transform: none;\r\n    box-shadow: none;\r\n    outline: none !important;\r\n  }\r\n\r\n  h1:hover > a.anchorjs-link, h5:hover > a.anchorjs-link, h6:hover > a.anchorjs-link {\r\n    color: #FFFFFF;\r\n    text-decoration: none;\r\n    display: none;\r\n  }\r\n\r\n  table {\r\n    font-size: 12px;\r\n    letter-spacing: 0.34px;\r\n    color: #1e252f;\r\n\r\n    caption {\r\n      color: #1e252f;\r\n    }\r\n\r\n    thead {\r\n      font-weight: 600;\r\n\r\n      th {\r\n        padding: 7px 0;\r\n      }\r\n    }\r\n\r\n    tbody {\r\n      background-color: #f5f9fa;\r\n\r\n      tr {\r\n        border-bottom: 15px solid #fff;\r\n\r\n        td {\r\n          padding: 5px;\r\n\r\n          p {\r\n            font-size: 12px;\r\n            letter-spacing: 0.34px;\r\n            padding: 10px;\r\n          }\r\n\r\n          &:nth-child(even) {\r\n            background-color: #fafcfc;\r\n          }\r\n        }\r\n      }\r\n    }\r\n\r\n    .tableblock {\r\n      line-height: 20px;\r\n    }\r\n  }\r\n}\r\n\r\n.admonitionblock-content {\r\n  position: relative;\r\n  padding: 19px;\r\n  border-top: 9px solid #06a6fd;\r\n  background-color: #f5f9fa;\r\n  text-align: center;\r\n  margin: 40px 0;\r\n\r\n  .title {\r\n    font-weight: bold;\r\n    margin-bottom: 0 !important;\r\n  }\r\n\r\n  table {\r\n    text-align: center;\r\n    margin-left: auto;\r\n    margin-right: auto;\r\n\r\n    tbody tr {\r\n      border-bottom: none !important;\r\n\r\n      .icon .title {\r\n        margin-right: 40px;\r\n        position: relative;\r\n        font-size: 0;\r\n        text-align: center;\r\n\r\n        &:after {\r\n          display: block;\r\n          position: absolute;\r\n          color: #fff;\r\n          content: \"!\";\r\n          border-radius: 50%;\r\n          width: 21px;\r\n          height: 21px;\r\n          line-height: 21px;\r\n          font-weight: normal;\r\n          left: 0;\r\n          font-size: 16px;\r\n          margin-top: -9px;\r\n        }\r\n      }\r\n\r\n      a {\r\n        color: #07a7fc;\r\n        text-decoration: none;\r\n        font-weight: 600;\r\n      }\r\n\r\n      td {\r\n        text-align: initial;\r\n        font-size: 12px;\r\n        line-height: normal;\r\n        letter-spacing: 0.34px;\r\n        color: #1e252f;\r\n        padding: 0;\r\n\r\n        p {\r\n          text-align: initial;\r\n          font-size: 12px;\r\n          line-height: normal;\r\n          letter-spacing: 0.34px;\r\n          color: #1e252f;\r\n          padding: 0;\r\n        }\r\n\r\n        .js-subscribe-cta, p .js-subscribe-cta {\r\n          color: #07a7fc;\r\n          text-decoration: none;\r\n          font-weight: 600;\r\n        }\r\n\r\n        .js-subscribe-cta:hover, p .js-subscribe-cta:hover {\r\n          cursor: pointer;\r\n          text-decoration: underline;\r\n        }\r\n\r\n        .title, p .title {\r\n          margin-bottom: 12px;\r\n        }\r\n\r\n        code, p code {\r\n          background: #e5ecee !important;\r\n          color: #1e252f;\r\n          font-family: AndaleMono;\r\n          font-weight: normal;\r\n          font-size: 12px;\r\n        }\r\n\r\n        &:nth-child(even), p:nth-child(even) {\r\n          background-color: inherit !important;\r\n        }\r\n      }\r\n    }\r\n  }\r\n}\r\n\r\n.admonitionblock {\r\n  &.important, &.note {\r\n    position: relative;\r\n    padding: 19px;\r\n    border-top: 9px solid #06a6fd;\r\n    background-color: #f5f9fa;\r\n    text-align: center;\r\n    margin: 40px 0;\r\n  }\r\n\r\n  &.important .title, &.note .title {\r\n    font-weight: bold;\r\n    margin-bottom: 0 !important;\r\n  }\r\n\r\n  &.important table, &.note table {\r\n    text-align: center;\r\n    margin-left: auto;\r\n    margin-right: auto;\r\n  }\r\n\r\n  &.important table tbody tr, &.note table tbody tr {\r\n    border-bottom: none !important;\r\n  }\r\n\r\n  &.important table tbody tr .icon .title, &.note table tbody tr .icon .title {\r\n    margin-right: 40px;\r\n    position: relative;\r\n    font-size: 0;\r\n    text-align: center;\r\n  }\r\n\r\n  &.important table tbody tr .icon .title:after, &.note table tbody tr .icon .title:after {\r\n    display: block;\r\n    position: absolute;\r\n    color: #fff;\r\n    content: \"!\";\r\n    border-radius: 50%;\r\n    width: 21px;\r\n    height: 21px;\r\n    line-height: 21px;\r\n    font-weight: normal;\r\n    left: 0;\r\n    font-size: 16px;\r\n    margin-top: -9px;\r\n  }\r\n\r\n  &.important table tbody tr a, &.note table tbody tr a {\r\n    color: #07a7fc;\r\n    text-decoration: none;\r\n    font-weight: 600;\r\n  }\r\n\r\n  &.important table tbody tr td, &.note table tbody tr td, &.important table tbody tr td p, &.note table tbody tr td p {\r\n    text-align: initial;\r\n    font-size: 12px;\r\n    line-height: normal;\r\n    letter-spacing: 0.34px;\r\n    color: #1e252f;\r\n    padding: 0;\r\n  }\r\n\r\n  &.important table tbody tr td .js-subscribe-cta, &.note table tbody tr td .js-subscribe-cta, &.important table tbody tr td p .js-subscribe-cta, &.note table tbody tr td p .js-subscribe-cta {\r\n    color: #07a7fc;\r\n    text-decoration: none;\r\n    font-weight: 600;\r\n  }\r\n\r\n  &.important table tbody tr td .js-subscribe-cta:hover, &.note table tbody tr td .js-subscribe-cta:hover, &.important table tbody tr td p .js-subscribe-cta:hover, &.note table tbody tr td p .js-subscribe-cta:hover {\r\n    cursor: pointer;\r\n    text-decoration: underline;\r\n  }\r\n\r\n  &.important table tbody tr td .title, &.note table tbody tr td .title, &.important table tbody tr td p .title, &.note table tbody tr td p .title {\r\n    margin-bottom: 12px;\r\n  }\r\n\r\n  &.important table tbody tr td code, &.note table tbody tr td code, &.important table tbody tr td p code, &.note table tbody tr td p code {\r\n    background: #e5ecee !important;\r\n    color: #1e252f;\r\n    font-family: AndaleMono;\r\n    font-weight: normal;\r\n    font-size: 12px;\r\n  }\r\n\r\n  &.important table tbody tr td:nth-child(even), &.note table tbody tr td:nth-child(even), &.important table tbody tr td p:nth-child(even), &.note table tbody tr td p:nth-child(even) {\r\n    background-color: inherit !important;\r\n  }\r\n\r\n  &.important .title:after, &.note .title:after {\r\n    background-color: #06a6fd;\r\n    border: 1px solid #06a6fd;\r\n  }\r\n\r\n  &.error {\r\n    position: relative;\r\n    padding: 19px;\r\n    border-top: 9px solid #06a6fd;\r\n    background-color: #f5f9fa;\r\n    text-align: center;\r\n    margin: 40px 0;\r\n\r\n    .title {\r\n      font-weight: bold;\r\n      margin-bottom: 0 !important;\r\n    }\r\n\r\n    table {\r\n      text-align: center;\r\n      margin-left: auto;\r\n      margin-right: auto;\r\n\r\n      tbody tr {\r\n        border-bottom: none !important;\r\n\r\n        .icon .title {\r\n          margin-right: 40px;\r\n          position: relative;\r\n          font-size: 0;\r\n          text-align: center;\r\n\r\n          &:after {\r\n            display: block;\r\n            position: absolute;\r\n            color: #fff;\r\n            content: \"!\";\r\n            border-radius: 50%;\r\n            width: 21px;\r\n            height: 21px;\r\n            line-height: 21px;\r\n            font-weight: normal;\r\n            left: 0;\r\n            font-size: 16px;\r\n            margin-top: -9px;\r\n          }\r\n        }\r\n\r\n        a {\r\n          color: #07a7fc;\r\n          text-decoration: none;\r\n          font-weight: 600;\r\n        }\r\n\r\n        td {\r\n          text-align: initial;\r\n          font-size: 12px;\r\n          line-height: normal;\r\n          letter-spacing: 0.34px;\r\n          color: #1e252f;\r\n          padding: 0;\r\n\r\n          p {\r\n            text-align: initial;\r\n            font-size: 12px;\r\n            line-height: normal;\r\n            letter-spacing: 0.34px;\r\n            color: #1e252f;\r\n            padding: 0;\r\n          }\r\n\r\n          .js-subscribe-cta, p .js-subscribe-cta {\r\n            color: #07a7fc;\r\n            text-decoration: none;\r\n            font-weight: 600;\r\n          }\r\n\r\n          .js-subscribe-cta:hover, p .js-subscribe-cta:hover {\r\n            cursor: pointer;\r\n            text-decoration: underline;\r\n          }\r\n\r\n          .title, p .title {\r\n            margin-bottom: 12px;\r\n          }\r\n\r\n          code, p code {\r\n            background: #e5ecee !important;\r\n            color: #1e252f;\r\n            font-family: AndaleMono;\r\n            font-weight: normal;\r\n            font-size: 12px;\r\n          }\r\n\r\n          &:nth-child(even), p:nth-child(even) {\r\n            background-color: inherit !important;\r\n          }\r\n        }\r\n      }\r\n    }\r\n\r\n    .title:after {\r\n      background-color: #ef5a73;\r\n      border: 1px solid #ef5a73;\r\n    }\r\n  }\r\n}\r\n\r\n.center {\r\n  display: flex;\r\n  justify-content: center;\r\n}\r\n\r\n.white-section {\r\n  background-color: #fff;\r\n  padding: 20px 25px 0 25px;\r\n  min-height: 200px;\r\n}\r\n\r\n.comments {\r\n  padding: 50px;\r\n\r\n  h2 {\r\n    font-size: 28px;\r\n    font-style: normal;\r\n    font-stretch: normal;\r\n    line-height: 1.43;\r\n    letter-spacing: 0.79px;\r\n    text-align: center;\r\n    color: #1e252f;\r\n    padding: 5px 7px;\r\n    margin: 0;\r\n    border-bottom: 2px solid #06a6fd;\r\n  }\r\n\r\n  .sm-icon {\r\n    color: #fff;\r\n    border-radius: 50%;\r\n    width: 18px;\r\n    height: 18px;\r\n    font-size: 14px;\r\n    background-color: #06a6fd;\r\n    margin: 20px 8px 0 0;\r\n    text-align: center;\r\n    padding: 2px;\r\n  }\r\n\r\n  .share-buttons ul {\r\n    padding: 0;\r\n    display: flex;\r\n    list-style-type: none;\r\n\r\n    li {\r\n      display: inline;\r\n\r\n      a {\r\n        text-decoration: none;\r\n      }\r\n    }\r\n  }\r\n\r\n  .utterances {\r\n    margin: 20px 0 0 0;\r\n\r\n    body {\r\n      background-color: black;\r\n    }\r\n\r\n    .timeline .comment-header {\r\n      background-color: #fff;\r\n      border-bottom: none;\r\n    }\r\n  }\r\n}\r\n\r\n.timeline-comment.current-user .comment-header {\r\n  background-color: black;\r\n}\r\n\r\n.subscribe-modal {\r\n  max-width: 600px;\r\n\r\n  .modal-content {\r\n    background: white;\r\n    display: flex;\r\n    flex-direction: column;\r\n    position: relative;\r\n    border: none;\r\n    -webkit-border-radius: 0px !important;\r\n    -moz-border-radius: 0px !important;\r\n    border-radius: 8px !important;\r\n\r\n    .modal-header {\r\n      background: linear-gradient(#7cd7ff, #07a7fc, #57a1ea);\r\n      position: relative;\r\n      display: block;\r\n      width: 100%;\r\n      max-height: inherit;\r\n      border-top-left-radius: 8px;\r\n      border-top-right-radius: 8px;\r\n\r\n      img {\r\n        width: 130px;\r\n        margin-bottom: -12px;\r\n      }\r\n\r\n      .close {\r\n        position: absolute;\r\n        padding: 0px 20px;\r\n        right: 0px;\r\n        font-size: 34px;\r\n        font-weight: 100;\r\n      }\r\n    }\r\n\r\n    .modal-body {\r\n      padding: 1px 60px;\r\n\r\n      h2, h3, p {\r\n        color: #1e252f;\r\n      }\r\n\r\n      h2 {\r\n        margin: 30px 0;\r\n        font-size: 28px;\r\n      }\r\n\r\n      h3 {\r\n        font-size: 20px;\r\n        line-height: 1.22;\r\n        letter-spacing: 0.51px;\r\n      }\r\n\r\n      p {\r\n        font-size: 14px;\r\n        line-height: 1.5;\r\n        letter-spacing: 0.39px;\r\n      }\r\n\r\n      a {\r\n        text-decoration: none;\r\n        font-weight: 600;\r\n        color: #06a6fd;\r\n      }\r\n\r\n      ul {\r\n        text-align: left;\r\n\r\n        li {\r\n          list-style-type: none;\r\n          position: relative;\r\n\r\n          &:before {\r\n            content: \"\";\r\n            background-color: transparent;\r\n            position: absolute;\r\n            left: -30px;\r\n            top: 0px;\r\n            width: 10px;\r\n            border-bottom: 4px solid #07a6fd;\r\n            height: 23px;\r\n            border-right: 4px solid #07a6fd;\r\n            transform: rotate(45deg);\r\n            -o-transform: rotate(45deg);\r\n            -ms-transform: rotate(45deg);\r\n            -webkit-transform: rotate(45deg);\r\n          }\r\n        }\r\n      }\r\n    }\r\n\r\n    .modal-footer {\r\n      text-align: center;\r\n      font-size: 14px;\r\n\r\n      .btn-primary {\r\n        background: linear-gradient(to bottom, #5cbde7, #569cea);\r\n        padding: 10px 27px;\r\n        border-radius: 23.5px;\r\n        height: 47px;\r\n        margin-bottom: 10px;\r\n      }\r\n\r\n      .btn-link {\r\n        letter-spacing: -0.2px;\r\n        color: #06a6fd;\r\n        font-size: 14px;\r\n        font-weight: 600;\r\n        margin-bottom: 10px;\r\n\r\n        &:hover {\r\n          transform: none;\r\n          box-shadow: none;\r\n          outline: none !important;\r\n        }\r\n      }\r\n\r\n      .btn-primary:hover {\r\n        transform: none;\r\n        box-shadow: none;\r\n        outline: none !important;\r\n      }\r\n    }\r\n  }\r\n}\r\n\r\n.cb-post-cta {\r\n  display: flex;\r\n  flex-direction: column;\r\n  justify-content: center;\r\n  align-items: center;\r\n  margin: 80px 0;\r\n  margin-bottom: 10px;\r\n\r\n  .title {\r\n    font-weight: bold;\r\n    color: $text-color;\r\n    font-size: $font-size-h2;\r\n    margin-bottom: 15px;\r\n  }\r\n}\r\n\r\n.doc-styled-table {\r\n  font-size: $font-size-sm;\r\n  th {\r\n    border-bottom: 1px solid $primary-color;\r\n  }\r\n  td {\r\n    padding: 3px;\r\n    border-bottom: 1px solid $gray-color-1;\r\n  }\r\n}\r\n"
  },
  {
    "path": "docs/assets/css/components.scss",
    "content": ".box-component {\n  position: relative;\n  z-index: 1;\n  max-width: 800px;\n  margin: auto;\n\n  .box {\n    max-width: 800px;\n    padding: 15px;\n    padding-top: 50px;\n    padding-bottom: 70px;\n    text-align: center;\n\n    h1, h2, h3, h4 {\n      text-align: center;\n      font-size: 44px;\n      line-height: 1.27;\n      margin-top: 10px;\n      margin-bottom: 10px;\n    }\n\n    .subtitle {\n      font-size: 22px;\n      margin-bottom: 30px;\n    }\n\n    .btn.btn-large {\n      min-width: 320px;\n    }\n  }\n}\n\n.links-section-cmp {\n  min-height: 200px;\n  width: 100%;\n  text-align: center;\n  padding-top: 90px;\n  padding-bottom: 90px;\n  margin-top: -80px;\n  position: relative;\n  display: flex;\n  flex-direction: column;\n\n  h2 {\n    font-weight: bold;\n    font-size: 34px;\n    position: relative;\n    z-index: 1;\n  }\n\n  .links {\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    width: 100%;\n    max-width: 980px;\n    padding: 0 30px;\n    margin: auto;\n    position: relative;\n    z-index: 1;\n\n    a {\n      margin-right: 25px;\n\n      &:last-child {\n        margin-right: 0px;\n      }\n    }\n  }\n\n  .links-shapes-left {\n    position: absolute;\n    left: 10vw;\n    top: -52px;\n    width: 30vw;\n    max-width: 330px;\n  }\n\n  .links-shapes-right {\n    position: absolute;\n    right: 0;\n    top: -40px;\n    width: 460px;\n    max-width: 460px;\n  }\n}\n\n.get-access-cmp{\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  justify-content: center;\n  padding-top:100px;\n  padding-bottom:100px;\n  text-align: center;\n  width: 100%;\n  span{\n    font-size:26px;\n    font-weight: 300;\n    padding: 40px;\n    max-width: 550px;\n  }\n}\n.links-n-get-access__section{\n  .links-section-cmp {\n    margin-top: 0;\n  }\n}\n.index-page .links-section-cmp {\n  padding-top: 90px;\n  padding-bottom: 90px;\n  margin-top: -80px;\n\n  .links-shapes-right {\n    position: absolute;\n    right: 0;\n    top: -40px;\n    width: 525px;\n  }\n}\n\n@media all and (max-width: 768px) {\n  .links-section-cmp {\n    padding-bottom: 60px;\n\n    .links {\n      flex-direction: column;\n\n      a {\n        margin: 10px auto;\n\n        &:last-child {\n          margin-right: auto;\n        }\n      }\n    }\n  }\n}\n\n.use-cases-cmp {\n  z-index: 1;\n  position: relative;\n}\n\n.subpage .subpage__main.subpage-404 {\n  min-height: 50vh;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n}\n\n//\n// COOKIES\n// ----------------------------------------------\n/* Elmo - The Grunty Cookie Policy Gatekeeper */\n#gruntyCookie {\n  background: #ffffff;\n  display: block;\n  color: #1e252f;\n  position: fixed;\n  bottom: 10px;\n  left: 50%;\n  width: 90vw;\n  transform: translateX(-50%);\n  margin: 0 auto;\n  max-width: 600px;\n  padding: 10px 25px;\n  -moz-box-sizing: border-box;\n  -webkit-box-sizing: border-box;\n  box-sizing: border-box;\n  z-index: 1112;\n  box-shadow: 0 8px 16px 0 rgba(34, 50, 84, 0.6);\n  border-radius: 8px;\n  text-align: center;\n\n  p {\n    margin: 0;\n    float: left;\n    padding: 15px 0 10px;\n    font-size: 18px;\n    line-height: 22px;\n\n    a {\n      &:link, &:visited, &:hover, &:active {\n        color: #5b4de5;\n        font-weight: bold;\n      }\n    }\n  }\n\n  #cookieModalClose {\n    float: right;\n    margin-left: 10px;\n    margin-top: 5px;\n  }\n}\n\n@media (max-width: 767px) {\n  #gruntyCookie {\n    width: 100%;\n    max-width: none;\n    border-radius: 0;\n    bottom: 0;\n    padding: 4px 10px;\n\n    p {\n      width: 78%;\n      display: inline-block;\n      text-align: left;\n    }\n\n    #cookieModalClose {\n      width: 20%;\n      display: inline-block;\n      float: none;\n      margin: 18px 0 10px;\n    }\n  }\n}\n\n.cookieModalCBeginAnimate {\n  -webkit-animation: gruntyCookieModal 0.5s;\n  animation: gruntyCookieModal 0.5s;\n}\n\n/* Chrome, Safari, Opera */\n@-webkit-keyframes gruntyCookieModal {\n  from {\n    opacity: 0;\n    transform: translate(0px, 40px);\n  }\n\n  to {\n    opacity: 1;\n    transform: translate(0px, 0px);\n  }\n}\n\n/* Standard syntax */\n@keyframes gruntyCookieModal {\n  from {\n    opacity: 0;\n    transform: translate(-50%, 40px);\n  }\n\n  to {\n    opacity: 1;\n    transform: translate(-50%, 0px);\n  }\n}\n\n//\n// VIDEO PLAYER\n// ----------------------------------------------\n.video-player {\n  position: relative;\n  cursor: pointer;\n\n  .btn-video {\n    position: absolute;\n    top: 50%;\n    left: 50%;\n    margin-top: -79px;\n    margin-left: -79px;\n  }\n  &:hover {\n    .frame {\n      opacity: 0.92;\n    }\n    .btn-video {\n      opacity: 0.72;\n    }\n  }\n}\n\n//\n// TABLES: basic-table-style\n// ----------------------------------------------\n.basic-table-style {\n  width: 100%;\n  font-size: $font-size-xs;\n\n  tr {\n\n    th, td {\n      border-bottom: 1px solid $gray-color-1;\n      padding: 3px;\n    }\n\n    th {\n\n    }\n\n    td {\n\n    }\n  }\n\n}\n"
  },
  {
    "path": "docs/assets/css/examples.scss",
    "content": ".examples__container {\r\n  max-width: 1200px;\r\n  padding: 15px 50px;\r\n  margin-left: auto;\r\n  margin-right: auto;\r\n  .examples__nav {\r\n    display: flex;\r\n    align-items: stretch;\r\n    border-bottom: 1px solid $gray-color-1;\r\n\r\n    .hidden-navs, .hidden-navs__static-links {\r\n      display: none;\r\n    }\r\n\r\n    .navs {\r\n      display: flex;\r\n      align-items: stretch;\r\n      width: 100%;\r\n      position: relative;\r\n\r\n      .examples__nav-item {\r\n        flex: 1;\r\n        height: 80px;\r\n        display: flex;\r\n        align-items: center;\r\n        justify-content: center;\r\n        flex-direction: column;\r\n        padding: 5px;\r\n        max-width: 200px;\r\n        min-width: 100px;\r\n        border-bottom: 2px solid #fff;\r\n        max-height: 100px;\r\n        cursor: pointer;\r\n\r\n        img {\r\n          max-width: 120px;\r\n          max-height: 80px;\r\n          margin-bottom: 5px;\r\n        }\r\n\r\n        .name {\r\n          font-size: $font-size-xxs;\r\n          font-weight: bold;\r\n          color: $gray-color-2;\r\n          text-transform: uppercase;\r\n          letter-spacing: 0.2px;\r\n        }\r\n\r\n        &.active {\r\n          border-bottom: 2px solid $primary-color-2;\r\n          .name {\r\n            color: $primary-color-2;\r\n          }\r\n        }\r\n\r\n        &:not(.active) {\r\n          img {\r\n            -webkit-filter: grayscale(100%);\r\n            filter: grayscale(100%);\r\n            opacity: 0.25;\r\n          }\r\n          &:hover {\r\n            img {\r\n              opacity: 1;\r\n              -webkit-filter: grayscale(0%);\r\n              filter: grayscale(0%);\r\n            }\r\n            .name {\r\n              color: $primary-color-2;\r\n            }\r\n          }\r\n        }\r\n\r\n        img {\r\n          width: 100%;\r\n        }\r\n      }\r\n\r\n      .navs__visible-bar {\r\n        flex: 1;\r\n        display: flex;\r\n        align-items: stretch;\r\n      }\r\n\r\n      .navs__dropdown-input {\r\n        width: 150px;\r\n      }\r\n\r\n      .navs__dropdown-arrow {\r\n        width: 100px;\r\n        display: flex;\r\n        justify-content: center;\r\n        align-items: center;\r\n      }\r\n\r\n      .navs__dropdown-menu {\r\n        position: absolute;\r\n        top: 80px;\r\n        right: 0px;\r\n        display: none;\r\n        min-width: 260px;\r\n        width: 100%;\r\n        max-width: 400px;\r\n        border: 1px solid $gray-color-1;\r\n        border-radius: 4px;\r\n        flex-direction: column;\r\n        justify-content: center;\r\n        align-items: stretch;\r\n        box-shadow: $box-shadow-sm;\r\n        background: #fff;\r\n\r\n        .examples__nav-item {\r\n          padding: 20px 10px;\r\n          width: 100%;\r\n          max-width: 100%;\r\n          border-bottom: 1px solid $gray-color-1;\r\n          &:last-child {\r\n            border-width: 0px;\r\n          }\r\n        }\r\n\r\n        &.active {\r\n          display: flex;\r\n          z-index: 10;\r\n        }\r\n      }\r\n\r\n      @media all and (max-width: 439px) {\r\n        .navs__visible-bar {\r\n          display: none;\r\n        }\r\n        .navs__dropdown-input {\r\n          width: 100%;\r\n          .examples__nav-item {\r\n            max-width: 100%;\r\n          }\r\n        }\r\n      }\r\n    }\r\n  }\r\n\r\n  .examples__block {\r\n    display: none;\r\n\r\n    &.active {\r\n      display: block;\r\n    }\r\n\r\n    .examples__tabs {\r\n      display: flex;\r\n      margin-top: 30px;\r\n      .tab {\r\n        min-width: 50px;\r\n        display: flex;\r\n        flex-direction: column;\r\n        border: 1px solid $gray-color-1;\r\n        border-bottom: 2px solid $gray-color-1;\r\n        padding: 10px 15px;\r\n        cursor: pointer;\r\n        label {\r\n          line-height: 1;\r\n          font-weight: normal;\r\n          font-size: $font-size-xxs;\r\n          color: $gray-color-3;\r\n        }\r\n        .filename {\r\n          font-size: $font-size-xs;\r\n          color: $gray-color-3;\r\n        }\r\n\r\n        &.active {\r\n          border-bottom: 2px solid $primary-color-2;\r\n          label {\r\n            font-weight: bold;\r\n            color: $primary-color-2;\r\n          }\r\n          .filename {\r\n            color: $text-color;\r\n          }\r\n        }\r\n        &:hover {\r\n          opacity: 0.7;\r\n          border-bottom: 2px solid $primary-color-2;\r\n          label {\r\n            color: $primary-color-2;\r\n          }\r\n          .filename {\r\n            color: $text-color;\r\n          }\r\n        }\r\n      }\r\n    }\r\n\r\n    @media all and (max-width: 800px) {\r\n      .examples__tabs {\r\n        flex-direction: column;\r\n      }\r\n    }\r\n\r\n    .examples__code {\r\n      display: none;\r\n      width: 60%;\r\n      position: relative;\r\n      margin-bottom: 50px;\r\n\r\n      pre[class*=\"language-\"] {\r\n        font-size: 14px;\r\n        margin-top: 0px;\r\n        border-top-left-radius: 0;\r\n        code {\r\n          font-size: 11px;\r\n          line-height: 22px;\r\n        }\r\n      }\r\n\r\n      &.active {\r\n        display: block;\r\n      }\r\n    }\r\n\r\n    .example__file-link {\r\n      padding-top: 20px;\r\n      padding-bottom: 20px;\r\n      font-size: $font-size-xs;\r\n      word-break: break-all;\r\n      p {\r\n        margin-bottom: 4px;\r\n        font-weight: bold;\r\n      }\r\n\r\n      @media all and (max-width: 768px) {\r\n        font-size: 12px;\r\n      }\r\n    }\r\n\r\n    .description {\r\n      display: none;\r\n    }\r\n\r\n    .examples__learn-more {\r\n      min-height: 300px;\r\n      padding: 15px 50px;\r\n      text-align: center;\r\n      margin-top: 50px;\r\n\r\n      .title {\r\n        font-size: 44px;\r\n        font-weight: 300;\r\n      }\r\n\r\n      ul {\r\n        list-style: none;\r\n        padding-left: 0px;\r\n        li {\r\n          padding: 5px 10px;\r\n          a {\r\n            font-size: $font-size-lg;\r\n            font-weight: bold;\r\n            color: $primary-color-2;\r\n            text-decoration: none;\r\n            &:hover, &:focus, &:active {\r\n              text-decoration: none;\r\n            }\r\n            &:hover {\r\n              opacity: .8;\r\n            }\r\n          }\r\n        }\r\n      }\r\n    }\r\n  }\r\n}\r\n\r\n@media all and (max-width: 800px) {\r\n  .examples__container {\r\n    padding-left: 10px;\r\n    padding-right: 10px;\r\n\r\n    .examples__block  {\r\n      .examples__code {\r\n        width: 100%;\r\n      }\r\n    }\r\n  }\r\n}\r\n\r\n@media all and (max-width: 768px) {\r\n  .examples__container {\r\n    .examples__block  {\r\n      .description {\r\n        column-count: 1;\r\n      }\r\n    }\r\n  }\r\n}\r\n\r\n.code-popup-handler {\r\n  position: absolute;\r\n  right: 0px;\r\n  width: 28px;\r\n  height: 28px;\r\n  color: #fff;\r\n  margin-right: -14px;\r\n  margin-top: -16px;\r\n  cursor: pointer;\r\n  z-index: 1;\r\n\r\n  .number {\r\n    border-radius: 50%;\r\n    font-weight: bold;\r\n    font-family: 'Source Sans Pro', sans-serif;\r\n    display: flex;\r\n    align-items: center;\r\n    justify-content: center;\r\n    background: $secondary-gradient-start-color;\r\n    background: -webkit-linear-gradient(-45deg, $secondary-gradient-start-color, $secondary-gradient-stop-color);\r\n    background: linear-gradient(135deg, $secondary-gradient-start-color, $secondary-gradient-stop-color);\r\n  }\r\n\r\n  &.sm-scale .number,\r\n  &.sm-scale .shadow-bg-1,\r\n  &.sm-scale .shadow-bg-2 {\r\n    transform: scale(0.88);\r\n  }\r\n\r\n  .shadow-bg-1 {\r\n    z-index: -1;\r\n    position: absolute;\r\n    width: 34px;\r\n    height: 34px;\r\n    opacity: 0.2;\r\n    top: 50%;\r\n    margin-top: -17px;\r\n    left: 50%;\r\n    margin-left: -17px;\r\n    background: #068ee4;\r\n    border-radius: 50%;\r\n  }\r\n\r\n  .shadow-bg-2 {\r\n    z-index: -2;\r\n    position: absolute;\r\n    width: 40px;\r\n    height: 40px;\r\n    opacity: 0.2;\r\n    top: 50%;\r\n    margin-top: -20px;\r\n    left: 50%;\r\n    margin-left: -20px;\r\n    background: #068ee4;\r\n    border-radius: 50%;\r\n  }\r\n\r\n  &:hover {\r\n    .shadow-bg-1, .shadow-bg-2 {\r\n      opacity: 0.28;\r\n    }\r\n  }\r\n\r\n  .popup {\r\n    display: none;\r\n    width: 300px;\r\n    background: #fff;\r\n    box-shadow: $box-shadow-sm;\r\n    color: $text-color;\r\n    align-items: stretch;\r\n    border-radius: 4px;\r\n    .left-border {\r\n      border-top-left-radius: 4px;\r\n      border-bottom-left-radius: 4px;\r\n      width: 4px;\r\n      min-width: 4px;\r\n      background: $secondary-gradient-start-color;\r\n      background: -webkit-linear-gradient(90deg, $secondary-gradient-start-color, $secondary-gradient-stop-color);\r\n      background: linear-gradient(180deg, $secondary-gradient-start-color, $secondary-gradient-stop-color);\r\n    }\r\n    .content {\r\n      padding: 5px;\r\n      .title {\r\n        font-size: $font-size-xxs;\r\n        font-weight: bold;\r\n      }\r\n      p, .text {\r\n        font-size: $font-size-xxs;\r\n        font-weight: normal;\r\n        margin-bottom: 0;\r\n      }\r\n    }\r\n  }\r\n\r\n  &.active {\r\n    transform: scale(1.2);\r\n    .number {\r\n      background: $primary-gradient-start-color;\r\n      background: -webkit-linear-gradient(0deg, $primary-gradient-start-color, $primary-gradient-stop-color);\r\n      background: linear-gradient(90deg, $primary-gradient-start-color, $primary-gradient-stop-color);\r\n    }\r\n\r\n    .shadow-bg-1, .shadow-bg-2 {\r\n      display: none;\r\n    }\r\n\r\n    .popup {\r\n      display: flex;\r\n      position: absolute;\r\n      left: 50px;\r\n      top: 0;\r\n    }\r\n  }\r\n\r\n  &.active.sm-scale {\r\n    .number {\r\n      transform: scale(0.92);\r\n    }\r\n  }\r\n}\r\n\r\n@media all and (max-width: 1200px) {\r\n  .code-popup-handler {\r\n    .popup {\r\n      width: 200px;\r\n    }\r\n  }\r\n}\r\n\r\n@media all and (max-width: 800px) {\r\n  .code-popup-handler {\r\n    margin-right: 0;\r\n    right: 5px;\r\n\r\n    &.active {\r\n      .popup {\r\n        left: -220px;\r\n      }\r\n    }\r\n  }\r\n}\r\n\r\n@media all and (max-width: 350px) {\r\n  .code-popup-handler {\r\n    margin-right: 0;\r\n    right: 3px;\r\n\r\n    &.active {\r\n      .popup {\r\n        width: 180px;\r\n        left: -182px;\r\n      }\r\n    }\r\n  }\r\n}\r\n\r\n.examples__container.quick-start-examples {\r\n  padding: 0;\r\n\r\n  .examples__nav {\r\n    display: none;\r\n  }\r\n\r\n  .examples__code {\r\n    margin-bottom: 30px;\r\n  }\r\n}\r\n\r\n.examples__container.wide {\r\n  .examples__block {\r\n    .examples__code {\r\n      width: 100%;\r\n    }\r\n  }\r\n\r\n  .code-popup-handler {\r\n    .popup {\r\n      left: 40px;\r\n      width: 280px;\r\n    }\r\n\r\n    @media all and (max-width: 1600px) {\r\n      .popup {\r\n        width: 220px;\r\n      }\r\n    }\r\n\r\n    @media all and (max-width: 1450px) {\r\n      .popup {\r\n        width: 180px;\r\n      }\r\n    }\r\n\r\n    @media all and (max-width: 1366px) {\r\n      .popup {\r\n        width: 280px;\r\n        right: 40px;\r\n        left: auto;\r\n      }\r\n    }\r\n\r\n    @media all and (max-width: 400px) {\r\n      .popup {\r\n        width: 200px;\r\n      }\r\n    }\r\n  }\r\n}\r\n"
  },
  {
    "path": "docs/assets/css/global.scss",
    "content": "//\n// BODY\n// -------------------------------------------------------\nbody {\n  background: #fff;\n}\n\n@media all and (max-width: 991) {\n  body.modal-opened {\n    width: 100vw;\n    height: 100vh;\n    overflow: hidden;\n  }\n}\n\n//\n// BADGES, TAGS\n// -------------------------------------------------------\n.badge {\n  background: rgba(91, 77, 229, 0.1);\n  padding: 9px 26px;\n  border-radius: 4px;\n  color: #5b4de5;\n  font-size: 12px;\n  letter-spacing: 0.34px;\n  font-weight: bold;\n  text-align: center;\n}\n\n//\n// BOX\n// -------------------------------------------------------\n.box {\n  max-width: 900px;\n  margin: 0 auto;\n  border-radius: 10px;\n  background-color: #ffffff;\n  max-width: 800px;\n}\n\n.box-with-links {\n  padding: 15px;\n  padding-top: 40px;\n  padding-bottom: 60px;\n\n  h1, h2, h3, h4 {\n    text-align: center;\n    font-weight: bold;\n    font-size: 44px;\n    line-height: 1.27;\n    margin-top: 10px;\n    margin-bottom: 20px;\n  }\n\n  .card-links {\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n    justify-content: center;\n\n    .card-link {\n      max-width: 90%;\n    }\n  }\n}\n\n//\n// BUTTONS\n// -------------------------------------------------------\n.btn {\n  padding: 17px 30px;\n  border: none;\n  font-size: 16px;\n  box-shadow: 0 5px 14px 0 rgba(22, 62, 122, 0.33);\n  text-transform: uppercase;\n  letter-spacing: 2px;\n  line-height: 1;\n\n  &:focus, &:hover {\n    transform: translateY(1px);\n    box-shadow: 0 2px 8px 0 rgba(22, 62, 122, 0.53);\n  }\n}\n\n.btn-link {\n  box-shadow: none;\n}\n\n.btn-primary {\n  &:focus, &:hover {\n    color: #fff;\n  }\n}\n\n.btn-white {\n  background: #fff;\n  color: #2d7ef4;\n\n  &:focus, &:hover {\n    color: #2d7ef4;\n    background: #fff;\n  }\n}\n\n.btn-lg, .btn-group-lg > .btn {\n  height: 60px;\n  border-radius: 30px;\n  padding: 20px 30px;\n  min-width: 200px;\n}\n\n.btn-sm, .btn-group-sm > .btn {\n  height: 40px;\n  border-radius: 30px;\n  padding: 12px 15px;\n  min-width: 100px;\n  font-size: 12px;\n  line-height: 16px;\n}\n\n.btn-info {\n  &:focus, &:hover {\n    color: #fff;\n  }\n}\n\n.btn-primary-hollow {\n  border: 1px solid #13cfe7;\n\n  &:hover {\n    background: #9c9c9c;\n    background: -webkit-linear-gradient(-45deg, #3c3c3c, #5e5e5e);\n    background: linear-gradient(135deg, #3c3c3c, #5e5e5e);\n  }\n}\n\n.btn-light {\n  background: #fff;\n\n  &:focus, &:hover {\n    color: #13cfe7;\n    background: #fff;\n  }\n}\n\n.btn-group-social-icons {\n  margin-top: 6px;\n  margin-left: -4px;\n\n  .btn {\n    margin: 0;\n    padding: 0;\n    border: none;\n    font-size: 22px;\n    line-height: 22px;\n    background: transparent;\n    box-shadow: none;\n  }\n}\n\n.btn-group-package-icons {\n  .btn {\n    margin: 0;\n    padding: 0 6px;\n    border: none;\n    background: transparent;\n    box-shadow: none;\n  }\n\n  img {\n    margin-left: 6px;\n  }\n}\n\n.btn-primary {\n  &:focus, &:hover {\n    background: #80cdfc;\n    background: -webkit-linear-gradient(-45deg, #80cdfc, #579ff7);\n    background: linear-gradient(135deg, #80cdfc, #579ff7);\n  }\n}\n\n.btn-info {\n  background: #ff2b67;\n  background: -webkit-linear-gradient(-45deg, #ff2b67, #ff5442);\n  background: linear-gradient(135deg, #ff2b67, #ff5442);\n}\n\n.btn-info {\n  &:focus, &:hover {\n    background: #ff024a;\n    background: -webkit-linear-gradient(-45deg, #ff024a, #ff2f19);\n    background: linear-gradient(135deg, #ff024a, #ff2f19);\n  }\n}\n\n.card-link {\n  transition: 0.2s all ease;\n  display: flex;\n  min-height: 56px;\n  width: 100%;\n  align-items: center;\n  justify-content: center;\n  margin: 2px 0px;\n\n  a {\n    border-radius: 10px;\n    border: solid 1px #e8e8e8;\n    background-color: #ffffff;\n    color: #1e252f;\n    text-decoration: none;\n    padding: 15px 25px;\n    width: 100%;\n    font-size: 18px;\n    line-height: 18px;\n\n    &:hover {\n      -webkit-box-shadow: 0 2px 34px 0 rgba(0, 0, 0, 0.1);\n      -moz-box-shadow: 0 2px 34px 0 rgba(0, 0, 0, 0.1);\n      box-shadow: 0 2px 34px 0 rgba(0, 0, 0, 0.1);\n      border: none;\n      transform: scale(1.02) translateZ(0);\n    }\n  }\n}\n\n.link-tag-styled {\n  background: #fff;\n  padding: 7px 12px;\n  border-radius: 10px;\n  text-transform: uppercase;\n  line-height: 20px;\n  font-weight: bold;\n  letter-spacing: 2px;\n  text-decoration: none;\n  color: #2d7ef4;\n\n  &:hover, &:active, &:focus {\n    text-decoration: none;\n  }\n\n  img {\n    margin-right: 5px;\n  }\n\n  .link-icon {\n    height: 16px;\n  }\n}\n\n.btn-outline-white {\n  border: 2px solid white;\n}\n\n//\n// CODE\n// -------------------------------------------------------\ncode, pre {\n  border-color: $gray-color-1;\n}\n\n.code-snippet {\n  font-family: 'Fira Mono', monospace;\n\n  pre {\n    background: $gray-color-1;\n    border-width: 0px;\n    border-radius: 10px;\n    padding: 20px;\n    text-align: left;\n  }\n\n  .purple {\n    color: #5b55e8;\n  }\n\n  .salmon {\n    color: #ee4f5d;\n  }\n}\n\n.highlighter-rouge.language-plaintext .highlight {\n  background: #F5F3FD;\n\n  code {\n    color: #545492;\n  }\n}\n\npre[class*=\"language-\"] {\n  background: $gray-color-1;\n}\n\n\ncode[class*=\"language-\"], pre[class*=\"language-\"]  {\n    line-height: 15px;\n}\n\ncode::selection, pre[class*=\"language-\"]::selection, pre[class*=\"language-\"] span::selection {\n  background: rgba(0,0,30,0.1) !important;\n}\n\ncode.highlighter-rouge.language-plaintext {\n  background: $gray-color-1;\n  color: #5b4de5;\n  border-radius: 4px;\n  padding-left: 8px;\n  padding-right: 8px;\n  font-size: 87%;\n}\n\ncode {\n  background: none;\n  color: $inline-code-color-base;\n  background: $inline-code-bg-color-base;\n  font-size: 87%;\n}\n\np code, h1 code, h2 code, h3 code, h4 code, h5 code {\n  background: rgba(91, 77, 229, 0.1);\n  color: #5b4de5;\n  border-radius: 4px;\n  padding-left: 8px;\n  padding-right: 8px;\n}\n\n//\n// GRADIENTS\n// -------------------------------------------------------\n.gradient-primary.gradient-vertical, .sub-page .navbar-default, .index-page .section-hero, .section-bg-blue, .how-it-works .section-hero {\n  background: $primary-gradient-start-color;\n  background: -webkit-linear-gradient(top, $primary-gradient-start-color, $primary-gradient-stop-color);\n  background: linear-gradient(to bottom, $primary-gradient-start-color, $primary-gradient-stop-color);\n}\n\n.gradient-primary.gradient-diagonal {\n  background: $primary-gradient-start-color;\n  background: -webkit-linear-gradient(-45deg, $primary-gradient-start-color, $primary-gradient-stop-color);\n  background: linear-gradient(135deg, $primary-gradient-start-color, $primary-gradient-stop-color);\n}\n\n.btn-primary {\n  background: $secondary-gradient-start-color;\n  background: -webkit-linear-gradient(-45deg, $secondary-gradient-start-color, $secondary-gradient-stop-color);\n  background: linear-gradient(135deg, $secondary-gradient-start-color, $secondary-gradient-stop-color);\n}\n\n.gradient-primary-hover {\n  &.gradient-vertical {\n    background: #80cdfc;\n    background: -webkit-linear-gradient(top, #80cdfc, #579ff7);\n    background: linear-gradient(to bottom, #80cdfc, #579ff7);\n  }\n\n  &.gradient-diagonal {\n    background: #80cdfc;\n    background: -webkit-linear-gradient(-45deg, #80cdfc, #579ff7);\n    background: linear-gradient(135deg, #80cdfc, #579ff7);\n  }\n}\n\n//\n// GRIDS & SECTIONS\n// -------------------------------------------------------\n\n.row-divider {\n  padding-top: 20px;\n  padding-bottom: 20px;\n}\n\n.flex {\n  display: flex;\n}\n.align-items--stretch {\n  align-items: stretch;\n}\n.align-items--end {\n  align-items: flex-end;\n}\n\n@media (min-width: 768px) {\n  .row-divider > div {\n    + div {\n      border-left: 1px solid #194c5f;\n    }\n\n    &:first-child {\n      padding-left: 0;\n      padding-right: 30px;\n    }\n\n    &:last-child {\n      padding-right: 0;\n      padding-left: 30px;\n    }\n  }\n}\n\n@media (max-width: 991px) {\n  .container {\n    padding-right: 30px;\n    padding-left: 30px;\n  }\n}\n\n.container > .row + .row {\n  padding-top: 45px;\n}\n\n.index-page .container > .row + .row {\n  padding-top: 60px;\n}\n\n@media (min-width: 768px) {\n  .container > .row + .row {\n    padding-top: 60px;\n  }\n}\n\n@media (min-width: 992px) {\n  .index-page .section-dark .container > .row {\n    padding-top: 90px;\n  }\n}\n\n.section-blue {\n  background-color: #0FC0EF;\n  color: #fff;\n}\n\n.section {\n  padding-top: 45px;\n  padding-bottom: 45px;\n}\n\n@media (min-width: 768px) {\n  .section {\n    padding-top: 60px;\n    padding-bottom: 60px;\n  }\n}\n\n@media (min-width: 768px) {\n  .section-hero-with-button {\n    padding-top: 30px;\n    padding-bottom: 20px;\n  }\n}\n\n.index-page .section-hero {\n  color: #fff;\n  background: $primary-gradient-start-color;\n  background: -webkit-linear-gradient(-45deg, $primary-gradient-start-color, $primary-gradient-stop-color);\n  background: linear-gradient(135deg, $primary-gradient-start-color, $primary-gradient-stop-color);\n}\n\n@media (min-width: 768px) {\n  .index-page .section-hero {\n    padding-top: 30px;\n\n    .floating-img-infrastructure-in-a-day {\n      position: absolute;\n      width: 220px;\n      top: 30px;\n      left: -60px;\n    }\n  }\n}\n\n@media (min-width: 992px) {\n  .index-page .section-hero {\n    margin-top: -84px;\n    padding-top: 174px;\n    padding-bottom: 135px;\n\n    .floating-img-infrastructure-in-a-day {\n      position: absolute;\n      width: auto;\n      top: -60px;\n      left: 15px;\n    }\n  }\n}\n\n.section-bg-blue {\n  color: #fff;\n  padding-bottom: 20px;\n  padding-top: 84px;\n}\n\n@media (min-width: 991px) {\n  .section-bg-blue {\n    margin-top: -84px;\n  }\n}\n\n.sub-page .section-hero {\n  background: url('../img/bg-header-squares.png') center bottom no-repeat, linear-gradient(#1a232d, #2e3b4a);\n}\n\n@media only screen {\n  .sub-page .section-hero {\n    border-bottom: 2px solid rgba(15, 192, 239, 0.25);\n  }\n}\n\n.index-page .main .section-dark {\n  background-image: url('../img/bg-content-squares.png');\n  background-position: center top;\n  background-repeat: no-repeat;\n}\n\n@media (min-width: 992px) {\n  .index-page .main .section-dark {\n    padding-bottom: 190px;\n\n    .container {\n      position: relative;\n\n      &::after {\n        content: \"\";\n        display: block;\n        position: absolute;\n        height: 305px;\n        width: 224px;\n        bottom: -260px;\n        right: 6px;\n        z-index: 980;\n        background: url('../img/infrastructure-cube-icons/infrastructure-endcap.png') right bottom no-repeat;\n        background-size: 224px 305px;\n      }\n    }\n  }\n}\n\n@media (min-width: 768px) {\n  .section-sub-hero {\n    padding-top: 20px;\n    padding-bottom: 20px;\n  }\n}\n\n.thanks-page {\n  .header {\n    background: linear-gradient(135deg, #001191, #06a3ff);\n  }\n\n  .section-thanks {\n    display: flex;\n    justify-content: center;\n    align-items: center;\n    min-height: 60vh;\n    \n    .copy-container {\n      margin-top: 10px;\n    }\n  }\n}\n\n\n.section-heading {\n  padding-bottom: 0;\n  margin-bottom: -30px;\n\n  h1, h2 {\n    margin-bottom: 0;\n    line-height: 1.4;\n  }\n}\n\n//\n// HEADING\n// ----------------------------------------------\nh1, .h1, h2, .h2, h3, .h3 {\n  font-weight: 200;\n}\n\n.section-colorful-bg {\n  h1, .h1, h2, .h2, h3, .h3, h4, .h4 {\n    color: #ffffff;\n  }\n}\n\nh1, .h1 {\n  margin-top: 38.88px;\n  margin-bottom: 27px;\n  line-height: 1.15;\n}\n\nh2, .h2 {\n  margin-top: 32.4px;\n  margin-bottom: 21.6px;\n  line-height: 1.21;\n}\n\nh3, .h3 {\n  margin-top: 27px;\n  margin-bottom: 19.44px;\n  line-height: 1.27;\n}\n\nh4, .h4 {\n  margin-top: 23.76px;\n  margin-bottom: 17.28px;\n  line-height: 1.33;\n}\n\nh5, .h5 {\n  margin-top: 20.52px;\n  margin-bottom: 15.12px;\n  line-height: 1.4;\n}\n\nh6, .h6 {\n  margin-top: 18.36px;\n  margin-bottom: 14.04px;\n  line-height: 1.45;\n}\n\n@media all and (max-width: 992px) {\n  h1, .h1 {\n    font-size: 44px;\n  }\n\n  h2, .h2 {\n    font-size: 38px;\n  }\n\n  h3, .h3 {\n    font-size: 36px;\n  }\n\n  h4, .h4 {\n    font-size: 32px;\n  }\n\n  h5, .h5 {\n    font-size: 26px;\n  }\n\n  h6, .h6 {\n    font-size: 20px;\n  }\n}\n\n.h2-tag-style {\n  font-size: 16px;\n  text-transform: uppercase;\n  display: table;\n  padding: 7px 20px;\n  color: #5b4de5;\n  font-weight: bold;\n  letter-spacing: 2px;\n  background: rgba(91, 77, 229, 0.1);\n  border-radius: 10px;\n  margin: 30px auto;\n}\n\n.sub-page .section-hero h1 + p {\n  margin-top: -15px;\n}\n\n.section > .container > .row > div[class*=\"col-\"] > {\n  p:first-child, h1:first-child, h2:first-child, h3:first-child, h4:first-child, h5:first-child, h6:first-child {\n    margin-top: 0;\n  }\n\n  p:last-child, h1:last-child, h2:last-child, h3:last-child, h4:last-child, h5:last-child, h6:last-child {\n    margin-bottom: 0;\n  }\n}\n\n//\n// LINES\n// ----------------------------------------------\nhr {\n  margin: 45px 0;\n  border-width: 2px;\n  border-color: #194c5f;\n\n  &.hr-dim {\n    border-color: #1e5c73;\n  }\n\n  &.hr-xl {\n    margin: 75px 0;\n  }\n\n  &.hr-lg {\n    margin: 60px 0;\n  }\n\n  &.hr-sm {\n    margin: 20px 0;\n  }\n\n  &.hr-xs {\n    margin: 10px 0;\n  }\n\n  &.hr-dashed {\n    border-style: dashed;\n  }\n\n  &.hr-reset {\n    border-color: initial;\n    border-width: 1px;\n  }\n\n  &.short {\n    width: 80%;\n    max-width: 300px;\n    border-width: 1px;\n    opacity: 0.3;\n    background-color: #1e252f;\n    margin: 45px auto;\n  }\n}\n\nfooter hr {\n  border-color: #ffffff;\n  border-width: 1px;\n}\n\n//\n// NAVBAR\n// -------------------------------------------------------\n.navbar-nav > li > .dropdown-menu {\n  margin-top: -20px;\n  border-top-width: 2px;\n  border-right: 2px solid #194C5F;\n  border-bottom: 2px solid #194C5F;\n  border-left: 2px solid #194C5F;\n}\n\n.navbar-nav {\n  li {\n    &:first-child {\n      a {\n        display: flex;\n        span {\n          margin-right: 12px;\n          padding: 0;\n          background: linear-gradient(101.84deg, #FE3162 2.31%, #FF4F47 98.56%);\n          border-radius: 3.5px;\n          font-weight: 600;\n          min-width: 52px;\n          display: flex;\n          align-items: center;\n          justify-content: center;\n        }\n      }\n    }\n  }\n}\n\n@media (min-width: 768px) {\n  .dropdown:hover .dropdown-menu {\n    display: block;\n  }\n}\n\n.navbar-brand {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n}\n\n.close {\n  opacity: 1.0;\n}\n\n.navbar-default, .navbar-collapse {\n  border: none;\n}\n\n.index-page .navbar-default {\n  border: none;\n}\n\n@media all and (min-width: 1200px) {\n  .navbar.navbar-default {\n    max-width: 90%;\n    margin: 0px auto;\n  }\n}\n\n.navbar-brand {\n  flex-direction: column;\n  align-items: flex-start;\n}\n.navbar-nav > li > a {\n  padding-right: 10px;\n  padding-left: 10px;\n  text-transform: uppercase;\n  letter-spacing: 2px;\n  font-size: 16px;\n\n  &.btn {\n    padding: 6px 12px;\n    margin-top: 19px;\n    margin-bottom: 19px;\n    margin-left: 20px;\n\n    &:hover, &:focus {\n      color: #fff;\n    }\n  }\n}\n\n.github-nav-link {\n  img {\n    max-height: 18px;\n    margin-top: -3px;\n    &:hover {\n      opacity: 0.7;\n    }\n  }\n}\n\n@media (min-width: 992px) {\n  .navbar-nav > li > a {\n    padding-right: 20px;\n    padding-left: 20px;\n  }\n}\n\n@media (min-width: 1200px) {\n  .navbar-nav > li > a.contact-sales {\n    margin-right: -5em;\n  }\n}\n\n@media (min-width: 1400px) {\n  .navbar-nav > li > a.contact-sales {\n    margin-right: -9em;\n  }\n}\n\n@media (min-width: 768px) {\n  .navbar-nav.navbar-center {\n    position: absolute;\n    left: 50%;\n    transform: translatex(-50%);\n  }\n}\n\n\n\n//\n// SHADOWS\n// -------------------------------------------------------\n.shadow {\n  box-shadow: 0 2px 34px 0 rgba(0, 0, 0, 0.1);\n}\n\n//\n// TEXT & TYPOGRAPHY\n// -------------------------------------------------------\n\n@media only screen and (max-width: 767px) {\n  p + ul > li {\n    font-size: 14px;\n  }\n}\n\nlabel {\n  font-weight: 400;\n  margin-bottom: 6px;\n  color: #1e252f;\n}\n\np a:not(.btn) {\n  text-decoration: underline;\n\n  &:hover, &:focus {\n    text-decoration: underline;\n  }\n\n  &.caret-right {\n    text-decoration: none;\n  }\n}\n\na.caret-right::after {\n  font-family: 'FontAwesome';\n  content: \"\\f0da\";\n  margin-left: 4px;\n}\n\n.section-blue a {\n  color: #fff;\n\n  &:hover, &:focus {\n    color: rgba(255, 255, 255, 0.7);\n  }\n}\n\n//\n// TEXT-DECORATION \n// -------------------------------------------------------\n\n.new-badge{\n  background: #ee4f5d;\n  padding: 0px 10px;\n  border-radius: 3px;\n  display: inline-block;\n  color: #fff;\n  font-family: 'Source Sans Pro', sans-serif;\n  font-size: 16px;\n  font-weight: 800;\n}\n\n//\n// LOGO\n// -------------------------------------------------------\n.terragrunt-logo {\n  display: flex;\n  flex-direction: column;\n  align-items: flex-start;\n\n  a {\n    color: #fff;\n\n    &:hover {\n      opacity: 0.8;\n      text-decoration: none;\n    }\n\n    &:focus, &:active {\n      text-decoration: none;\n    }\n\n    &.logo-terragrunt {\n      font-size: 32px;\n      font-weight: 700;\n    }\n\n    &.gruntwork {\n      font-size: 14px;\n    }\n  }\n}\n\n//\n// SUBPAGE\n// ----------------------------------------------\n.sub-page {\n  .main {\n    position: relative;\n    border-bottom: 2px solid rgba(15, 192, 239, 0.25);\n\n    > .section:last-child {\n      padding-bottom: 120px;\n    }\n\n    &::after {\n      content: \"\";\n      display: block;\n      position: absolute;\n      height: 55px;\n      width: 100%;\n      bottom: -34px;\n      z-index: 990;\n      background-image: url('../img/hr-boxes.png');\n      background-position: center bottom;\n      background-repeat: no-repeat;\n      background-size: 80px 55px;\n    }\n  }\n\n  &.customers .main > .section:last-child {\n    padding-bottom: 0;\n  }\n}\n\n//\n// FOOTER\n// ----------------------------------------------\n.section-footer {\n  margin-top: -30px;\n  padding-top: 0;\n}\n\n.section-footer-copyright {\n  background-color: transparent;\n  color: #647b9c;\n\n  a {\n    color: rgba(100, 123, 156, 0.8);\n\n    &:hover, &:focus {\n      color: #647b9c;\n    }\n  }\n}\n\n.footer {\n  background: linear-gradient(100deg, #1b242e 5%, #2a3644 98%);\n  padding-top: 60px;\n  padding-bottom: 60px;\n\n  > .container-fluid {\n    max-width: 1200px;\n    margin: 0 auto;\n  }\n\n  .terragrunt-logo {\n    margin-bottom: 10px;\n    margin-top: 10px;\n\n    .logo-terragrunt {\n      margin-bottom: 4px;\n    }\n  }\n\n  .subtitle {\n    color: #fff;\n    margin-bottom: 30px;\n    font-size: 14px;\n  }\n\n  a, ul > li > a {\n    color: #fff;\n  }\n\n  a:hover, ul > li > a:hover, a:active, ul > li > a:active, a:focus, ul > li > a:focus {\n    color: #fff;\n    text-decoration: none;\n  }\n\n  h4 {\n    font-size: 16px;\n    color: #fff;\n    text-transform: uppercase;\n    font-weight: bold;\n    margin-bottom: 5px;\n  }\n\n  .learn-col {\n    text-align: right;\n\n    ul {\n      display: flex;\n      flex-wrap: wrap;\n      list-style: none;\n      padding-left: 0;\n      margin-left: 0;\n      justify-content: flex-end;\n\n      li {\n        margin-bottom: 1rem;\n        margin-left: 40px;\n      }\n    }\n  }\n\n  .copy-container {\n    display: flex;\n    align-items: center;\n    justify-content: flex-end;\n    margin-top: 90px;\n    ul {\n      margin-left: 0;\n      margin-bottom: 0;\n      li {\n        display: inline-block;\n        margin-left: 0;\n        margin-bottom: 0;\n        line-height: 1;\n        margin-right: 20px;\n        a {\n          font-size: 12px;\n        }\n      }\n    }\n  }\n  .footer__copyright {\n    font-size: 12px;\n    margin-top: 2px;\n    color: #fff;\n    opacity: 0.5;\n    line-height: 1;\n  }\n}\n\n@media all and (max-width: 991px) {\n  .footer {\n    .row {\n      display: flex;\n      align-items: center;\n    }\n\n    .learn-col {\n      text-align: center;\n\n      ul {\n        flex-direction: column;\n        justify-content: center;\n        align-items: center;\n\n        li {\n          margin-left: 0px;\n        }\n      }\n\n      .copy-container {\n        ul {\n          flex-direction: row;\n        }\n      }\n    }\n\n  }\n}\n\n@media all and (max-width: 768px) {\n  .footer {\n    text-align: center;\n\n    .row {\n      flex-direction: column;\n      justify-content: center;\n    }\n\n    .learn-col {\n      margin-top: 30px;\n\n      .copy-container {\n        justify-content: center;\n        flex-direction: column;\n        margin-top: 40px;\n        ul {\n          flex-direction: column;\n          li {\n            margin-bottom: 10px;\n            margin-right: 0;\n          }\n        }\n      }\n    }\n\n    .terragrunt-logo {\n      align-items: center;\n    }\n  }\n}\n\n@media all and (max-width: 400px) {\n  .footer {\n    text-align: center;\n\n    .btn.btn-primary {\n      font-size: 12px;\n    }\n  }\n}\n"
  },
  {
    "path": "docs/assets/css/pages/contact.scss",
    "content": "\n.contact {\n  font-family: \"Source Sans Pro\", sans-serif;\n  background: #001191;\n  background: -webkit-linear-gradient(-45deg, #001191, #06a3ff);\n  background: linear-gradient(135deg, #001191, #06a3ff);\n\n  #error-message {\n    text-align: center;\n    h3 {\n      font-size: 24px;\n      font-weight: 500;\n    }\n  }\n\n  .has-error {\n    border-color: #ff2b67!important;\n  }\n\n  .navbar-header {\n    margin: auto;\n\n    .navbar-toggle {\n      .icon-bar {\n        &:nth-child(3) {\n          width: 16px;\n          margin-left: auto;\n        }\n      }\n    }\n  }\n\n  .hidden-shape {\n    display: none;\n  }\n\n  .built-by-cmp {\n    display: none;\n  }\n\n  .subpage__header {\n    background: transparent;\n    position: relative;\n    .header-shapes-top {\n      position: absolute;\n      top: -50px;\n      left: 200px;\n    }\n  }\n\n  .links-section-cmp {\n    background: transparent;\n  }\n\n  .subpage__contact {\n    background: transparent;\n    max-width: 1200px;\n    width: 90%;\n    margin: auto;\n    min-height: 1000px;\n\n    .contact-column {\n      display: flex;\n      flex-direction: column;\n      justify-content: left;\n      margin-top: 100px;\n      color: white;\n\n      h1 {\n        color: white;\n      }\n\n      .contact-subtitle {\n        font-size: 26px;\n        line-height: 1.2;\n        a {\n          color: white;\n          &:hover {\n            opacity: .9;\n          }\n        }\n      }\n\n      .contact-subtitle-back {\n        margin-top: 100px;\n      }\n    }\n\n    .form-column {\n      margin-top: 1rem;\n\n      .contact-form-container {\n        padding: 20px 30px;\n        border-radius: 6px;\n        border: solid 1px #cececf;\n        background: #fff;\n        max-width: 550px;\n        position: relative;\n\n        .contact-form-back {\n          position: absolute;\n          right: -40px;\n          z-index: -1;\n          top: 50px;\n        }\n\n        #submit-button {\n          min-width: 180px;\n        }\n        \n        #contact-form {\n          display: flex;\n          flex-direction: column;\n          label {\n              margin-top: 2rem;\n              font-size:16px;\n              font-weight: bold;\n          }\n          input, textarea {\n            border-radius: 6px;\n            border: solid 1px #cececf;\n            text-indent: 12px;\n            &::placeholder {\n              color: #cececf;\n            }\n          }\n          span {\n            margin: 2rem 0;\n            font-size: 16px;\n            font-weight: normal;\n            font-stretch: normal;\n            font-style: normal;\n            line-height: normal;\n            letter-spacing: normal;\n            color: #1e252f;\n          }\n\n          textarea {\n            resize: vertical;\n          }\n\n          input{\n              height: 50px;\n          }\n\n          .radio-element{\n            display: flex;\n            align-items: center;\n            position: relative;\n\n            input {\n              position: absolute;\n              opacity: 0;\n              cursor: pointer;\n              height: 0;\n              width: 0;\n              margin: auto 0;\n              &:checked ~ .checkmark:after {\n                display: block;\n              }\n            }\n\n            .checkmark {\n              position: absolute;\n              left: 0;\n              height: 16px;\n              width: 16px;\n              margin: 0;\n              border: 1px solid #0237ae;\n              border-radius: 3px;\n              &:after {\n                content: \"\";\n                position: absolute;\n                display: none;\n                left: 5px;\n                top: 2px;\n                width: 5px;\n                height: 9px;\n                border: solid #0237ae;\n                border-width: 0 2px 2px 0;\n                -webkit-transform: rotate(45deg);\n                -ms-transform: rotate(45deg);\n                transform: rotate(45deg);\n              }\n            }\n            label {\n              cursor: pointer;\n              margin: auto 1rem auto 0;\n              padding-left: 25px;\n              width: 100%;\n              text-transform: uppercase;\n            }\n\n            .radio-label {\n              width: auto;\n              margin: 0;\n              position: absolute;\n              left: 0;\n              top: 0;\n              height: 100%;\n              display: flex;\n              align-items: center;\n            }\n\n            .radio-btn-contact{\n              input[radio]{\n                margin:0;\n                padding:0;\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n}\n\n.contact__page {\n  min-height: 100vh;\n}\n\n@media only screen and (max-width: 992px) {\n  .contact {\n    .links {\n\n      flex-wrap: wrap;\n\n      a {\n        margin-top: 15px;\n      }\n    }\n\n    .contact-form-container {\n      margin: auto;\n    }\n\n    .contact-form-back {\n      display: none;\n    }\n\n    .header-shapes-bottom {\n      margin: auto;\n      margin-top: 2rem;\n      width: 100%;\n      max-width: 500px;\n    }\n\n    .hidden-shape-xs {\n      display: none;\n    }\n\n    .hidden-shape {\n      display: block;\n    }\n\n    .subpage__contact {\n      .form-column {\n        margin-top: 3rem;\n      }\n\n      .contact-column {\n        margin-top: 0;\n      }\n    }\n\n    .subpage__header {\n      padding-top: 20px;\n      padding-bottom: 0;\n      \n      .header-shapes-top {\n        top: -35px;\n        left: 160px;\n      }\n    }\n  }\n}\n\n@media only screen and (max-width: 440px) {\n  .contact {\n    .subpage__contact {\n      .form-column {\n        .contact-form-container {\n          #contact-form {\n            .radio-element {\n              label {\n                font-size: 14px;\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n}"
  },
  {
    "path": "docs/assets/css/pages/cookie-policy.scss",
    "content": ".page-cookie-policy {\n  .container > .row + .row {\n    padding-top: 28px;\n  }\n}\n"
  },
  {
    "path": "docs/assets/css/pages/home.scss",
    "content": "body.index-page {\n  header.header {\n    z-index: 20;\n    position: relative;\n  }\n\n  .section.section-hero {\n    position: relative;\n  }\n\n  .header-bg {\n    position: absolute;\n    width: 100%;\n    height: 100%;\n    left: 0;\n    top: 0;\n    z-index: 0;\n    overflow-x: hidden;\n\n    .header-shapes-top {\n      z-index: 0;\n      position: absolute;\n      top: 0;\n      width: 60vw;\n      min-width: 600px;\n      max-width: 800px;\n      margin-left: -20%;\n      left: 60vw;\n    }\n  }\n\n  .header-shapes-bottom {\n    position: absolute;\n    bottom: 5%;\n    right: 0;\n    width: 250px;\n    height: 400px;\n    background-position: top left;\n    background-size: 250px 400px;\n    background-repeat: no-repeat;\n  }\n\n  .index-page__header {\n    z-index: 1;\n    position: relative;\n  }\n\n  .navbar.navbar-default {\n    padding-top: 30px;\n  }\n\n  .section-hero {\n    margin-top: 0px;\n    padding-top: 0px;\n    padding-bottom: 70px;\n  }\n\n  .index-page__header {\n    display: flex;\n    width: 100%;\n    margin: auto;\n\n    .img-container {\n      flex: 1;\n      max-width: 50%;\n      position: relative;\n\n      .header-shapes-hero {\n        position: absolute;\n        max-width: 800px;\n        left: 15%;\n        height: calc(100% + 220px);\n        bottom: -72px;\n      }\n    }\n\n    .text-container {\n      flex: 1;\n      padding: 15px;\n      max-width: 600px;\n      position: relative;\n\n      h1 {\n        line-height: 1;\n        margin-top: 10px;\n      }\n\n      .subtitle {\n        font-size: 22px;\n        display: block;\n        margin-bottom: 40px;\n        line-height: 1.23;\n      }\n\n      .btn {\n        min-width: 250px;\n      }\n    }\n  }\n\n  .link-to-test-with-terratest {\n    text-decoration: underline;\n    cursor: pointer;\n    &:hover {\n      opacity: 0.7;\n    }\n  }\n\n  .index-page__terratest-in-4-steps {\n    h2 {\n      max-width: 800px;\n      margin-left: auto;\n      margin-right: auto;\n    }\n    .test-steps {\n      display: flex;\n      align-items: stretch;\n      justify-content: center;\n      padding: 15px 50px;\n      max-width: 1200px;\n      margin-left: auto;\n      margin-right: auto;\n      margin-bottom: 120px;\n      flex-wrap: wrap;\n      .test-step {\n        flex: 1;\n        display: flex;\n        flex-direction: column;\n        padding: 0;\n        min-width: 240px;\n\n        .icon-wrapper {\n          display: flex;\n          margin-bottom: 20px;\n          .line {\n            flex: 1;\n            border-top: 1px dashed $gray-color-3;\n            display: block;\n            margin-left: 0;\n            margin-right: 2px;\n            margin-top: 15px;\n            max-width: calc(50% - 36px);\n          }\n          img {\n            margin-left: 5px;\n            margin-right: 5px;\n          }\n        }\n\n        .text-wrapper {\n          display: flex;\n          flex-direction: column;\n          text-align: center;\n          padding-left: 5px;\n          padding-right: 5px;\n        }\n\n        label {\n          font-size: $font-size-lg;\n          font-weight: bold;\n          max-width: 280px;\n          line-height: 1.2;\n          min-height: 58px;\n        }\n        .desc {\n          font-size: $font-size-sm;\n          max-width: 280px;\n        }\n\n        @media all and (min-width: 451px) {\n          &:first-child {\n            .icon-wrapper {\n              justify-content: flex-end;\n            }\n          }\n        }\n\n        &:last-child {\n          .icon-wrapper {\n            justify-content: flex-start;\n          }\n        }\n\n        @media all and (max-width: 1100px) {\n          min-width: 400px;\n          margin-bottom: 20px;\n\n          .icon-wrapper {\n            .line {\n              display: none;\n            }\n          }\n\n          .desc {\n            max-width: 400px;\n          }\n          .text-wrapper {\n            text-align: left;\n            label {\n              min-height: auto;\n            }\n          }\n        }\n        @media all and (max-width: 1100px) {\n          flex-direction: row;\n          margin-bottom: 40px;\n          .text-wrapper {\n            margin-left: 15px;\n          }\n        }\n        @media all and (max-width: 450px) {\n          flex-direction: column;\n          min-width: auto;\n          .text-wrapper {\n            margin-left: 0px;\n          }\n          img {\n            width: 46px;\n          }\n        }\n      }\n\n      @media all and (max-width: 1100px) {\n        max-width: 600px;\n        margin-left: auto;\n        margin-right: auto;\n        margin-bottom: 50px;\n      }\n      @media all and (max-width: 450px) {\n        flex-direction: column;\n        padding: 15px;\n      }\n    }\n  }\n\n  .index-page__cta-section {\n    padding-top: 100px;\n    padding-bottom: 100px;\n    position: relative;\n    margin-bottom: 120px;\n\n    .btn {\n      z-index: 1;\n      position: relative;\n    }\n    .left-img {\n      position: absolute;\n      left: 0;\n      top: -208px;\n    }\n\n    .right-img {\n      position: absolute;\n      right: 100px;\n      top: -220px;\n    }\n\n    @media all and (max-width: 991px) {\n      margin-bottom: 30px;\n\n      .right-img {\n        width: 300px;\n        top: -180px;\n        right: 5%;\n      }\n      .left-img {\n        width: 300px;\n        top: -100px;\n      }\n    }\n\n    @media all and (max-width: 600px) {\n      .right-img {\n        display: none;\n      }\n      .left-img {\n        width: 80%;\n        top: -100px;\n      }\n    }\n\n    @media all and (max-width: 450px) {\n      .hide-on-cxs {\n        display: none;\n      }\n    }\n\n  }\n\n  .code-mark {\n    display: absolute;\n    right: 0px;\n    width: 200px;\n    height: 200px;\n    background: red;\n  }\n\n  .index-page__watch {\n    max-width: 1200px;\n    margin-left: auto;\n    margin-right: auto;\n    padding-top: 100px;\n\n    .title-label {\n      color: $primary-color;\n      font-weight: bold;\n      display: block;\n      margin-bottom: 30px;\n      font-size: 22px;\n    }\n\n    h2 {\n      font-size: 34px;\n    }\n    .btn-sm {\n      margin-right: 10px;\n    }\n\n    @media all and (max-width: 991px) {\n      .row.flex {\n        flex-direction: column;\n        &>.col-xs-12:first-child {\n          margin-bottom: 50px;\n          max-width: 800px;\n          margin-left: auto;\n          margin-right: auto;\n        }\n        .col-xs-12 {\n          align-items: center;\n          justify-content: center;\n\n        }\n      }\n    }\n  }\n\n  .index-page__built-by {\n    text-align: center;\n    position: relative;\n    overflow-x: hidden;\n    margin-bottom: 30px;\n    padding-bottom: 80px;\n\n    .subtitle {\n      font-size: 22px;\n    }\n\n    .bg-image {\n      position: absolute;\n      z-index: -1;\n      width: 1440px;\n      bottom: 0;\n      left: 50%;\n      transform: translateX(-50%);\n    }\n  }\n}\n\n@media all and (max-width: 1600px) {\n  body.index-page {\n    .index-page__header .img-container .header-shapes-hero {\n      left: 15px;\n    }\n  }\n}\n\n@media all and (max-width: 1310px) {\n  body.index-page {\n    .header-shapes-bottom {\n      display: none;\n    }\n\n    .index-page__header .img-container .header-shapes-hero {\n      left: 5vw;\n      right: auto;\n      height: 120%;\n      max-height: 440px;\n    }\n  }\n}\n\n@media all and (max-width: 1200px) {\n  body.index-page {\n    .index-page__header .img-container .header-shapes-hero {\n      right: 5vw;\n      left: auto;\n    }\n\n    .index-page__header .text-container .subtitle {\n      font-size: 20px;\n    }\n  }\n}\n\n@media all and (max-width: 991px) {\n  body.index-page {\n    .header-shapes-bottom {\n      display: none;\n    }\n\n    .index-page__header {\n      justify-content: center;\n\n      .img-container {\n        display: none;\n      }\n\n      .text-container {\n        text-align: center;\n        max-width: 700px;\n\n        .subtitle {\n          font-size: 16px;\n        }\n      }\n    }\n\n    .index-page__key-features .index-page__key-feature {\n      text-align: center;\n      max-width: 700px;\n\n      .subtitle {\n        max-width: 100%;\n        font-size: 16px;\n      }\n\n      img {\n        &.custom-width, &.custom-width-2 {\n          display: none;\n        }\n      }\n\n      .custom-width-3 {\n        display: block;\n        margin: auto;\n        margin-bottom: 20px;\n      }\n\n      .code-snippet {\n        font-size: 10px;\n\n        pre {\n          max-width: 600px;\n          margin: 0 auto 30px auto;\n        }\n      }\n    }\n  }\n}\n\n@media all and (max-width: 768px) {\n  body.index-page {\n    .index-page__key-features .index-page__key-feature h3 {\n      margin-top: 5px;\n    }\n\n    hr.short {\n      margin: 10px auto;\n    }\n  }\n}\n\n@media all and (max-width: 480px) {\n  body.index-page .index-page__key-features .index-page__key-feature .code-snippet pre {\n    font-size: 10px;\n  }\n}\n"
  },
  {
    "path": "docs/assets/css/pages/support.scss",
    "content": ".page-support {\n  .subheader {\n    font-weight: bold; font-style:italic; margin: -16px 0 10px 0;\n  }\n}\n"
  },
  {
    "path": "docs/assets/css/prism.css",
    "content": "/* PrismJS 1.17.1\nhttps://prismjs.com/download#themes=prism-solarizedlight&languages=markup+css+clike+javascript+bash+ruby+docker+go+hcl+java+json+python+yaml */\n/*\n Solarized Color Schemes originally by Ethan Schoonover\n http://ethanschoonover.com/solarized\n\n Ported for PrismJS by Hector Matos\n Website: https://krakendev.io\n Twitter Handle: https://twitter.com/allonsykraken)\n*/\n\n/*\nSOLARIZED HEX\n--------- -------\nbase03    #002b36\nbase02    #073642\nbase01    #586e75\nbase00    #657b83\nbase0     #839496\nbase1     #93a1a1\nbase2     #eee8d5\nbase3     #fdf6e3\nyellow    #b58900\norange    #cb4b16\nred       #dc322f\nmagenta   #d33682\nviolet    #6c71c4\nblue      #268bd2\ncyan      #2aa198\ngreen     #859900\n*/\n\ncode[class*=\"language-\"],\npre[class*=\"language-\"] {\n\tcolor: #657b83; /* base00 */\n\tfont-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;\n\tfont-size: 1em;\n\ttext-align: left;\n\twhite-space: pre;\n\tword-spacing: normal;\n\tword-break: normal;\n\tword-wrap: normal;\n\n\tline-height: 1.5;\n\n\t-moz-tab-size: 4;\n\t-o-tab-size: 4;\n\ttab-size: 4;\n\n\t-webkit-hyphens: none;\n\t-moz-hyphens: none;\n\t-ms-hyphens: none;\n\thyphens: none;\n}\n\npre[class*=\"language-\"]::-moz-selection, pre[class*=\"language-\"] ::-moz-selection,\ncode[class*=\"language-\"]::-moz-selection, code[class*=\"language-\"] ::-moz-selection {\n\tbackground: #073642; /* base02 */\n}\n\npre[class*=\"language-\"]::selection, pre[class*=\"language-\"] ::selection,\ncode[class*=\"language-\"]::selection, code[class*=\"language-\"] ::selection {\n\tbackground: #073642; /* base02 */\n}\n\n/* Code blocks */\npre[class*=\"language-\"] {\n\tpadding: 1em;\n\tmargin: .5em 0;\n\toverflow: auto;\n\tborder-radius: 0.3em;\n}\n\n:not(pre) > code[class*=\"language-\"],\npre[class*=\"language-\"] {\n\tbackground-color: #f0f0f1; /* base3 */\n}\n\n/* Inline code */\n:not(pre) > code[class*=\"language-\"] {\n\tpadding: .1em;\n\tborder-radius: .3em;\n}\n\n.token.comment,\n.token.prolog,\n.token.doctype,\n.token.cdata {\n\tcolor: #93a1a1; /* base1 */\n}\n\n.token.punctuation {\n\tcolor: #586e75; /* base01 */\n}\n\n.namespace {\n\topacity: .7;\n}\n\n.token.property,\n.token.tag,\n.token.boolean,\n.token.number,\n.token.constant,\n.token.symbol,\n.token.deleted {\n\tcolor: #268bd2; /* blue */\n}\n\n.token.selector,\n.token.attr-name,\n.token.string,\n.token.char,\n.token.builtin,\n.token.url,\n.token.inserted {\n\tcolor: #2aa198; /* cyan */\n}\n\n.token.entity {\n\tcolor: #657b83; /* base00 */\n\tbackground: #eee8d5; /* base2 */\n}\n\n.token.atrule,\n.token.attr-value,\n.token.keyword {\n\tcolor: #859900; /* green */\n}\n\n.token.function,\n.token.class-name {\n\tcolor: #b58900; /* yellow */\n}\n\n.token.regex,\n.token.important,\n.token.variable {\n\tcolor: #cb4b16; /* orange */\n}\n\n.token.important,\n.token.bold {\n\tfont-weight: bold;\n}\n.token.italic {\n\tfont-style: italic;\n}\n\n.token.entity {\n\tcursor: help;\n}\n"
  },
  {
    "path": "docs/assets/css/prism_custom.scss",
    "content": ".token.atrule, .token.attr-value, .token.keyword {\n  color: #07a7fd;\n}\n\n.token.function, .token.class-name {\n  color: #0352c2;\n}\n\n.token.selector, .token.attr-name, .token.string, .token.char, .token.builtin, .token.url, .token.inserted {\n  color: #7fae17;\n}\n\ncode[class*=\"language-\"], pre[class*=\"language-\"] {\n  color: #1e252f;\n}\n\n.token.punctuation {\n  color: #1e252f;\n}\n\n.token.regex, .token.important, .token.variable {\n  color: #ea1473;\n}\n\n\n.highlight pre[class*=\"language-\"] {\n  line-height: 25px;\n}"
  },
  {
    "path": "docs/assets/css/styles.scss",
    "content": "---\n# Frontmatter\n---\n\n@import \"_variables\";\n@import \"bootstrap/scss/bootstrap\";\n@import \"global\";\n@import \"components\";\n@import \"collection_browser\";\n@import \"examples\";\n@import \"subpage\";\n@import \"prism_custom\";\n\n@import \"pages/cookie-policy\";\n@import \"pages/home\";\n@import \"pages/support\";\n@import \"pages/contact\";\n\n@import \"utilities\";\n\nbody.index-page {\n  .header-shapes-bottom {\n    background-image: url('{{site.baseurl}}/assets/img/home/terratest_top_right.svg');\n  }\n}\n"
  },
  {
    "path": "docs/assets/css/subpage.scss",
    "content": ".subpage {\n  .subpage-top-spacing.main.subpage__main {\n    padding-top: 120px;\n  }\n\n  .subpage__header {\n    background: $primary-gradient-start-color;\n    background: -webkit-linear-gradient(-45deg, $primary-gradient-start-color, $primary-gradient-stop-color);\n    background: linear-gradient(135deg, $primary-gradient-start-color, $primary-gradient-stop-color);\n    position: relative;\n    margin-top: 0;\n    padding-top: 30px;\n    padding-bottom: 70px;\n    z-index: 1;\n\n    .subpage__hero {\n      position: relative;\n      z-index: 1;\n      text-align: center;\n\n      h1 {\n        font-weight: bold;\n        margin-bottom: 5px;\n      }\n\n      .subtitle {\n        color: #fff;\n        font-size: 22px;\n      }\n    }\n\n    .header {\n      z-index: 20;\n      position: relative;\n    }\n\n    .overflow-hide {\n      overflow: hidden;\n      width: 100%;\n      height: 100%;\n      max-width: 100vw;\n      position: relative;\n    }\n\n    .header-bg {\n      position: absolute;\n      width: 100%;\n      height: 100%;\n      left: 0;\n      top: 0;\n      z-index: 0;\n      max-width: 100vw;\n\n      .header-shapes-left {\n        z-index: 0;\n        position: absolute;\n        top: 50%;\n        margin-top: -63px;\n        width: 400px;\n        margin-left: 0;\n        left: 0;\n        transition: all 0.2s ease;\n\n        @media all and (max-width: 410px) {\n          max-width: 400px;\n          width: 90%;\n        }\n      }\n\n      .header-shapes-right {\n        z-index: 0;\n        position: absolute;\n        right: 10%;\n        width: 230px;\n        bottom: -50px;\n        transition: all 0.5s ease;\n      }\n    }\n  }\n\n  .main {\n    padding-top: 15px;\n    padding-bottom: 60px;\n\n    &.subpage__main {\n      padding: 30px 15px 120px;\n      max-width: 800px;\n      margin: auto;\n    }\n\n    h1, h2, h3, h4, h5, h6 {\n      font-weight: bold;\n      margin-bottom: 5px;\n      margin-top: 50px;\n    }\n\n    h1 {\n      font-size: 35px;\n    }\n\n    h2 {\n      font-size: 32px;\n    }\n\n    h3 {\n      font-size: 29px;\n    }\n\n    h4 {\n      font-size: 23px;\n    }\n\n    h5 {\n      font-size: 16px;\n    }\n\n    h6 {\n      font-size: 13px;\n    }\n\n    ul {\n      margin-top: 15px;\n\n      li p {\n        margin-bottom: 0px;\n      }\n    }\n  }\n}\n\n@media all and (max-width: 768px) {\n  .header-shapes-right {\n    display: none;\n  }\n}\n"
  },
  {
    "path": "docs/assets/css/utilities.scss",
    "content": ".text-red {\n  color: #f44336;\n}\n\n.text-pink {\n  color: #e91e63;\n}\n\n.text-purple {\n  color: #9c27b0;\n}\n\n.text-deep-purple {\n  color: #673ab7;\n}\n\n.text-indigo {\n  color: #3f51b5;\n}\n\n.text-blue {\n  color: #2196f3;\n}\n\n.text-light-blue {\n  color: #03a9f4;\n}\n\n.text-cyan {\n  color: #00bcd4;\n}\n\n.text-teal {\n  color: #009688;\n}\n\n.text-green {\n  color: #4caf50;\n}\n\n.text-light-green {\n  color: #8bc34a;\n}\n\n.text-lime {\n  color: #cddc39;\n}\n\n.text-yellow {\n  color: #ffeb3b;\n}\n\n.text-amber {\n  color: #ffc107;\n}\n\n.text-orange {\n  color: #ff9800;\n}\n\n.text-deep-orange {\n  color: #ff5722;\n}\n\n.text-brown {\n  color: #795548;\n}\n\n.text-gray {\n  color: #545454;\n}\n\n.text-default {\n  color: #1e252f;\n\n  > a {\n    color: #1e252f;\n\n    &:hover, &:focus {\n      color: #1e252f;\n    }\n  }\n}\n\n.text-white, .link-white, .text-white > a, .link-white > a {\n  color: #fff;\n}\n\na {\n  &.text-white, &.link-white {\n    color: #fff;\n  }\n}\n\n.text-white:hover, .link-white:hover, .text-white > a:hover, .link-white > a:hover {\n  color: #fff;\n}\n\na {\n  &.text-white:hover, &.link-white:hover {\n    color: #fff;\n  }\n}\n\n.text-white:focus, .link-white:focus, .text-white > a:focus, .link-white > a:focus {\n  color: #fff;\n}\n\na {\n  &.text-white:focus, &.link-white:focus {\n    color: #fff;\n  }\n}\n\n.link-muted {\n  color: rgba(10, 12, 16, 0.6);\n\n  > a {\n    color: rgba(10, 12, 16, 0.6);\n  }\n}\n\n.text-muted > a, a.text-muted {\n  color: rgba(10, 12, 16, 0.6);\n}\n\n.link-muted {\n  &:hover, > a:hover {\n    color: rgba(30, 37, 47, 0.6);\n  }\n}\n\n.text-muted > a:hover, a.text-muted:hover {\n  color: rgba(30, 37, 47, 0.6);\n}\n\n.link-muted {\n  &:focus, > a:focus {\n    color: rgba(30, 37, 47, 0.6);\n  }\n}\n\n.text-muted > a:focus, a.text-muted:focus {\n  color: rgba(30, 37, 47, 0.6);\n}\n\n@media (max-width: 992px) {\n  .text-sm-center {\n    text-align: center;\n  }\n}\n\nbody {\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n  font-weight: 400;\n}\n\n.text-normal {\n  font-weight: 400;\n}\n\n.text-bold {\n  font-weight: 700;\n}\n\n.text-lg {\n  font-size: 20px;\n}\n\n.big {\n  font-size: 115%;\n}\n\n.link-decoration-none {\n  text-decoration: none;\n\n  &:hover, &:focus {\n    text-decoration: none;\n  }\n}\n\nh1 a {\n  color: inherit;\n  text-decoration: none;\n\n  &:focus, &:hover {\n    color: inherit;\n    text-decoration: none;\n  }\n}\n\nh2 a {\n  color: inherit;\n  text-decoration: none;\n\n  &:focus, &:hover {\n    color: inherit;\n    text-decoration: none;\n  }\n}\n\nh3 a {\n  color: inherit;\n  text-decoration: none;\n\n  &:focus, &:hover {\n    color: inherit;\n    text-decoration: none;\n  }\n}\n\nh4 a {\n  color: inherit;\n  text-decoration: none;\n\n  &:focus, &:hover {\n    color: inherit;\n    text-decoration: none;\n  }\n}\n\nh5 a {\n  color: inherit;\n  text-decoration: none;\n\n  &:focus, &:hover {\n    color: inherit;\n    text-decoration: none;\n  }\n}\n\nh6 a {\n  color: inherit;\n  text-decoration: none;\n\n  &:focus, &:hover {\n    color: inherit;\n    text-decoration: none;\n  }\n}\n\n.text-tiny {\n  font-size: 12px;\n}\n\n.text-large {\n  font-size: 22px;\n  line-height: 1.33;\n}\n\n\n.margin-top-lg {\n  margin-top: 30px;\n}\n\n.margin-top-xlg {\n  margin-top: 50px;\n}\n\n.margin-top-none {\n  margin-top: 0 !important;\n}\n\n.margin-bottom-none {\n  margin-bottom: 0 !important;\n}\n\n.padding-top-none {\n  padding-top: 0 !important;\n}\n\n.padding-bottom-none {\n  padding-bottom: 0 !important;\n}\n\n.page-header {\n  padding-bottom: 21.6px;\n  margin: 60px 0 45px;\n\n  &.page-header-sm {\n    margin: 40.5px 0 21.6px;\n    padding-bottom: 10.8px;\n  }\n}\n\n.header, .main {\n  background-color: transparent;\n}\n\n.pull-none {\n  float: none;\n}\n\n.container .container {\n  width: 100%;\n}\n\n.row img {\n  max-width: 100%;\n}\n\n.icon-flipped {\n  transform: scaleX(-1);\n  -moz-transform: scaleX(-1);\n  -webkit-transform: scaleX(-1);\n  -ms-transform: scaleX(-1);\n}\n\n.d-inline-block {\n  display: inline-block;\n}\n\n.flex {\n  display: -webkit-flex;\n  display: -moz-flex;\n  display: -ms-flex;\n  display: -o-flex;\n  display: flex;\n}\n\n.flex-column {\n  -webkit-flex-direction: column;\n  -ms-flex-direction: column;\n  flex-direction: column;\n}\n\n.flex-middle {\n  -webkit-justify-content: center;\n  justify-content: center;\n}\n\n.flex-full {\n  -webkit-flex: 1 1 auto;\n  -ms-flex: 1 1 auto;\n  flex: 1 1 auto;\n}\n\n.flex-1 {\n  -webkit-flex: 1;\n  -ms-flex: 1;\n  flex: 1;\n}\n\n.underline {\n  text-decoration: underline;\n}\n\n.nowrap-white-space {\n  white-space: nowrap;\n}\n"
  },
  {
    "path": "docs/assets/img/favicon/browserconfig.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<browserconfig><msapplication><tile><square70x70logo src=\"/assets/img/favicon/ms-icon-70x70.png\"/><square150x150logo src=\"/assets/img/favicon/ms-icon-150x150.png\"/><square310x310logo src=\"/assets/img/favicon/ms-icon-310x310.png\"/><TileColor>#ffffff</TileColor></tile></msapplication></browserconfig>"
  },
  {
    "path": "docs/assets/img/favicon/manifest.json",
    "content": "{\n \"name\": \"App\",\n \"icons\": [\n  {\n   \"src\": \"/assets/img/favicon/android-icon-36x36.png\",\n   \"sizes\": \"36x36\",\n   \"type\": \"image\\/png\",\n   \"density\": \"0.75\"\n  },\n  {\n   \"src\": \"/assets/img/favicon/android-icon-48x48.png\",\n   \"sizes\": \"48x48\",\n   \"type\": \"image\\/png\",\n   \"density\": \"1.0\"\n  },\n  {\n   \"src\": \"/assets/img/favicon/android-icon-72x72.png\",\n   \"sizes\": \"72x72\",\n   \"type\": \"image\\/png\",\n   \"density\": \"1.5\"\n  },\n  {\n   \"src\": \"/assets/img/favicon/android-icon-96x96.png\",\n   \"sizes\": \"96x96\",\n   \"type\": \"image\\/png\",\n   \"density\": \"2.0\"\n  },\n  {\n   \"src\": \"/assets/img/favicon/android-icon-144x144.png\",\n   \"sizes\": \"144x144\",\n   \"type\": \"image\\/png\",\n   \"density\": \"3.0\"\n  },\n  {\n   \"src\": \"/assets/img/favicon/android-icon-192x192.png\",\n   \"sizes\": \"192x192\",\n   \"type\": \"image\\/png\",\n   \"density\": \"4.0\"\n  }\n ]\n}"
  },
  {
    "path": "docs/assets/js/collection-browser_scroll.js",
    "content": "$(document).ready(function () {\n\n  const getElementForDataSelector = function (parentElement, selectorName, elementName) {\n    const selector = parentElement.data(selectorName);\n    if (!selector) {\n      throw new Error(`You must specify a 'data-${selectorName}' attribute for '${elementName}'.`);\n    }\n\n    const element = $(selector);\n    if (element.length !== 1) {\n      throw new Error(`Expected one element that matched selector '${selector}' for '${elementName}' but got ${element.length}`);\n    }\n\n    return element;\n  };\n\n  // Move the TOC on the left side of the page with the user as the user scrolls down, so the TOC is always visible.\n  // Only start moving the TOC once the user has scrolled past the element specified in scroll-after-selector. Stop\n  // moving it at the bottom of the content.\n  const moveToCWithScrolling = function () {\n    const sidebar = $(\".js-scroll-with-user\");\n\n    const scrollAfter = getElementForDataSelector(sidebar, 'scroll-after-selector', 'moveTocWithScrolling');\n    const scrollUntil = getElementForDataSelector(sidebar, 'scroll-until-selector', 'moveTocWithScrolling');\n\n    const scrollPosition = $(window).scrollTop();\n    const scrollAfterHeightBottom = scrollAfter.offset().top + scrollAfter.innerHeight();\n\n    const contentHeight = scrollUntil.innerHeight() + scrollAfterHeightBottom;\n    const sidebarHeight = sidebar.height();\n    const sidebarBottomPos = scrollPosition + sidebarHeight;\n\n    // Only start moving the TOC once we're past the scroll-after item\n    if (scrollPosition >= scrollAfterHeightBottom) {\n      // Stop moving the TOC when we're at the bottom of the content\n      if (sidebarBottomPos >= contentHeight) {\n        sidebar.removeClass('fixed');\n        sidebar.addClass('bottom');\n      } else {\n        sidebar.addClass('fixed');\n        sidebar.removeClass('bottom');\n      }\n    } else {\n      sidebar.removeClass('fixed');\n      sidebar.removeClass('bottom');\n    }\n  };\n\n  // Show a dot next to the part of the TOC where the user has scrolled to. We can't use bootstrap's built-in ScrollSpy\n  // because with Bootstrap 3.3.7, it only works with a Bootstrap Nav, whereas our TOC is auto-generated and does not\n  // use Bootstrap Nav classes/markup.\n  const scrollSpy = function () {\n    const content = $(\".js-scroll-spy\");\n\n    const nav = getElementForDataSelector(content, 'scroll-spy-nav-selector', 'scrollSpy');\n\n    const allNavLinks = nav.find('a');\n    allNavLinks.removeClass('selected');\n\n    // Only consider an item in view if it's visible in the top 20% of the screen\n    const buffer = $(window).height() / 5;\n    const scrollPosition = $(window).scrollTop();\n    const contentHeadings = content.find('h2, h3, h4');\n    const visibleHeadings = contentHeadings.filter((index, el) => scrollPosition + buffer >= $(el).offset().top);\n\n    if (visibleHeadings.length > 0) {\n      const selectedHeading = visibleHeadings.last();\n      const selectedHeadingId = selectedHeading.attr('id');\n\n      if (selectedHeadingId) {\n        const hash = `#${selectedHeadingId}`;\n        const selectedNavLink = nav.find(`a[href$='${hash}']`);\n        if (selectedNavLink.length > 0) {\n          selectedNavLink.addClass('selected');\n\n          const allTopLevelNavListItems = nav.find('.sectlevel1 > li');\n\n          const parentNavListItem = selectedNavLink.parents('.sectlevel2').parent();\n          const topLevelNavListItem = selectedNavLink.parents('.sectlevel1');\n        }\n      }\n    }\n  };\n\n\n\n  $(window).scroll(moveToCWithScrolling);\n  $(moveToCWithScrolling);\n\n  $(window).scroll(scrollSpy);\n  $(scrollSpy);\n\n  $('.post-detail img').on('click', function () {\n    window.open(this.src, '_blank')\n  })\n});\n"
  },
  {
    "path": "docs/assets/js/collection-browser_search.js",
    "content": "/**\n * Javascript for the Collection Browser search.\n *\n * TOC:\n *  - FILTER FUNCTIONS - functions to extract the terms from DOM element(s) and use them in search engine to show/hide elements.\n *  - MAIN - INITIALIZE - initializes Browser Search and registers actions (click etc.) on filter components.\n *  - SEARCH ENGINE - here is the logic to show & hide elements accoriding to filters terms.\n *  - OTHER\n */\n(function () {\n\n  /** ********************************************************************* **/\n  /** *********                 FILTER FUNCTIONS                  ********* **/\n  /** ********************************************************************* **/\n\n  /**\n   * Note\n   * This function is wrapped in a \"debounce\" so that if the user is typing quickly, we aren't trying to run searches\n   * (and fire Google Analytics events!) on every key stroke, but only when they pause from typing.\n   * @type {Function}\n   */\n  const searchInputFilter = debounce(function (event) {\n    const target = $(event.currentTarget)\n    const collectionName = target.data('collection_name')\n    filterSearchData(collectionName)\n  }, 250);\n\n\n  /** ********************************************************************* **/\n  /** *********               MAIN - INITIALIZE                   ********* **/\n  /** ********************************************************************* **/\n\n  /**\n  * Bind actions to search input and tags.\n  * If you want to add more filter components (like cloud providers on gruntwork.io/guides)\n  * you can register actions on them here.\n  */\n  function initializeCbSearch(el) {\n    const collectionName = el.data('collection_name')\n\n    /* SEARCH INPUT box on page */\n    $('#cb-search-box-'+collectionName).on(\"keyup\", searchInputFilter);\n\n    /* Triggered when TAGS filter checkboxes are checked */\n    $(document)\n      .on('click', '[data-collection_name=\"'+collectionName+'\"] .tags', function () {\n        filterSearchData(collectionName);\n      });\n  }\n\n  /* Find collection browser's search component on the page and initialize: */\n  if($('.cb-search-cmp').length > 0) {\n    $('.cb-search-cmp').each(function () {\n      initializeCbSearch($(this))\n      showNoResults(false)\n    })\n  }\n\n  /** ********************************************************************* **/\n  /** *********                 SEARCH ENGINE                     ********* **/\n  /** ********************************************************************* **/\n\n  /**\n   * Filters posts/docs/entries against search input and tags.\n   * It always gets terms from both: search input and tags whenever any of them changed.\n   */\n  function filterSearchData(collectionName) {\n    // Get data from all filter components\n    // a) Get Search input:\n    const searchInputValue = $('#cb-search-box-'+collectionName).val().toLowerCase().split(\" \").filter(v => v != '')\n    // b) Get tags:\n    let checkedTags = []\n    $('[data-collection_name=\"'+collectionName+'\"] input[type=\"checkbox\"]:checked')\n      .each(function () {\n        checkedTags.push($(this).val())\n      })\n\n    // If there is no filter terms, show all posts. Otherwise, filter posts:\n    if (searchInputValue.length === 0 && checkedTags.length === 0) {\n      showNoResults(false)\n      showAll()\n    } else {\n      // Get the list of posts and categories  to show\n      const toShow = filterDocs(collectionName, searchInputValue, checkedTags)\n\n      // If there is no posts to show, display no-results component\n      if (toShow.docs.length === 0) {\n        hideAll()\n        showNoResults(true)\n      } else {\n        // Hide no-results component\n        showNoResults(false)\n        // Hide all elements\n        hideAll()\n        // Show elements\n        toShow.docs.forEach(docId => {\n          showDoc(docId)\n        })\n        toShow.categories.forEach(catId => {\n          showCategory(catId)\n        })\n      }\n    }\n  }\n\n  /**\n   * Filter docs (entries) against search input and checked tags\n   * It returns list of documents and categories to show (satisfying search terms).\n   */\n  function filterDocs(collectionName, searchInputValue, checkedTags) {\n    // Fetch docs data\n    const docs = fetchDocsData(collectionName)\n    let toShowDocs = []\n    let toShowCategories = []\n    // Check each doc's data against search value and selected tags:\n    docs.forEach(doc => {\n      if (containsText(doc, searchInputValue) && containsTag(doc, checkedTags)) {\n        toShowDocs.push(doc.id)\n        if (toShowCategories.indexOf(doc.category) === -1) {\n          toShowCategories.push(doc.category.replace(/\\s+/g, '-'))\n        }\n      }\n    })\n    return { docs: toShowDocs, categories: toShowCategories }\n  }\n\n  function containsTerms(content, terms) {\n    let allMatches = true\n    terms.forEach(term => {\n      if (content.indexOf(term.toLowerCase()) < 0) {\n        allMatches = false\n      }\n    })\n    return allMatches\n  }\n\n  function containsText(doc, terms) {\n    const content = doc.text || doc.title + \" \" + doc.excerpt + \" \" + doc.category + \" \" + doc.content + \" \" + doc.tags\n    return containsTerms(content, terms)\n  }\n\n  function containsTag(doc, terms) {\n    const content = doc.tags\n    return containsTerms(content, terms)\n  }\n\n  /**\n   * Function to fetch posts/docs data.\n   * Now it gets from window, but it can be transformed to get it from API.\n   */\n  function fetchDocsData(collectionName) {\n    return window['bc_'+collectionName+'Entries']\n  }\n\n  /** ********************************************************************* **/\n  /** *********                 OTHER                             ********* **/\n  /** ********************************************************************* **/\n\n  // Returns a function, that, as long as it continues to be invoked, will not be\n  // triggered. The function will be called after it stops being called for N\n  // milliseconds. If `immediate` is passed, trigger the function on the leading\n  // edge, instead of the trailing. Ensures a given task doesn't fire so often\n  // that it bricks browser performance. From:\n  // https://davidwalsh.name/javascript-debounce-function\n  function debounce(func, wait, immediate) {\n    let timeout\n    return function () {\n      const context = this,\n        args = arguments\n      const later = function () {\n        timeout = null\n        if (!immediate)\n          func.apply(context, args)\n      };\n      const callNow = immediate && !timeout\n      clearTimeout(timeout)\n      timeout = setTimeout(later, wait)\n      if (callNow)\n        func.apply(context, args)\n    };\n  }\n\n  /**\n   * Functions to show & hide items on the page\n   */\n  function showAll() {\n    $('.cb-doc-card').show()\n    $('.category-head').show()\n    $('.categories ul li').show()\n  }\n\n  function hideAll() {\n    $('.cb-doc-card').hide()\n    $('.category-head').hide()\n    $('.categories ul li').hide()\n  }\n\n  function showDoc(docId) {\n    $('#' + docId + '.cb-doc-card').show()\n  }\n\n  function showCategory(categoryId) {\n    $(`.categories ul [data-category=${categoryId}]`).show()\n    $(`#${categoryId}.category-head`).show()\n  }\n\n  /**\n   * Show / hide no-results component\n   */\n   function showNoResults(state) {\n     if (state) {\n       $('#no-matches').show()\n     } else {\n       $('#no-matches').hide()\n     }\n   }\n\n}());\n"
  },
  {
    "path": "docs/assets/js/collection-browser_toc.js",
    "content": "$(document).ready(function () {\n  $('#toc-toggle-open').on('click', function () {\n    $('.col-md-2-5').addClass('opened')\n    $('body').addClass('modal-opened')\n  })\n\n  $('#toc-toggle-close').on('click', function () {\n    $('.col-md-2-5').removeClass('opened')\n    $('body').removeClass('modal-opened')\n  })\n\n  $('#toc a').on('click', function (){\n    $('.col-md-2-5').removeClass('opened')\n    $('body').removeClass('modal-opened')\n  })\n\n  /* Collapsing toc */\n  $('#toc ul ul').addClass('collapse')\n\n  // Change initial icon for nav without children:\n  $('#toc .nav-collapse-handler').each(function () {\n    if ($(this).siblings('ul').length === 0) {\n      $(this).find('.glyphicon').removeClass('glyphicon-triangle-bottom')\n      $(this).find('.glyphicon').addClass('glyphicon-chevron-down')\n      $(this).addClass('no-children')\n    }\n  })\n\n  // Expand / collpase on click\n  $('#toc .nav-collapse-handler').on('click', function() {\n    toggleNav($(this))\n  })\n\n  $(docSidebarInitialExpand)\n})\n\n// Expand / collpase on click\nfunction toggleNav(el) {\n  if (el.hasClass('collapsed')) {\n    if (!el.hasClass('no-children')) {\n      el.removeClass('collapsed')\n      el.siblings('ul').collapse('show')\n    }\n  } else {\n    el.addClass('collapsed')\n    el.siblings('ul').collapse('hide')\n  }\n}\n\nconst docSidebarInitialExpand = function () {\n  const toc = $('#toc')\n  const pathname = window.location.pathname\n  const hash = window.location.hash\n  toc.find('a[href=\"'+pathname+hash+'\"]').each(function(i, nav) {\n    $(nav).parents('ul').each(function(i, el) {\n      $(el).collapse('show')\n      $(el).siblings('span.nav-collapse-handler:not(.no-children)').removeClass('collapsed')\n      $(el).siblings('span.nav-collapse-handler').addClass('active')\n    })\n    $(nav).siblings('span.nav-collapse-handler:not(.no-children)').removeClass('collapsed')\n    $(nav).siblings('span.nav-collapse-handler').addClass('active')\n    $(nav).siblings('ul').collapse('show')\n  })\n}\n"
  },
  {
    "path": "docs/assets/js/contact-form.js",
    "content": "\n/* Contact form */\n$(function() {\n  var submitButton = $(\"#submit-button\");\n  var form = $(\"#contact-form\");\n  \n  selectPlanFromUrl();\n  \n  submitButton.on(\"click\", submitForm);\n  \n  function selectPlanFromUrl() {\n    var params = window.location.search;\n    if (params === \"?plan=enterprise\") {\n      var plan = $(\"#enterprise\");\n      plan.prop('checked', true);\n    } else {\n      var plan = $(\"#pro\");\n      plan.prop('checked', true);\n    }\n  }\n\n  function submitForm(e) {\n    e.preventDefault();\n\n    if(validateForm()) {\n      var data = form.serialize(form.get(0), { hash: true });\n      submitToFormSpree(data);\n    }\n\n    function submitToFormSpree(data) {\n      submitButton.html(\"Sending...\");\n      submitButton.prop(\"disabled\", true);\n\n      var postParams = {\n        url: form.attr('action'),\n        type: \"POST\",\n        data: data,\n        dataType: \"json\"\n      };\n\n      $.ajax(postParams)\n        .done(function() {\n          inCall = false;\n          window.location.replace(\"/thanks\");\n        })\n        .fail(function() {\n          showFormError(\n            \"Oops, something went wrong! Please try again. If the issue persists please email us directly at info@gruntwork.io\"\n          );\n          inCall = false;\n          submitButton.html(\"Submit\");\n          submitButton.prop(\"disabled\", false);\n        });\n    }\n    \n    function showInputError(el) {\n      $(el).addClass(\"has-error\");\n    };\n    \n    function showFormError(message) {\n      $(\"#error-message\").html(\n        '<h3 class=\"text-danger text-center\">' + message + \"</h3>\"\n        );\n      };\n      \n      function clearErrors() {\n        $(\"#error-message\").html(\"\");\n        form.find(\"*\").removeClass(\"has-error\");\n      };\n\n     function validateForm() {\n        var isValid = true;\n    \n        clearErrors();\n    \n        form.find(\"[required]\").each(function(index, el) {\n          if (!$(el).val()) {\n            isValid = false;\n            showInputError(el);\n            showFormError(\"Please fill in all required fields\");\n          }\n        });\n    \n        return isValid;\n      };\n    }\n  });"
  },
  {
    "path": "docs/assets/js/cookie.js",
    "content": "---\n---\n/**\n * Cookie notice\n * @author AKOS\n *\n * This cookie script must load AFTER the Intercom code above to detect the global \"Intercom\" variable\n * and render the cookie notice right after Intercom's script is injected into the <body>. This will ensure\n * that our cookie notice renders ABOVE the Intercom bubble to avoid conflicts with z-index.\n */\n\n(function ($) { \"use strict\";\n  var cookieInnerHtml = '<div><p>By using this website you agree to our <a href=\"{{ site.baseurl }}/cookie-policy/\">cookie policy</a></p><button id=\"cookieModalClose\" class=\"btn btn-primary btn-sm\">OK</button></div>';\n\n  var initCookie = function () {\n\n    // Don't create cookie notice if already acknowledged\n    if (getCookiebyName('GruntyCookie')) {\n      return;\n    }\n\n    // Create the cookie modal\n    var $cookieModal = $('<div />');\n    $cookieModal.attr('id', 'gruntyCookie');\n    $cookieModal.css('z-index', '2147483647');\n    $cookieModal.html(cookieInnerHtml);\n\n    $(document).on('click', '#cookieModalClose', function () {\n      setCookie('GruntyCookie', '1', 365);\n      $cookieModal.hide();\n    });\n\n    $('body').append($cookieModal);\n  };\n\n  initCookie();\n\n})(window.jQuery);\n"
  },
  {
    "path": "docs/assets/js/examples.js",
    "content": "---\n---\n$(document).ready(function () {\n\n  const CODE_LINE_HEIGHT = 22\n  const CODE_BLOCK_PADDING = 10\n\n  window.examples = {\n    tags: {},\n    nav: {}\n  }\n\n  initExamplesNav()\n\n  $(window).resize($.debounce(250, function() {\n     buildExamplesNav()\n  }))\n\n  // Activate first example\n  $('.examples__container').each(function (i, ec) {\n    // Find first element:\n    const firstElementId = $(ec).find('.examples__nav-item').data('id')\n    // Open first element:\n    openExample($(ec).attr('id'), firstElementId)\n\n    // Open example when user clicks on tab\n    $('.navs').on('click', '.examples__nav-item:not(.static-link)', function() {\n      openExample($(ec).attr('id'), $(this).data('id'))\n      $('.navs__dropdown-menu').removeClass('active')\n    })\n  })\n\n  // Open example and scroll to examples section when user clicks on\n  // tech in the header\n  $('.link-to-test-with-terratest').on('click', function() {\n    // Find any containting the keyword from data-target\n    const found = $('.navs .examples__nav-item[data-id*=\"'+$(this).data('target')+'\"]')\n    if (found && found.length > 0) {\n      openExample('index_page', $(found[0]).data('id'))\n    } else {\n      // RESCUE: If none found, open any (first available):\n      openExample('index_page', $($('.navs .examples__nav-item')[0]).data('id'))\n    }\n    scrollToTests()\n  })\n\n  // Switch between code snippets (files)\n  $('.examples__tabs .tab').on('click', function() {\n    $(this).parents('.examples__tabs').find('.tab').removeClass('active')\n    $(this).addClass('active')\n\n    $(this).parents('.examples__block').find('.examples__code').removeClass('active')\n    $($(this).data('target')).addClass('active')\n\n    loadCodeSnippet()\n  })\n\n  // Open dropdown of technologies to select\n  $('.navs__dropdown-arrow').on('click', function() {\n    $('.navs__dropdown-menu').toggleClass('active')\n  })\n\n  // Open popup when user click on circle with the number\n  $('.examples__container').on('click', '.code-popup-handler', function() {\n    const isActive = $(this).hasClass('active')\n    $('.code-popup-handler').removeClass('active')\n    if (!isActive) {\n      $(this).addClass('active')\n    }\n  })\n\n  function scrollToTests() {\n    $([document.documentElement, document.body]).animate({\n        scrollTop: $('#index-page__test-with-terratest').offset().top\n    }, 500)\n  }\n\n  function openExample(exampleContainerId, target) {\n    // Change active nav in window state and rebuild navigation first\n    const $ecId = $('#'+exampleContainerId)\n    window.examples.nav[exampleContainerId].current = target\n    buildExamplesNav()\n\n    // Change active tab in navigation\n    $ecId.find('.examples__nav-item').removeClass('active')\n    const jTarget = $('.navs .examples__nav-item[data-id=\"'+target+'\"]')\n    jTarget.addClass('active')\n\n    // Change the block below navigation (with code snippets)\n    $ecId.find('.examples__block').removeClass('active')\n    $ecId.find('#example__block-' + target).addClass('active')\n\n    // Set current tab\n    $ecId.find('.examples__nav .navs').removeClass('active')\n\n    loadCodeSnippet()\n  }\n\n  function loadCodeSnippet() {\n    $('.examples__block.active .examples__code.active').each(async function (i, activeCodeSnippet) {\n      const $activeCodeSnippet = $(activeCodeSnippet)\n      const exampleTarget = $(this).data('example')\n      const fileId = $(this).data('target')\n      const snippetId = $(this).data('snippet-id')\n      if (!$activeCodeSnippet.data('loaded')) {\n        try {\n          const response = await fetch($activeCodeSnippet.data('url'))\n          let content = await response.text()\n          $activeCodeSnippet.attr('data-loaded', true)\n          if ($activeCodeSnippet.data('skip-tags')) {\n            // Remove the website::tag::xxx:: prefix from the code snippet\n            content = content.replace(/website::tag::.*?:: ?/mg, '')\n          } else {\n            findTags(content, exampleTarget, fileId)\n            // Remove the website::tag::xxx:: comment entirely from the code snippet\n            content = content.replace(/^.*website::tag.*\\n?/mg, '')\n          }\n          // Find the range specified by range-id if specified\n          if (snippetId) {\n            snippet = extractSnippet(content, snippetId)\n            $activeCodeSnippet.find('code').text(snippet)\n          } else {\n            $activeCodeSnippet.find('code').text(content)\n          }\n\n          Prism.highlightAll()\n        } catch(err) {\n          $activeCodeSnippet.find('code').text('Resource could not be loaded.')\n          console.error(err)\n        }\n      }\n      updatePopups()\n      openPopup(exampleTarget, 1)\n    })\n  }\n\n  function extractSnippet(content, snippetId) {\n    // Split the content into an array of lines\n    lines = content.split('\\n')\n    // Search the array for \"snippet-tag-start::{id}\" - save location\n    const startLine = searchTagInLines(`snippet-tag-start::${snippetId}`, lines)\n    // Search the array for \"snippet-tag-end::{id}\" - save location\n    const endLine = searchTagInLines(`snippet-tag-end::${snippetId}`, lines)\n\n    // If you have both a start and end, slice as below\n    if (startLine >= 0 && endLine >= 0) {\n      const range = lines.slice(startLine + 2, endLine)\n      return range.join('\\n')\n    } else {\n      console.error('Could not find specified range.')\n      return content\n    }\n  }\n\n  function searchTagInLines (tagRegExp, lines) {\n    return lines.findIndex(line => line.match(tagRegExp))\n  }\n\n  function findTags(content, exampleTarget, fileId) {\n    let tags = []\n    let regexpTags = /website::tag::(\\d)::\\s*(.*)/mg\n    let match = regexpTags.exec(content)\n    do {\n      if (match && match.length > 0) {\n        tags.push({\n          text: match[2],\n          tag: match[0],\n          step: match[1],\n          line: findLineNumber(content, match[0])\n        })\n      }\n    } while((match = regexpTags.exec(content)) !== null)\n    window.examples.tags[exampleTarget] = Object.assign({\n        [fileId]: tags\n      },\n      window.examples.tags[exampleTarget]\n    )\n  }\n\n  function findLineNumber(content, text) {\n    let tagIndex = content.indexOf(text)\n    let tempString = content.substring(0, tagIndex)\n    let lineNumber = tempString.split('\\n').length\n    return lineNumber\n  }\n\n  function updatePopups() {\n    $('.code-popup-handler').remove()\n    const activeCode = $('.examples__block.active .examples__code.active')\n    const exampleTarget = activeCode.data('example')\n    const fileId = activeCode.data('target')\n    const exampleTargetTags = window.examples.tags[exampleTarget] || {};\n    const fileTags = exampleTargetTags[fileId];\n\n    if (fileTags) {\n      const tagsLen = fileTags.length\n\n      fileTags.map( function(v,k) {\n        const top = (CODE_LINE_HEIGHT * (v.line - k)) + CODE_BLOCK_PADDING;\n\n        // If two pop-ups are close to each other, add CSS class that will scale them down\n        let scaleClass = ''\n        if (\n            (k > 0 && Math.abs(v.line - fileTags[k-1].line) < 3 )\n            || (k < tagsLen - 1 && Math.abs(v.line - fileTags[k+1].line) < 3 )\n        ) {\n          scaleClass = 'sm-scale'\n        }\n\n        const elToAppend =\n            '<div class=\"code-popup-handler '+scaleClass+'\" style=\"top: '+top+'px\" data-step=\"'+v.step+'\">' +\n            '<span class=\"number\">' + v.step + '</span>' +\n            '<div class=\"shadow-bg-1\"></div><div class=\"shadow-bg-2\"></div>' +\n            '<div class=\"popup\">' +\n            '<div class=\"left-border\"></div>' +\n            '<div class=\"content\">' +\n            '<p class=\"text\">' + v.text + '</p>' +\n            '</div>' +\n            '</div>'\n        const code = $(\"#example__code-\"+exampleTarget+\"-\"+fileId)\n        code.append(elToAppend)\n      })\n    }\n\n    openPopup(exampleTarget, 0)\n  }\n\n  function openPopup(techName, step) {\n    $('.code-popup-handler').removeClass('active')\n    $('#example__block-'+techName).find('.code-popup-handler[data-step=\"'+step+'\"]').addClass('active')\n  }\n\n  function loadExampleDescription(name) {\n    return $('#index-page__examples').find('#example__block-'+name+' .description').html()\n  }\n\n  function initExamplesNav() {\n    window.examples.nav = {}\n    $('.examples__container').each(function(eci, ec) {\n      $(ec).find('.examples__nav .hidden-navs').each(function(rni, refNavs) {\n        let navsArr = []\n        let currentNav\n        $(refNavs).find('.examples__nav-item').each( function(ni, nav) {\n          if ($(nav).hasClass('active')) {\n            currentNav = $(nav).data('id')\n          }\n          navsArr.push($(nav))\n        })\n        window.examples.nav = Object.assign({\n          [$(ec).attr('id')]: {\n            current: currentNav,\n            items: navsArr\n          }\n        }, window.examples.nav)\n      })\n    })\n  }\n\n  function buildExamplesNav() {\n    $('.examples__container').each(function(eci, ec) {\n      const ecId = $(ec).attr('id')\n      const containerWidth = $(ec).width()\n      const NAV_WIDTH = 150\n      const ARROW_SLOT_WIDTH = 100\n\n      const noOfVisible = Math.floor((containerWidth - NAV_WIDTH - ARROW_SLOT_WIDTH) / 150)\n\n      const $visibleBar = $($(ec).find('.navs__visible-bar'))\n      const $dropdownInput = $($(ec).find('.navs__dropdown-input'))\n      const $dropdownMenu = $($(ec).find('.navs__dropdown-menu'))\n\n      $visibleBar.html('')\n      $dropdownInput.html('')\n      $dropdownMenu.html('')\n\n      let settingCurrent = false\n\n      // Build initial a navigation bar\n      if (window.examples.nav\n        && ecId in window.examples.nav\n        && window.examples.nav[ecId].items) {\n\n        // Visible elements\n        let breakSlice = noOfVisible > window.examples.nav[ecId].items.length ? window.examples.nav[ecId].items.length : noOfVisible\n        let visibleEls = window.examples.nav[ecId].items.slice(0, breakSlice)\n        let hiddenEls = window.examples.nav[ecId].items.slice(breakSlice, window.examples.nav[ecId].items.length)\n\n        let visibleNavIsActive = false\n        let hiddenNavIsActive = -1\n\n        if (window.examples.nav[ecId].current) {\n          visibleEls.map( function(x,i) {\n            if(x.data('id') === window.examples.nav[ecId].current) {\n              visibleNavIsActive = true\n              x.addClass('active')\n            }\n          })\n          hiddenEls.map( function(x,i) {\n            if(x.data('id') === window.examples.nav[ecId].current) {\n              hiddenNavIsActive = i\n              x.addClass('active')\n            }\n          })\n        }\n\n        visibleEls.map(function(nav,i) {\n          $visibleBar.append($(nav).clone())\n        })\n\n        if (hiddenNavIsActive > -1) {\n          const sliced = hiddenEls.splice(hiddenNavIsActive, 1)\n          $dropdownInput.append($(sliced[0]).clone())\n        } else {\n          $dropdownInput.append($(hiddenEls.shift()).clone())\n        }\n\n        hiddenEls.map(function(nav,i) {\n          $dropdownMenu.append($(nav).clone())\n        })\n\n        // Add static links\n        $dropdownMenu.append($(ec).find('.hidden-navs__static-links').html())\n      }\n    })\n  }\n\n})\n"
  },
  {
    "path": "docs/assets/js/main.js",
    "content": "/*! jQuery v1.12.4 | (c) jQuery Foundation | jquery.org/license */\n!function(a,b){\"object\"==typeof module&&\"object\"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error(\"jQuery requires a window with a document\");return b(a)}:b(a)}(\"undefined\"!=typeof window?window:this,function(a,b){var c=[],d=a.document,e=c.slice,f=c.concat,g=c.push,h=c.indexOf,i={},j=i.toString,k=i.hasOwnProperty,l={},m=\"1.12.4\",n=function(a,b){return new n.fn.init(a,b)},o=/^[\\s\\uFEFF\\xA0]+|[\\s\\uFEFF\\xA0]+$/g,p=/^-ms-/,q=/-([\\da-z])/gi,r=function(a,b){return b.toUpperCase()};n.fn=n.prototype={jquery:m,constructor:n,selector:\"\",length:0,toArray:function(){return e.call(this)},get:function(a){return null!=a?0>a?this[a+this.length]:this[a]:e.call(this)},pushStack:function(a){var b=n.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a){return n.each(this,a)},map:function(a){return this.pushStack(n.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(e.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor()},push:g,sort:c.sort,splice:c.splice},n.extend=n.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for(\"boolean\"==typeof g&&(j=g,g=arguments[h]||{},h++),\"object\"==typeof g||n.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(e=arguments[h]))for(d in e)a=g[d],c=e[d],g!==c&&(j&&c&&(n.isPlainObject(c)||(b=n.isArray(c)))?(b?(b=!1,f=a&&n.isArray(a)?a:[]):f=a&&n.isPlainObject(a)?a:{},g[d]=n.extend(j,f,c)):void 0!==c&&(g[d]=c));return g},n.extend({expando:\"jQuery\"+(m+Math.random()).replace(/\\D/g,\"\"),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return\"function\"===n.type(a)},isArray:Array.isArray||function(a){return\"array\"===n.type(a)},isWindow:function(a){return null!=a&&a==a.window},isNumeric:function(a){var b=a&&a.toString();return!n.isArray(a)&&b-parseFloat(b)+1>=0},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},isPlainObject:function(a){var b;if(!a||\"object\"!==n.type(a)||a.nodeType||n.isWindow(a))return!1;try{if(a.constructor&&!k.call(a,\"constructor\")&&!k.call(a.constructor.prototype,\"isPrototypeOf\"))return!1}catch(c){return!1}if(!l.ownFirst)for(b in a)return k.call(a,b);for(b in a);return void 0===b||k.call(a,b)},type:function(a){return null==a?a+\"\":\"object\"==typeof a||\"function\"==typeof a?i[j.call(a)]||\"object\":typeof a},globalEval:function(b){b&&n.trim(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(p,\"ms-\").replace(q,r)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b){var c,d=0;if(s(a)){for(c=a.length;c>d;d++)if(b.call(a[d],d,a[d])===!1)break}else for(d in a)if(b.call(a[d],d,a[d])===!1)break;return a},trim:function(a){return null==a?\"\":(a+\"\").replace(o,\"\")},makeArray:function(a,b){var c=b||[];return null!=a&&(s(Object(a))?n.merge(c,\"string\"==typeof a?[a]:a):g.call(c,a)),c},inArray:function(a,b,c){var d;if(b){if(h)return h.call(b,a,c);for(d=b.length,c=c?0>c?Math.max(0,d+c):c:0;d>c;c++)if(c in b&&b[c]===a)return c}return-1},merge:function(a,b){var c=+b.length,d=0,e=a.length;while(c>d)a[e++]=b[d++];if(c!==c)while(void 0!==b[d])a[e++]=b[d++];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,e,g=0,h=[];if(s(a))for(d=a.length;d>g;g++)e=b(a[g],g,c),null!=e&&h.push(e);else for(g in a)e=b(a[g],g,c),null!=e&&h.push(e);return f.apply([],h)},guid:1,proxy:function(a,b){var c,d,f;return\"string\"==typeof b&&(f=a[b],b=a,a=f),n.isFunction(a)?(c=e.call(arguments,2),d=function(){return a.apply(b||this,c.concat(e.call(arguments)))},d.guid=a.guid=a.guid||n.guid++,d):void 0},now:function(){return+new Date},support:l}),\"function\"==typeof Symbol&&(n.fn[Symbol.iterator]=c[Symbol.iterator]),n.each(\"Boolean Number String Function Array Date RegExp Object Error Symbol\".split(\" \"),function(a,b){i[\"[object \"+b+\"]\"]=b.toLowerCase()});function s(a){var b=!!a&&\"length\"in a&&a.length,c=n.type(a);return\"function\"===c||n.isWindow(a)?!1:\"array\"===c||0===b||\"number\"==typeof b&&b>0&&b-1 in a}var t=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u=\"sizzle\"+1*new Date,v=a.document,w=0,x=0,y=ga(),z=ga(),A=ga(),B=function(a,b){return a===b&&(l=!0),0},C=1<<31,D={}.hasOwnProperty,E=[],F=E.pop,G=E.push,H=E.push,I=E.slice,J=function(a,b){for(var c=0,d=a.length;d>c;c++)if(a[c]===b)return c;return-1},K=\"checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped\",L=\"[\\\\x20\\\\t\\\\r\\\\n\\\\f]\",M=\"(?:\\\\\\\\.|[\\\\w-]|[^\\\\x00-\\\\xa0])+\",N=\"\\\\[\"+L+\"*(\"+M+\")(?:\"+L+\"*([*^$|!~]?=)\"+L+\"*(?:'((?:\\\\\\\\.|[^\\\\\\\\'])*)'|\\\"((?:\\\\\\\\.|[^\\\\\\\\\\\"])*)\\\"|(\"+M+\"))|)\"+L+\"*\\\\]\",O=\":(\"+M+\")(?:\\\\((('((?:\\\\\\\\.|[^\\\\\\\\'])*)'|\\\"((?:\\\\\\\\.|[^\\\\\\\\\\\"])*)\\\")|((?:\\\\\\\\.|[^\\\\\\\\()[\\\\]]|\"+N+\")*)|.*)\\\\)|)\",P=new RegExp(L+\"+\",\"g\"),Q=new RegExp(\"^\"+L+\"+|((?:^|[^\\\\\\\\])(?:\\\\\\\\.)*)\"+L+\"+$\",\"g\"),R=new RegExp(\"^\"+L+\"*,\"+L+\"*\"),S=new RegExp(\"^\"+L+\"*([>+~]|\"+L+\")\"+L+\"*\"),T=new RegExp(\"=\"+L+\"*([^\\\\]'\\\"]*?)\"+L+\"*\\\\]\",\"g\"),U=new RegExp(O),V=new RegExp(\"^\"+M+\"$\"),W={ID:new RegExp(\"^#(\"+M+\")\"),CLASS:new RegExp(\"^\\\\.(\"+M+\")\"),TAG:new RegExp(\"^(\"+M+\"|[*])\"),ATTR:new RegExp(\"^\"+N),PSEUDO:new RegExp(\"^\"+O),CHILD:new RegExp(\"^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\\\(\"+L+\"*(even|odd|(([+-]|)(\\\\d*)n|)\"+L+\"*(?:([+-]|)\"+L+\"*(\\\\d+)|))\"+L+\"*\\\\)|)\",\"i\"),bool:new RegExp(\"^(?:\"+K+\")$\",\"i\"),needsContext:new RegExp(\"^\"+L+\"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\\\(\"+L+\"*((?:-\\\\d)?\\\\d*)\"+L+\"*\\\\)|)(?=[^-]|$)\",\"i\")},X=/^(?:input|select|textarea|button)$/i,Y=/^h\\d$/i,Z=/^[^{]+\\{\\s*\\[native \\w/,$=/^(?:#([\\w-]+)|(\\w+)|\\.([\\w-]+))$/,_=/[+~]/,aa=/'|\\\\/g,ba=new RegExp(\"\\\\\\\\([\\\\da-f]{1,6}\"+L+\"?|(\"+L+\")|.)\",\"ig\"),ca=function(a,b,c){var d=\"0x\"+b-65536;return d!==d||c?b:0>d?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},da=function(){m()};try{H.apply(E=I.call(v.childNodes),v.childNodes),E[v.childNodes.length].nodeType}catch(ea){H={apply:E.length?function(a,b){G.apply(a,I.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function fa(a,b,d,e){var f,h,j,k,l,o,r,s,w=b&&b.ownerDocument,x=b?b.nodeType:9;if(d=d||[],\"string\"!=typeof a||!a||1!==x&&9!==x&&11!==x)return d;if(!e&&((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,p)){if(11!==x&&(o=$.exec(a)))if(f=o[1]){if(9===x){if(!(j=b.getElementById(f)))return d;if(j.id===f)return d.push(j),d}else if(w&&(j=w.getElementById(f))&&t(b,j)&&j.id===f)return d.push(j),d}else{if(o[2])return H.apply(d,b.getElementsByTagName(a)),d;if((f=o[3])&&c.getElementsByClassName&&b.getElementsByClassName)return H.apply(d,b.getElementsByClassName(f)),d}if(c.qsa&&!A[a+\" \"]&&(!q||!q.test(a))){if(1!==x)w=b,s=a;else if(\"object\"!==b.nodeName.toLowerCase()){(k=b.getAttribute(\"id\"))?k=k.replace(aa,\"\\\\$&\"):b.setAttribute(\"id\",k=u),r=g(a),h=r.length,l=V.test(k)?\"#\"+k:\"[id='\"+k+\"']\";while(h--)r[h]=l+\" \"+qa(r[h]);s=r.join(\",\"),w=_.test(a)&&oa(b.parentNode)||b}if(s)try{return H.apply(d,w.querySelectorAll(s)),d}catch(y){}finally{k===u&&b.removeAttribute(\"id\")}}}return i(a.replace(Q,\"$1\"),b,d,e)}function ga(){var a=[];function b(c,e){return a.push(c+\" \")>d.cacheLength&&delete b[a.shift()],b[c+\" \"]=e}return b}function ha(a){return a[u]=!0,a}function ia(a){var b=n.createElement(\"div\");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ja(a,b){var c=a.split(\"|\"),e=c.length;while(e--)d.attrHandle[c[e]]=b}function ka(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&(~b.sourceIndex||C)-(~a.sourceIndex||C);if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function la(a){return function(b){var c=b.nodeName.toLowerCase();return\"input\"===c&&b.type===a}}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return(\"input\"===c||\"button\"===c)&&b.type===a}}function na(a){return ha(function(b){return b=+b,ha(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function oa(a){return a&&\"undefined\"!=typeof a.getElementsByTagName&&a}c=fa.support={},f=fa.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?\"HTML\"!==b.nodeName:!1},m=fa.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=n.documentElement,p=!f(n),(e=n.defaultView)&&e.top!==e&&(e.addEventListener?e.addEventListener(\"unload\",da,!1):e.attachEvent&&e.attachEvent(\"onunload\",da)),c.attributes=ia(function(a){return a.className=\"i\",!a.getAttribute(\"className\")}),c.getElementsByTagName=ia(function(a){return a.appendChild(n.createComment(\"\")),!a.getElementsByTagName(\"*\").length}),c.getElementsByClassName=Z.test(n.getElementsByClassName),c.getById=ia(function(a){return o.appendChild(a).id=u,!n.getElementsByName||!n.getElementsByName(u).length}),c.getById?(d.find.ID=function(a,b){if(\"undefined\"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c?[c]:[]}},d.filter.ID=function(a){var b=a.replace(ba,ca);return function(a){return a.getAttribute(\"id\")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(ba,ca);return function(a){var c=\"undefined\"!=typeof a.getAttributeNode&&a.getAttributeNode(\"id\");return c&&c.value===b}}),d.find.TAG=c.getElementsByTagName?function(a,b){return\"undefined\"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if(\"*\"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){return\"undefined\"!=typeof b.getElementsByClassName&&p?b.getElementsByClassName(a):void 0},r=[],q=[],(c.qsa=Z.test(n.querySelectorAll))&&(ia(function(a){o.appendChild(a).innerHTML=\"<a id='\"+u+\"'></a><select id='\"+u+\"-\\r\\\\' msallowcapture=''><option selected=''></option></select>\",a.querySelectorAll(\"[msallowcapture^='']\").length&&q.push(\"[*^$]=\"+L+\"*(?:''|\\\"\\\")\"),a.querySelectorAll(\"[selected]\").length||q.push(\"\\\\[\"+L+\"*(?:value|\"+K+\")\"),a.querySelectorAll(\"[id~=\"+u+\"-]\").length||q.push(\"~=\"),a.querySelectorAll(\":checked\").length||q.push(\":checked\"),a.querySelectorAll(\"a#\"+u+\"+*\").length||q.push(\".#.+[+~]\")}),ia(function(a){var b=n.createElement(\"input\");b.setAttribute(\"type\",\"hidden\"),a.appendChild(b).setAttribute(\"name\",\"D\"),a.querySelectorAll(\"[name=d]\").length&&q.push(\"name\"+L+\"*[*^$|!~]?=\"),a.querySelectorAll(\":enabled\").length||q.push(\":enabled\",\":disabled\"),a.querySelectorAll(\"*,:x\"),q.push(\",.*:\")})),(c.matchesSelector=Z.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ia(function(a){c.disconnectedMatch=s.call(a,\"div\"),s.call(a,\"[s!='']:x\"),r.push(\"!=\",O)}),q=q.length&&new RegExp(q.join(\"|\")),r=r.length&&new RegExp(r.join(\"|\")),b=Z.test(o.compareDocumentPosition),t=b||Z.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===n||a.ownerDocument===v&&t(v,a)?-1:b===n||b.ownerDocument===v&&t(v,b)?1:k?J(k,a)-J(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,g=[a],h=[b];if(!e||!f)return a===n?-1:b===n?1:e?-1:f?1:k?J(k,a)-J(k,b):0;if(e===f)return ka(a,b);c=a;while(c=c.parentNode)g.unshift(c);c=b;while(c=c.parentNode)h.unshift(c);while(g[d]===h[d])d++;return d?ka(g[d],h[d]):g[d]===v?-1:h[d]===v?1:0},n):n},fa.matches=function(a,b){return fa(a,null,null,b)},fa.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(T,\"='$1']\"),c.matchesSelector&&p&&!A[b+\" \"]&&(!r||!r.test(b))&&(!q||!q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return fa(b,n,null,[a]).length>0},fa.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},fa.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&D.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},fa.error=function(a){throw new Error(\"Syntax error, unrecognized expression: \"+a)},fa.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=fa.getText=function(a){var b,c=\"\",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if(\"string\"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=fa.selectors={cacheLength:50,createPseudo:ha,match:W,attrHandle:{},find:{},relative:{\">\":{dir:\"parentNode\",first:!0},\" \":{dir:\"parentNode\"},\"+\":{dir:\"previousSibling\",first:!0},\"~\":{dir:\"previousSibling\"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(ba,ca),a[3]=(a[3]||a[4]||a[5]||\"\").replace(ba,ca),\"~=\"===a[2]&&(a[3]=\" \"+a[3]+\" \"),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),\"nth\"===a[1].slice(0,3)?(a[3]||fa.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*(\"even\"===a[3]||\"odd\"===a[3])),a[5]=+(a[7]+a[8]||\"odd\"===a[3])):a[3]&&fa.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return W.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||\"\":c&&U.test(c)&&(b=g(c,!0))&&(b=c.indexOf(\")\",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(ba,ca).toLowerCase();return\"*\"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+\" \"];return b||(b=new RegExp(\"(^|\"+L+\")\"+a+\"(\"+L+\"|$)\"))&&y(a,function(a){return b.test(\"string\"==typeof a.className&&a.className||\"undefined\"!=typeof a.getAttribute&&a.getAttribute(\"class\")||\"\")})},ATTR:function(a,b,c){return function(d){var e=fa.attr(d,a);return null==e?\"!=\"===b:b?(e+=\"\",\"=\"===b?e===c:\"!=\"===b?e!==c:\"^=\"===b?c&&0===e.indexOf(c):\"*=\"===b?c&&e.indexOf(c)>-1:\"$=\"===b?c&&e.slice(-c.length)===c:\"~=\"===b?(\" \"+e.replace(P,\" \")+\" \").indexOf(c)>-1:\"|=\"===b?e===c||e.slice(0,c.length+1)===c+\"-\":!1):!0}},CHILD:function(a,b,c,d,e){var f=\"nth\"!==a.slice(0,3),g=\"last\"!==a.slice(-4),h=\"of-type\"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?\"nextSibling\":\"previousSibling\",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h,t=!1;if(q){if(f){while(p){m=b;while(m=m[p])if(h?m.nodeName.toLowerCase()===r:1===m.nodeType)return!1;o=p=\"only\"===a&&!o&&\"nextSibling\"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){m=q,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n&&j[2],m=n&&q.childNodes[n];while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if(1===m.nodeType&&++t&&m===b){k[a]=[w,n,t];break}}else if(s&&(m=b,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n),t===!1)while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if((h?m.nodeName.toLowerCase()===r:1===m.nodeType)&&++t&&(s&&(l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),k[a]=[w,t]),m===b))break;return t-=e,t===d||t%d===0&&t/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||fa.error(\"unsupported pseudo: \"+a);return e[u]?e(b):e.length>1?(c=[a,a,\"\",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ha(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=J(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ha(function(a){var b=[],c=[],d=h(a.replace(Q,\"$1\"));return d[u]?ha(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ha(function(a){return function(b){return fa(a,b).length>0}}),contains:ha(function(a){return a=a.replace(ba,ca),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ha(function(a){return V.test(a||\"\")||fa.error(\"unsupported lang: \"+a),a=a.replace(ba,ca).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute(\"xml:lang\")||b.getAttribute(\"lang\"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+\"-\");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return\"input\"===b&&!!a.checked||\"option\"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return Y.test(a.nodeName)},input:function(a){return X.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return\"input\"===b&&\"button\"===a.type||\"button\"===b},text:function(a){var b;return\"input\"===a.nodeName.toLowerCase()&&\"text\"===a.type&&(null==(b=a.getAttribute(\"type\"))||\"text\"===b.toLowerCase())},first:na(function(){return[0]}),last:na(function(a,b){return[b-1]}),eq:na(function(a,b,c){return[0>c?c+b:c]}),even:na(function(a,b){for(var c=0;b>c;c+=2)a.push(c);return a}),odd:na(function(a,b){for(var c=1;b>c;c+=2)a.push(c);return a}),lt:na(function(a,b,c){for(var d=0>c?c+b:c;--d>=0;)a.push(d);return a}),gt:na(function(a,b,c){for(var d=0>c?c+b:c;++d<b;)a.push(d);return a})}},d.pseudos.nth=d.pseudos.eq;for(b in{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})d.pseudos[b]=la(b);for(b in{submit:!0,reset:!0})d.pseudos[b]=ma(b);function pa(){}pa.prototype=d.filters=d.pseudos,d.setFilters=new pa,g=fa.tokenize=function(a,b){var c,e,f,g,h,i,j,k=z[a+\" \"];if(k)return b?0:k.slice(0);h=a,i=[],j=d.preFilter;while(h){c&&!(e=R.exec(h))||(e&&(h=h.slice(e[0].length)||h),i.push(f=[])),c=!1,(e=S.exec(h))&&(c=e.shift(),f.push({value:c,type:e[0].replace(Q,\" \")}),h=h.slice(c.length));for(g in d.filter)!(e=W[g].exec(h))||j[g]&&!(e=j[g](e))||(c=e.shift(),f.push({value:c,type:g,matches:e}),h=h.slice(c.length));if(!c)break}return b?h.length:h?fa.error(a):z(a,i).slice(0)};function qa(a){for(var b=0,c=a.length,d=\"\";c>b;b++)d+=a[b].value;return d}function ra(a,b,c){var d=b.dir,e=c&&\"parentNode\"===d,f=x++;return b.first?function(b,c,f){while(b=b[d])if(1===b.nodeType||e)return a(b,c,f)}:function(b,c,g){var h,i,j,k=[w,f];if(g){while(b=b[d])if((1===b.nodeType||e)&&a(b,c,g))return!0}else while(b=b[d])if(1===b.nodeType||e){if(j=b[u]||(b[u]={}),i=j[b.uniqueID]||(j[b.uniqueID]={}),(h=i[d])&&h[0]===w&&h[1]===f)return k[2]=h[2];if(i[d]=k,k[2]=a(b,c,g))return!0}}}function sa(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function ta(a,b,c){for(var d=0,e=b.length;e>d;d++)fa(a,b[d],c);return c}function ua(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;i>h;h++)(f=a[h])&&(c&&!c(f,d,e)||(g.push(f),j&&b.push(h)));return g}function va(a,b,c,d,e,f){return d&&!d[u]&&(d=va(d)),e&&!e[u]&&(e=va(e,f)),ha(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||ta(b||\"*\",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:ua(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=ua(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?J(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=ua(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):H.apply(g,r)})}function wa(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[\" \"],i=g?1:0,k=ra(function(a){return a===b},h,!0),l=ra(function(a){return J(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];f>i;i++)if(c=d.relative[a[i].type])m=[ra(sa(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;f>e;e++)if(d.relative[a[e].type])break;return va(i>1&&sa(m),i>1&&qa(a.slice(0,i-1).concat({value:\" \"===a[i-2].type?\"*\":\"\"})).replace(Q,\"$1\"),c,e>i&&wa(a.slice(i,e)),f>e&&wa(a=a.slice(e)),f>e&&qa(a))}m.push(c)}return sa(m)}function xa(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,o,q,r=0,s=\"0\",t=f&&[],u=[],v=j,x=f||e&&d.find.TAG(\"*\",k),y=w+=null==v?1:Math.random()||.1,z=x.length;for(k&&(j=g===n||g||k);s!==z&&null!=(l=x[s]);s++){if(e&&l){o=0,g||l.ownerDocument===n||(m(l),h=!p);while(q=a[o++])if(q(l,g||n,h)){i.push(l);break}k&&(w=y)}c&&((l=!q&&l)&&r--,f&&t.push(l))}if(r+=s,c&&s!==r){o=0;while(q=b[o++])q(t,u,g,h);if(f){if(r>0)while(s--)t[s]||u[s]||(u[s]=F.call(i));u=ua(u)}H.apply(i,u),k&&!f&&u.length>0&&r+b.length>1&&fa.uniqueSort(i)}return k&&(w=y,j=v),t};return c?ha(f):f}return h=fa.compile=function(a,b){var c,d=[],e=[],f=A[a+\" \"];if(!f){b||(b=g(a)),c=b.length;while(c--)f=wa(b[c]),f[u]?d.push(f):e.push(f);f=A(a,xa(e,d)),f.selector=a}return f},i=fa.select=function(a,b,e,f){var i,j,k,l,m,n=\"function\"==typeof a&&a,o=!f&&g(a=n.selector||a);if(e=e||[],1===o.length){if(j=o[0]=o[0].slice(0),j.length>2&&\"ID\"===(k=j[0]).type&&c.getById&&9===b.nodeType&&p&&d.relative[j[1].type]){if(b=(d.find.ID(k.matches[0].replace(ba,ca),b)||[])[0],!b)return e;n&&(b=b.parentNode),a=a.slice(j.shift().value.length)}i=W.needsContext.test(a)?0:j.length;while(i--){if(k=j[i],d.relative[l=k.type])break;if((m=d.find[l])&&(f=m(k.matches[0].replace(ba,ca),_.test(j[0].type)&&oa(b.parentNode)||b))){if(j.splice(i,1),a=f.length&&qa(j),!a)return H.apply(e,f),e;break}}}return(n||h(a,o))(f,b,!p,e,!b||_.test(a)&&oa(b.parentNode)||b),e},c.sortStable=u.split(\"\").sort(B).join(\"\")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ia(function(a){return 1&a.compareDocumentPosition(n.createElement(\"div\"))}),ia(function(a){return a.innerHTML=\"<a href='#'></a>\",\"#\"===a.firstChild.getAttribute(\"href\")})||ja(\"type|href|height|width\",function(a,b,c){return c?void 0:a.getAttribute(b,\"type\"===b.toLowerCase()?1:2)}),c.attributes&&ia(function(a){return a.innerHTML=\"<input/>\",a.firstChild.setAttribute(\"value\",\"\"),\"\"===a.firstChild.getAttribute(\"value\")})||ja(\"value\",function(a,b,c){return c||\"input\"!==a.nodeName.toLowerCase()?void 0:a.defaultValue}),ia(function(a){return null==a.getAttribute(\"disabled\")})||ja(K,function(a,b,c){var d;return c?void 0:a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),fa}(a);n.find=t,n.expr=t.selectors,n.expr[\":\"]=n.expr.pseudos,n.uniqueSort=n.unique=t.uniqueSort,n.text=t.getText,n.isXMLDoc=t.isXML,n.contains=t.contains;var u=function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&n(a).is(c))break;d.push(a)}return d},v=function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c},w=n.expr.match.needsContext,x=/^<([\\w-]+)\\s*\\/?>(?:<\\/\\1>|)$/,y=/^.[^:#\\[\\.,]*$/;function z(a,b,c){if(n.isFunction(b))return n.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return n.grep(a,function(a){return a===b!==c});if(\"string\"==typeof b){if(y.test(b))return n.filter(b,a,c);b=n.filter(b,a)}return n.grep(a,function(a){return n.inArray(a,b)>-1!==c})}n.filter=function(a,b,c){var d=b[0];return c&&(a=\":not(\"+a+\")\"),1===b.length&&1===d.nodeType?n.find.matchesSelector(d,a)?[d]:[]:n.find.matches(a,n.grep(b,function(a){return 1===a.nodeType}))},n.fn.extend({find:function(a){var b,c=[],d=this,e=d.length;if(\"string\"!=typeof a)return this.pushStack(n(a).filter(function(){for(b=0;e>b;b++)if(n.contains(d[b],this))return!0}));for(b=0;e>b;b++)n.find(a,d[b],c);return c=this.pushStack(e>1?n.unique(c):c),c.selector=this.selector?this.selector+\" \"+a:a,c},filter:function(a){return this.pushStack(z(this,a||[],!1))},not:function(a){return this.pushStack(z(this,a||[],!0))},is:function(a){return!!z(this,\"string\"==typeof a&&w.test(a)?n(a):a||[],!1).length}});var A,B=/^(?:\\s*(<[\\w\\W]+>)[^>]*|#([\\w-]*))$/,C=n.fn.init=function(a,b,c){var e,f;if(!a)return this;if(c=c||A,\"string\"==typeof a){if(e=\"<\"===a.charAt(0)&&\">\"===a.charAt(a.length-1)&&a.length>=3?[null,a,null]:B.exec(a),!e||!e[1]&&b)return!b||b.jquery?(b||c).find(a):this.constructor(b).find(a);if(e[1]){if(b=b instanceof n?b[0]:b,n.merge(this,n.parseHTML(e[1],b&&b.nodeType?b.ownerDocument||b:d,!0)),x.test(e[1])&&n.isPlainObject(b))for(e in b)n.isFunction(this[e])?this[e](b[e]):this.attr(e,b[e]);return this}if(f=d.getElementById(e[2]),f&&f.parentNode){if(f.id!==e[2])return A.find(a);this.length=1,this[0]=f}return this.context=d,this.selector=a,this}return a.nodeType?(this.context=this[0]=a,this.length=1,this):n.isFunction(a)?\"undefined\"!=typeof c.ready?c.ready(a):a(n):(void 0!==a.selector&&(this.selector=a.selector,this.context=a.context),n.makeArray(a,this))};C.prototype=n.fn,A=n(d);var D=/^(?:parents|prev(?:Until|All))/,E={children:!0,contents:!0,next:!0,prev:!0};n.fn.extend({has:function(a){var b,c=n(a,this),d=c.length;return this.filter(function(){for(b=0;d>b;b++)if(n.contains(this,c[b]))return!0})},closest:function(a,b){for(var c,d=0,e=this.length,f=[],g=w.test(a)||\"string\"!=typeof a?n(a,b||this.context):0;e>d;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&n.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?n.uniqueSort(f):f)},index:function(a){return a?\"string\"==typeof a?n.inArray(this[0],n(a)):n.inArray(a.jquery?a[0]:a,this):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(n.uniqueSort(n.merge(this.get(),n(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function F(a,b){do a=a[b];while(a&&1!==a.nodeType);return a}n.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return u(a,\"parentNode\")},parentsUntil:function(a,b,c){return u(a,\"parentNode\",c)},next:function(a){return F(a,\"nextSibling\")},prev:function(a){return F(a,\"previousSibling\")},nextAll:function(a){return u(a,\"nextSibling\")},prevAll:function(a){return u(a,\"previousSibling\")},nextUntil:function(a,b,c){return u(a,\"nextSibling\",c)},prevUntil:function(a,b,c){return u(a,\"previousSibling\",c)},siblings:function(a){return v((a.parentNode||{}).firstChild,a)},children:function(a){return v(a.firstChild)},contents:function(a){return n.nodeName(a,\"iframe\")?a.contentDocument||a.contentWindow.document:n.merge([],a.childNodes)}},function(a,b){n.fn[a]=function(c,d){var e=n.map(this,b,c);return\"Until\"!==a.slice(-5)&&(d=c),d&&\"string\"==typeof d&&(e=n.filter(d,e)),this.length>1&&(E[a]||(e=n.uniqueSort(e)),D.test(a)&&(e=e.reverse())),this.pushStack(e)}});var G=/\\S+/g;function H(a){var b={};return n.each(a.match(G)||[],function(a,c){b[c]=!0}),b}n.Callbacks=function(a){a=\"string\"==typeof a?H(a):n.extend({},a);var b,c,d,e,f=[],g=[],h=-1,i=function(){for(e=a.once,d=b=!0;g.length;h=-1){c=g.shift();while(++h<f.length)f[h].apply(c[0],c[1])===!1&&a.stopOnFalse&&(h=f.length,c=!1)}a.memory||(c=!1),b=!1,e&&(f=c?[]:\"\")},j={add:function(){return f&&(c&&!b&&(h=f.length-1,g.push(c)),function d(b){n.each(b,function(b,c){n.isFunction(c)?a.unique&&j.has(c)||f.push(c):c&&c.length&&\"string\"!==n.type(c)&&d(c)})}(arguments),c&&!b&&i()),this},remove:function(){return n.each(arguments,function(a,b){var c;while((c=n.inArray(b,f,c))>-1)f.splice(c,1),h>=c&&h--}),this},has:function(a){return a?n.inArray(a,f)>-1:f.length>0},empty:function(){return f&&(f=[]),this},disable:function(){return e=g=[],f=c=\"\",this},disabled:function(){return!f},lock:function(){return e=!0,c||j.disable(),this},locked:function(){return!!e},fireWith:function(a,c){return e||(c=c||[],c=[a,c.slice?c.slice():c],g.push(c),b||i()),this},fire:function(){return j.fireWith(this,arguments),this},fired:function(){return!!d}};return j},n.extend({Deferred:function(a){var b=[[\"resolve\",\"done\",n.Callbacks(\"once memory\"),\"resolved\"],[\"reject\",\"fail\",n.Callbacks(\"once memory\"),\"rejected\"],[\"notify\",\"progress\",n.Callbacks(\"memory\")]],c=\"pending\",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return n.Deferred(function(c){n.each(b,function(b,f){var g=n.isFunction(a[b])&&a[b];e[f[1]](function(){var a=g&&g.apply(this,arguments);a&&n.isFunction(a.promise)?a.promise().progress(c.notify).done(c.resolve).fail(c.reject):c[f[0]+\"With\"](this===d?c.promise():this,g?[a]:arguments)})}),a=null}).promise()},promise:function(a){return null!=a?n.extend(a,d):d}},e={};return d.pipe=d.then,n.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[1^a][2].disable,b[2][2].lock),e[f[0]]=function(){return e[f[0]+\"With\"](this===e?d:this,arguments),this},e[f[0]+\"With\"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=e.call(arguments),d=c.length,f=1!==d||a&&n.isFunction(a.promise)?d:0,g=1===f?a:n.Deferred(),h=function(a,b,c){return function(d){b[a]=this,c[a]=arguments.length>1?e.call(arguments):d,c===i?g.notifyWith(b,c):--f||g.resolveWith(b,c)}},i,j,k;if(d>1)for(i=new Array(d),j=new Array(d),k=new Array(d);d>b;b++)c[b]&&n.isFunction(c[b].promise)?c[b].promise().progress(h(b,j,i)).done(h(b,k,c)).fail(g.reject):--f;return f||g.resolveWith(k,c),g.promise()}});var I;n.fn.ready=function(a){return n.ready.promise().done(a),this},n.extend({isReady:!1,readyWait:1,holdReady:function(a){a?n.readyWait++:n.ready(!0)},ready:function(a){(a===!0?--n.readyWait:n.isReady)||(n.isReady=!0,a!==!0&&--n.readyWait>0||(I.resolveWith(d,[n]),n.fn.triggerHandler&&(n(d).triggerHandler(\"ready\"),n(d).off(\"ready\"))))}});function J(){d.addEventListener?(d.removeEventListener(\"DOMContentLoaded\",K),a.removeEventListener(\"load\",K)):(d.detachEvent(\"onreadystatechange\",K),a.detachEvent(\"onload\",K))}function K(){(d.addEventListener||\"load\"===a.event.type||\"complete\"===d.readyState)&&(J(),n.ready())}n.ready.promise=function(b){if(!I)if(I=n.Deferred(),\"complete\"===d.readyState||\"loading\"!==d.readyState&&!d.documentElement.doScroll)a.setTimeout(n.ready);else if(d.addEventListener)d.addEventListener(\"DOMContentLoaded\",K),a.addEventListener(\"load\",K);else{d.attachEvent(\"onreadystatechange\",K),a.attachEvent(\"onload\",K);var c=!1;try{c=null==a.frameElement&&d.documentElement}catch(e){}c&&c.doScroll&&!function f(){if(!n.isReady){try{c.doScroll(\"left\")}catch(b){return a.setTimeout(f,50)}J(),n.ready()}}()}return I.promise(b)},n.ready.promise();var L;for(L in n(l))break;l.ownFirst=\"0\"===L,l.inlineBlockNeedsLayout=!1,n(function(){var a,b,c,e;c=d.getElementsByTagName(\"body\")[0],c&&c.style&&(b=d.createElement(\"div\"),e=d.createElement(\"div\"),e.style.cssText=\"position:absolute;border:0;width:0;height:0;top:0;left:-9999px\",c.appendChild(e).appendChild(b),\"undefined\"!=typeof b.style.zoom&&(b.style.cssText=\"display:inline;margin:0;border:0;padding:1px;width:1px;zoom:1\",l.inlineBlockNeedsLayout=a=3===b.offsetWidth,a&&(c.style.zoom=1)),c.removeChild(e))}),function(){var a=d.createElement(\"div\");l.deleteExpando=!0;try{delete a.test}catch(b){l.deleteExpando=!1}a=null}();var M=function(a){var b=n.noData[(a.nodeName+\" \").toLowerCase()],c=+a.nodeType||1;return 1!==c&&9!==c?!1:!b||b!==!0&&a.getAttribute(\"classid\")===b},N=/^(?:\\{[\\w\\W]*\\}|\\[[\\w\\W]*\\])$/,O=/([A-Z])/g;function P(a,b,c){if(void 0===c&&1===a.nodeType){var d=\"data-\"+b.replace(O,\"-$1\").toLowerCase();if(c=a.getAttribute(d),\"string\"==typeof c){try{c=\"true\"===c?!0:\"false\"===c?!1:\"null\"===c?null:+c+\"\"===c?+c:N.test(c)?n.parseJSON(c):c}catch(e){}n.data(a,b,c)}else c=void 0;\n}return c}function Q(a){var b;for(b in a)if((\"data\"!==b||!n.isEmptyObject(a[b]))&&\"toJSON\"!==b)return!1;return!0}function R(a,b,d,e){if(M(a)){var f,g,h=n.expando,i=a.nodeType,j=i?n.cache:a,k=i?a[h]:a[h]&&h;if(k&&j[k]&&(e||j[k].data)||void 0!==d||\"string\"!=typeof b)return k||(k=i?a[h]=c.pop()||n.guid++:h),j[k]||(j[k]=i?{}:{toJSON:n.noop}),\"object\"!=typeof b&&\"function\"!=typeof b||(e?j[k]=n.extend(j[k],b):j[k].data=n.extend(j[k].data,b)),g=j[k],e||(g.data||(g.data={}),g=g.data),void 0!==d&&(g[n.camelCase(b)]=d),\"string\"==typeof b?(f=g[b],null==f&&(f=g[n.camelCase(b)])):f=g,f}}function S(a,b,c){if(M(a)){var d,e,f=a.nodeType,g=f?n.cache:a,h=f?a[n.expando]:n.expando;if(g[h]){if(b&&(d=c?g[h]:g[h].data)){n.isArray(b)?b=b.concat(n.map(b,n.camelCase)):b in d?b=[b]:(b=n.camelCase(b),b=b in d?[b]:b.split(\" \")),e=b.length;while(e--)delete d[b[e]];if(c?!Q(d):!n.isEmptyObject(d))return}(c||(delete g[h].data,Q(g[h])))&&(f?n.cleanData([a],!0):l.deleteExpando||g!=g.window?delete g[h]:g[h]=void 0)}}}n.extend({cache:{},noData:{\"applet \":!0,\"embed \":!0,\"object \":\"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000\"},hasData:function(a){return a=a.nodeType?n.cache[a[n.expando]]:a[n.expando],!!a&&!Q(a)},data:function(a,b,c){return R(a,b,c)},removeData:function(a,b){return S(a,b)},_data:function(a,b,c){return R(a,b,c,!0)},_removeData:function(a,b){return S(a,b,!0)}}),n.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=n.data(f),1===f.nodeType&&!n._data(f,\"parsedAttrs\"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf(\"data-\")&&(d=n.camelCase(d.slice(5)),P(f,d,e[d])));n._data(f,\"parsedAttrs\",!0)}return e}return\"object\"==typeof a?this.each(function(){n.data(this,a)}):arguments.length>1?this.each(function(){n.data(this,a,b)}):f?P(f,a,n.data(f,a)):void 0},removeData:function(a){return this.each(function(){n.removeData(this,a)})}}),n.extend({queue:function(a,b,c){var d;return a?(b=(b||\"fx\")+\"queue\",d=n._data(a,b),c&&(!d||n.isArray(c)?d=n._data(a,b,n.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||\"fx\";var c=n.queue(a,b),d=c.length,e=c.shift(),f=n._queueHooks(a,b),g=function(){n.dequeue(a,b)};\"inprogress\"===e&&(e=c.shift(),d--),e&&(\"fx\"===b&&c.unshift(\"inprogress\"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+\"queueHooks\";return n._data(a,c)||n._data(a,c,{empty:n.Callbacks(\"once memory\").add(function(){n._removeData(a,b+\"queue\"),n._removeData(a,c)})})}}),n.fn.extend({queue:function(a,b){var c=2;return\"string\"!=typeof a&&(b=a,a=\"fx\",c--),arguments.length<c?n.queue(this[0],a):void 0===b?this:this.each(function(){var c=n.queue(this,a,b);n._queueHooks(this,a),\"fx\"===a&&\"inprogress\"!==c[0]&&n.dequeue(this,a)})},dequeue:function(a){return this.each(function(){n.dequeue(this,a)})},clearQueue:function(a){return this.queue(a||\"fx\",[])},promise:function(a,b){var c,d=1,e=n.Deferred(),f=this,g=this.length,h=function(){--d||e.resolveWith(f,[f])};\"string\"!=typeof a&&(b=a,a=void 0),a=a||\"fx\";while(g--)c=n._data(f[g],a+\"queueHooks\"),c&&c.empty&&(d++,c.empty.add(h));return h(),e.promise(b)}}),function(){var a;l.shrinkWrapBlocks=function(){if(null!=a)return a;a=!1;var b,c,e;return c=d.getElementsByTagName(\"body\")[0],c&&c.style?(b=d.createElement(\"div\"),e=d.createElement(\"div\"),e.style.cssText=\"position:absolute;border:0;width:0;height:0;top:0;left:-9999px\",c.appendChild(e).appendChild(b),\"undefined\"!=typeof b.style.zoom&&(b.style.cssText=\"-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;display:block;margin:0;border:0;padding:1px;width:1px;zoom:1\",b.appendChild(d.createElement(\"div\")).style.width=\"5px\",a=3!==b.offsetWidth),c.removeChild(e),a):void 0}}();var T=/[+-]?(?:\\d*\\.|)\\d+(?:[eE][+-]?\\d+|)/.source,U=new RegExp(\"^(?:([+-])=|)(\"+T+\")([a-z%]*)$\",\"i\"),V=[\"Top\",\"Right\",\"Bottom\",\"Left\"],W=function(a,b){return a=b||a,\"none\"===n.css(a,\"display\")||!n.contains(a.ownerDocument,a)};function X(a,b,c,d){var e,f=1,g=20,h=d?function(){return d.cur()}:function(){return n.css(a,b,\"\")},i=h(),j=c&&c[3]||(n.cssNumber[b]?\"\":\"px\"),k=(n.cssNumber[b]||\"px\"!==j&&+i)&&U.exec(n.css(a,b));if(k&&k[3]!==j){j=j||k[3],c=c||[],k=+i||1;do f=f||\".5\",k/=f,n.style(a,b,k+j);while(f!==(f=h()/i)&&1!==f&&--g)}return c&&(k=+k||+i||0,e=c[1]?k+(c[1]+1)*c[2]:+c[2],d&&(d.unit=j,d.start=k,d.end=e)),e}var Y=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if(\"object\"===n.type(c)){e=!0;for(h in c)Y(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,n.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(n(a),c)})),b))for(;i>h;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f},Z=/^(?:checkbox|radio)$/i,$=/<([\\w:-]+)/,_=/^$|\\/(?:java|ecma)script/i,aa=/^\\s+/,ba=\"abbr|article|aside|audio|bdi|canvas|data|datalist|details|dialog|figcaption|figure|footer|header|hgroup|main|mark|meter|nav|output|picture|progress|section|summary|template|time|video\";function ca(a){var b=ba.split(\"|\"),c=a.createDocumentFragment();if(c.createElement)while(b.length)c.createElement(b.pop());return c}!function(){var a=d.createElement(\"div\"),b=d.createDocumentFragment(),c=d.createElement(\"input\");a.innerHTML=\"  <link/><table></table><a href='/a'>a</a><input type='checkbox'/>\",l.leadingWhitespace=3===a.firstChild.nodeType,l.tbody=!a.getElementsByTagName(\"tbody\").length,l.htmlSerialize=!!a.getElementsByTagName(\"link\").length,l.html5Clone=\"<:nav></:nav>\"!==d.createElement(\"nav\").cloneNode(!0).outerHTML,c.type=\"checkbox\",c.checked=!0,b.appendChild(c),l.appendChecked=c.checked,a.innerHTML=\"<textarea>x</textarea>\",l.noCloneChecked=!!a.cloneNode(!0).lastChild.defaultValue,b.appendChild(a),c=d.createElement(\"input\"),c.setAttribute(\"type\",\"radio\"),c.setAttribute(\"checked\",\"checked\"),c.setAttribute(\"name\",\"t\"),a.appendChild(c),l.checkClone=a.cloneNode(!0).cloneNode(!0).lastChild.checked,l.noCloneEvent=!!a.addEventListener,a[n.expando]=1,l.attributes=!a.getAttribute(n.expando)}();var da={option:[1,\"<select multiple='multiple'>\",\"</select>\"],legend:[1,\"<fieldset>\",\"</fieldset>\"],area:[1,\"<map>\",\"</map>\"],param:[1,\"<object>\",\"</object>\"],thead:[1,\"<table>\",\"</table>\"],tr:[2,\"<table><tbody>\",\"</tbody></table>\"],col:[2,\"<table><tbody></tbody><colgroup>\",\"</colgroup></table>\"],td:[3,\"<table><tbody><tr>\",\"</tr></tbody></table>\"],_default:l.htmlSerialize?[0,\"\",\"\"]:[1,\"X<div>\",\"</div>\"]};da.optgroup=da.option,da.tbody=da.tfoot=da.colgroup=da.caption=da.thead,da.th=da.td;function ea(a,b){var c,d,e=0,f=\"undefined\"!=typeof a.getElementsByTagName?a.getElementsByTagName(b||\"*\"):\"undefined\"!=typeof a.querySelectorAll?a.querySelectorAll(b||\"*\"):void 0;if(!f)for(f=[],c=a.childNodes||a;null!=(d=c[e]);e++)!b||n.nodeName(d,b)?f.push(d):n.merge(f,ea(d,b));return void 0===b||b&&n.nodeName(a,b)?n.merge([a],f):f}function fa(a,b){for(var c,d=0;null!=(c=a[d]);d++)n._data(c,\"globalEval\",!b||n._data(b[d],\"globalEval\"))}var ga=/<|&#?\\w+;/,ha=/<tbody/i;function ia(a){Z.test(a.type)&&(a.defaultChecked=a.checked)}function ja(a,b,c,d,e){for(var f,g,h,i,j,k,m,o=a.length,p=ca(b),q=[],r=0;o>r;r++)if(g=a[r],g||0===g)if(\"object\"===n.type(g))n.merge(q,g.nodeType?[g]:g);else if(ga.test(g)){i=i||p.appendChild(b.createElement(\"div\")),j=($.exec(g)||[\"\",\"\"])[1].toLowerCase(),m=da[j]||da._default,i.innerHTML=m[1]+n.htmlPrefilter(g)+m[2],f=m[0];while(f--)i=i.lastChild;if(!l.leadingWhitespace&&aa.test(g)&&q.push(b.createTextNode(aa.exec(g)[0])),!l.tbody){g=\"table\"!==j||ha.test(g)?\"<table>\"!==m[1]||ha.test(g)?0:i:i.firstChild,f=g&&g.childNodes.length;while(f--)n.nodeName(k=g.childNodes[f],\"tbody\")&&!k.childNodes.length&&g.removeChild(k)}n.merge(q,i.childNodes),i.textContent=\"\";while(i.firstChild)i.removeChild(i.firstChild);i=p.lastChild}else q.push(b.createTextNode(g));i&&p.removeChild(i),l.appendChecked||n.grep(ea(q,\"input\"),ia),r=0;while(g=q[r++])if(d&&n.inArray(g,d)>-1)e&&e.push(g);else if(h=n.contains(g.ownerDocument,g),i=ea(p.appendChild(g),\"script\"),h&&fa(i),c){f=0;while(g=i[f++])_.test(g.type||\"\")&&c.push(g)}return i=null,p}!function(){var b,c,e=d.createElement(\"div\");for(b in{submit:!0,change:!0,focusin:!0})c=\"on\"+b,(l[b]=c in a)||(e.setAttribute(c,\"t\"),l[b]=e.attributes[c].expando===!1);e=null}();var ka=/^(?:input|select|textarea)$/i,la=/^key/,ma=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,na=/^(?:focusinfocus|focusoutblur)$/,oa=/^([^.]*)(?:\\.(.+)|)/;function pa(){return!0}function qa(){return!1}function ra(){try{return d.activeElement}catch(a){}}function sa(a,b,c,d,e,f){var g,h;if(\"object\"==typeof b){\"string\"!=typeof c&&(d=d||c,c=void 0);for(h in b)sa(a,h,c,d,b[h],f);return a}if(null==d&&null==e?(e=c,d=c=void 0):null==e&&(\"string\"==typeof c?(e=d,d=void 0):(e=d,d=c,c=void 0)),e===!1)e=qa;else if(!e)return a;return 1===f&&(g=e,e=function(a){return n().off(a),g.apply(this,arguments)},e.guid=g.guid||(g.guid=n.guid++)),a.each(function(){n.event.add(this,b,e,d,c)})}n.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=n._data(a);if(r){c.handler&&(i=c,c=i.handler,e=i.selector),c.guid||(c.guid=n.guid++),(g=r.events)||(g=r.events={}),(k=r.handle)||(k=r.handle=function(a){return\"undefined\"==typeof n||a&&n.event.triggered===a.type?void 0:n.event.dispatch.apply(k.elem,arguments)},k.elem=a),b=(b||\"\").match(G)||[\"\"],h=b.length;while(h--)f=oa.exec(b[h])||[],o=q=f[1],p=(f[2]||\"\").split(\".\").sort(),o&&(j=n.event.special[o]||{},o=(e?j.delegateType:j.bindType)||o,j=n.event.special[o]||{},l=n.extend({type:o,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&n.expr.match.needsContext.test(e),namespace:p.join(\".\")},i),(m=g[o])||(m=g[o]=[],m.delegateCount=0,j.setup&&j.setup.call(a,d,p,k)!==!1||(a.addEventListener?a.addEventListener(o,k,!1):a.attachEvent&&a.attachEvent(\"on\"+o,k))),j.add&&(j.add.call(a,l),l.handler.guid||(l.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,l):m.push(l),n.event.global[o]=!0);a=null}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=n.hasData(a)&&n._data(a);if(r&&(k=r.events)){b=(b||\"\").match(G)||[\"\"],j=b.length;while(j--)if(h=oa.exec(b[j])||[],o=q=h[1],p=(h[2]||\"\").split(\".\").sort(),o){l=n.event.special[o]||{},o=(d?l.delegateType:l.bindType)||o,m=k[o]||[],h=h[2]&&new RegExp(\"(^|\\\\.)\"+p.join(\"\\\\.(?:.*\\\\.|)\")+\"(\\\\.|$)\"),i=f=m.length;while(f--)g=m[f],!e&&q!==g.origType||c&&c.guid!==g.guid||h&&!h.test(g.namespace)||d&&d!==g.selector&&(\"**\"!==d||!g.selector)||(m.splice(f,1),g.selector&&m.delegateCount--,l.remove&&l.remove.call(a,g));i&&!m.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||n.removeEvent(a,o,r.handle),delete k[o])}else for(o in k)n.event.remove(a,o+b[j],c,d,!0);n.isEmptyObject(k)&&(delete r.handle,n._removeData(a,\"events\"))}},trigger:function(b,c,e,f){var g,h,i,j,l,m,o,p=[e||d],q=k.call(b,\"type\")?b.type:b,r=k.call(b,\"namespace\")?b.namespace.split(\".\"):[];if(i=m=e=e||d,3!==e.nodeType&&8!==e.nodeType&&!na.test(q+n.event.triggered)&&(q.indexOf(\".\")>-1&&(r=q.split(\".\"),q=r.shift(),r.sort()),h=q.indexOf(\":\")<0&&\"on\"+q,b=b[n.expando]?b:new n.Event(q,\"object\"==typeof b&&b),b.isTrigger=f?2:3,b.namespace=r.join(\".\"),b.rnamespace=b.namespace?new RegExp(\"(^|\\\\.)\"+r.join(\"\\\\.(?:.*\\\\.|)\")+\"(\\\\.|$)\"):null,b.result=void 0,b.target||(b.target=e),c=null==c?[b]:n.makeArray(c,[b]),l=n.event.special[q]||{},f||!l.trigger||l.trigger.apply(e,c)!==!1)){if(!f&&!l.noBubble&&!n.isWindow(e)){for(j=l.delegateType||q,na.test(j+q)||(i=i.parentNode);i;i=i.parentNode)p.push(i),m=i;m===(e.ownerDocument||d)&&p.push(m.defaultView||m.parentWindow||a)}o=0;while((i=p[o++])&&!b.isPropagationStopped())b.type=o>1?j:l.bindType||q,g=(n._data(i,\"events\")||{})[b.type]&&n._data(i,\"handle\"),g&&g.apply(i,c),g=h&&i[h],g&&g.apply&&M(i)&&(b.result=g.apply(i,c),b.result===!1&&b.preventDefault());if(b.type=q,!f&&!b.isDefaultPrevented()&&(!l._default||l._default.apply(p.pop(),c)===!1)&&M(e)&&h&&e[q]&&!n.isWindow(e)){m=e[h],m&&(e[h]=null),n.event.triggered=q;try{e[q]()}catch(s){}n.event.triggered=void 0,m&&(e[h]=m)}return b.result}},dispatch:function(a){a=n.event.fix(a);var b,c,d,f,g,h=[],i=e.call(arguments),j=(n._data(this,\"events\")||{})[a.type]||[],k=n.event.special[a.type]||{};if(i[0]=a,a.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,a)!==!1){h=n.event.handlers.call(this,a,j),b=0;while((f=h[b++])&&!a.isPropagationStopped()){a.currentTarget=f.elem,c=0;while((g=f.handlers[c++])&&!a.isImmediatePropagationStopped())a.rnamespace&&!a.rnamespace.test(g.namespace)||(a.handleObj=g,a.data=g.data,d=((n.event.special[g.origType]||{}).handle||g.handler).apply(f.elem,i),void 0!==d&&(a.result=d)===!1&&(a.preventDefault(),a.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,a),a.result}},handlers:function(a,b){var c,d,e,f,g=[],h=b.delegateCount,i=a.target;if(h&&i.nodeType&&(\"click\"!==a.type||isNaN(a.button)||a.button<1))for(;i!=this;i=i.parentNode||this)if(1===i.nodeType&&(i.disabled!==!0||\"click\"!==a.type)){for(d=[],c=0;h>c;c++)f=b[c],e=f.selector+\" \",void 0===d[e]&&(d[e]=f.needsContext?n(e,this).index(i)>-1:n.find(e,this,null,[i]).length),d[e]&&d.push(f);d.length&&g.push({elem:i,handlers:d})}return h<b.length&&g.push({elem:this,handlers:b.slice(h)}),g},fix:function(a){if(a[n.expando])return a;var b,c,e,f=a.type,g=a,h=this.fixHooks[f];h||(this.fixHooks[f]=h=ma.test(f)?this.mouseHooks:la.test(f)?this.keyHooks:{}),e=h.props?this.props.concat(h.props):this.props,a=new n.Event(g),b=e.length;while(b--)c=e[b],a[c]=g[c];return a.target||(a.target=g.srcElement||d),3===a.target.nodeType&&(a.target=a.target.parentNode),a.metaKey=!!a.metaKey,h.filter?h.filter(a,g):a},props:\"altKey bubbles cancelable ctrlKey currentTarget detail eventPhase metaKey relatedTarget shiftKey target timeStamp view which\".split(\" \"),fixHooks:{},keyHooks:{props:\"char charCode key keyCode\".split(\" \"),filter:function(a,b){return null==a.which&&(a.which=null!=b.charCode?b.charCode:b.keyCode),a}},mouseHooks:{props:\"button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement\".split(\" \"),filter:function(a,b){var c,e,f,g=b.button,h=b.fromElement;return null==a.pageX&&null!=b.clientX&&(e=a.target.ownerDocument||d,f=e.documentElement,c=e.body,a.pageX=b.clientX+(f&&f.scrollLeft||c&&c.scrollLeft||0)-(f&&f.clientLeft||c&&c.clientLeft||0),a.pageY=b.clientY+(f&&f.scrollTop||c&&c.scrollTop||0)-(f&&f.clientTop||c&&c.clientTop||0)),!a.relatedTarget&&h&&(a.relatedTarget=h===a.target?b.toElement:h),a.which||void 0===g||(a.which=1&g?1:2&g?3:4&g?2:0),a}},special:{load:{noBubble:!0},focus:{trigger:function(){if(this!==ra()&&this.focus)try{return this.focus(),!1}catch(a){}},delegateType:\"focusin\"},blur:{trigger:function(){return this===ra()&&this.blur?(this.blur(),!1):void 0},delegateType:\"focusout\"},click:{trigger:function(){return n.nodeName(this,\"input\")&&\"checkbox\"===this.type&&this.click?(this.click(),!1):void 0},_default:function(a){return n.nodeName(a.target,\"a\")}},beforeunload:{postDispatch:function(a){void 0!==a.result&&a.originalEvent&&(a.originalEvent.returnValue=a.result)}}},simulate:function(a,b,c){var d=n.extend(new n.Event,c,{type:a,isSimulated:!0});n.event.trigger(d,null,b),d.isDefaultPrevented()&&c.preventDefault()}},n.removeEvent=d.removeEventListener?function(a,b,c){a.removeEventListener&&a.removeEventListener(b,c)}:function(a,b,c){var d=\"on\"+b;a.detachEvent&&(\"undefined\"==typeof a[d]&&(a[d]=null),a.detachEvent(d,c))},n.Event=function(a,b){return this instanceof n.Event?(a&&a.type?(this.originalEvent=a,this.type=a.type,this.isDefaultPrevented=a.defaultPrevented||void 0===a.defaultPrevented&&a.returnValue===!1?pa:qa):this.type=a,b&&n.extend(this,b),this.timeStamp=a&&a.timeStamp||n.now(),void(this[n.expando]=!0)):new n.Event(a,b)},n.Event.prototype={constructor:n.Event,isDefaultPrevented:qa,isPropagationStopped:qa,isImmediatePropagationStopped:qa,preventDefault:function(){var a=this.originalEvent;this.isDefaultPrevented=pa,a&&(a.preventDefault?a.preventDefault():a.returnValue=!1)},stopPropagation:function(){var a=this.originalEvent;this.isPropagationStopped=pa,a&&!this.isSimulated&&(a.stopPropagation&&a.stopPropagation(),a.cancelBubble=!0)},stopImmediatePropagation:function(){var a=this.originalEvent;this.isImmediatePropagationStopped=pa,a&&a.stopImmediatePropagation&&a.stopImmediatePropagation(),this.stopPropagation()}},n.each({mouseenter:\"mouseover\",mouseleave:\"mouseout\",pointerenter:\"pointerover\",pointerleave:\"pointerout\"},function(a,b){n.event.special[a]={delegateType:b,bindType:b,handle:function(a){var c,d=this,e=a.relatedTarget,f=a.handleObj;return e&&(e===d||n.contains(d,e))||(a.type=f.origType,c=f.handler.apply(this,arguments),a.type=b),c}}}),l.submit||(n.event.special.submit={setup:function(){return n.nodeName(this,\"form\")?!1:void n.event.add(this,\"click._submit keypress._submit\",function(a){var b=a.target,c=n.nodeName(b,\"input\")||n.nodeName(b,\"button\")?n.prop(b,\"form\"):void 0;c&&!n._data(c,\"submit\")&&(n.event.add(c,\"submit._submit\",function(a){a._submitBubble=!0}),n._data(c,\"submit\",!0))})},postDispatch:function(a){a._submitBubble&&(delete a._submitBubble,this.parentNode&&!a.isTrigger&&n.event.simulate(\"submit\",this.parentNode,a))},teardown:function(){return n.nodeName(this,\"form\")?!1:void n.event.remove(this,\"._submit\")}}),l.change||(n.event.special.change={setup:function(){return ka.test(this.nodeName)?(\"checkbox\"!==this.type&&\"radio\"!==this.type||(n.event.add(this,\"propertychange._change\",function(a){\"checked\"===a.originalEvent.propertyName&&(this._justChanged=!0)}),n.event.add(this,\"click._change\",function(a){this._justChanged&&!a.isTrigger&&(this._justChanged=!1),n.event.simulate(\"change\",this,a)})),!1):void n.event.add(this,\"beforeactivate._change\",function(a){var b=a.target;ka.test(b.nodeName)&&!n._data(b,\"change\")&&(n.event.add(b,\"change._change\",function(a){!this.parentNode||a.isSimulated||a.isTrigger||n.event.simulate(\"change\",this.parentNode,a)}),n._data(b,\"change\",!0))})},handle:function(a){var b=a.target;return this!==b||a.isSimulated||a.isTrigger||\"radio\"!==b.type&&\"checkbox\"!==b.type?a.handleObj.handler.apply(this,arguments):void 0},teardown:function(){return n.event.remove(this,\"._change\"),!ka.test(this.nodeName)}}),l.focusin||n.each({focus:\"focusin\",blur:\"focusout\"},function(a,b){var c=function(a){n.event.simulate(b,a.target,n.event.fix(a))};n.event.special[b]={setup:function(){var d=this.ownerDocument||this,e=n._data(d,b);e||d.addEventListener(a,c,!0),n._data(d,b,(e||0)+1)},teardown:function(){var d=this.ownerDocument||this,e=n._data(d,b)-1;e?n._data(d,b,e):(d.removeEventListener(a,c,!0),n._removeData(d,b))}}}),n.fn.extend({on:function(a,b,c,d){return sa(this,a,b,c,d)},one:function(a,b,c,d){return sa(this,a,b,c,d,1)},off:function(a,b,c){var d,e;if(a&&a.preventDefault&&a.handleObj)return d=a.handleObj,n(a.delegateTarget).off(d.namespace?d.origType+\".\"+d.namespace:d.origType,d.selector,d.handler),this;if(\"object\"==typeof a){for(e in a)this.off(e,b,a[e]);return this}return b!==!1&&\"function\"!=typeof b||(c=b,b=void 0),c===!1&&(c=qa),this.each(function(){n.event.remove(this,a,c,b)})},trigger:function(a,b){return this.each(function(){n.event.trigger(a,b,this)})},triggerHandler:function(a,b){var c=this[0];return c?n.event.trigger(a,b,c,!0):void 0}});var ta=/ jQuery\\d+=\"(?:null|\\d+)\"/g,ua=new RegExp(\"<(?:\"+ba+\")[\\\\s/>]\",\"i\"),va=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\\w:-]+)[^>]*)\\/>/gi,wa=/<script|<style|<link/i,xa=/checked\\s*(?:[^=]|=\\s*.checked.)/i,ya=/^true\\/(.*)/,za=/^\\s*<!(?:\\[CDATA\\[|--)|(?:\\]\\]|--)>\\s*$/g,Aa=ca(d),Ba=Aa.appendChild(d.createElement(\"div\"));function Ca(a,b){return n.nodeName(a,\"table\")&&n.nodeName(11!==b.nodeType?b:b.firstChild,\"tr\")?a.getElementsByTagName(\"tbody\")[0]||a.appendChild(a.ownerDocument.createElement(\"tbody\")):a}function Da(a){return a.type=(null!==n.find.attr(a,\"type\"))+\"/\"+a.type,a}function Ea(a){var b=ya.exec(a.type);return b?a.type=b[1]:a.removeAttribute(\"type\"),a}function Fa(a,b){if(1===b.nodeType&&n.hasData(a)){var c,d,e,f=n._data(a),g=n._data(b,f),h=f.events;if(h){delete g.handle,g.events={};for(c in h)for(d=0,e=h[c].length;e>d;d++)n.event.add(b,c,h[c][d])}g.data&&(g.data=n.extend({},g.data))}}function Ga(a,b){var c,d,e;if(1===b.nodeType){if(c=b.nodeName.toLowerCase(),!l.noCloneEvent&&b[n.expando]){e=n._data(b);for(d in e.events)n.removeEvent(b,d,e.handle);b.removeAttribute(n.expando)}\"script\"===c&&b.text!==a.text?(Da(b).text=a.text,Ea(b)):\"object\"===c?(b.parentNode&&(b.outerHTML=a.outerHTML),l.html5Clone&&a.innerHTML&&!n.trim(b.innerHTML)&&(b.innerHTML=a.innerHTML)):\"input\"===c&&Z.test(a.type)?(b.defaultChecked=b.checked=a.checked,b.value!==a.value&&(b.value=a.value)):\"option\"===c?b.defaultSelected=b.selected=a.defaultSelected:\"input\"!==c&&\"textarea\"!==c||(b.defaultValue=a.defaultValue)}}function Ha(a,b,c,d){b=f.apply([],b);var e,g,h,i,j,k,m=0,o=a.length,p=o-1,q=b[0],r=n.isFunction(q);if(r||o>1&&\"string\"==typeof q&&!l.checkClone&&xa.test(q))return a.each(function(e){var f=a.eq(e);r&&(b[0]=q.call(this,e,f.html())),Ha(f,b,c,d)});if(o&&(k=ja(b,a[0].ownerDocument,!1,a,d),e=k.firstChild,1===k.childNodes.length&&(k=e),e||d)){for(i=n.map(ea(k,\"script\"),Da),h=i.length;o>m;m++)g=k,m!==p&&(g=n.clone(g,!0,!0),h&&n.merge(i,ea(g,\"script\"))),c.call(a[m],g,m);if(h)for(j=i[i.length-1].ownerDocument,n.map(i,Ea),m=0;h>m;m++)g=i[m],_.test(g.type||\"\")&&!n._data(g,\"globalEval\")&&n.contains(j,g)&&(g.src?n._evalUrl&&n._evalUrl(g.src):n.globalEval((g.text||g.textContent||g.innerHTML||\"\").replace(za,\"\")));k=e=null}return a}function Ia(a,b,c){for(var d,e=b?n.filter(b,a):a,f=0;null!=(d=e[f]);f++)c||1!==d.nodeType||n.cleanData(ea(d)),d.parentNode&&(c&&n.contains(d.ownerDocument,d)&&fa(ea(d,\"script\")),d.parentNode.removeChild(d));return a}n.extend({htmlPrefilter:function(a){return a.replace(va,\"<$1></$2>\")},clone:function(a,b,c){var d,e,f,g,h,i=n.contains(a.ownerDocument,a);if(l.html5Clone||n.isXMLDoc(a)||!ua.test(\"<\"+a.nodeName+\">\")?f=a.cloneNode(!0):(Ba.innerHTML=a.outerHTML,Ba.removeChild(f=Ba.firstChild)),!(l.noCloneEvent&&l.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||n.isXMLDoc(a)))for(d=ea(f),h=ea(a),g=0;null!=(e=h[g]);++g)d[g]&&Ga(e,d[g]);if(b)if(c)for(h=h||ea(a),d=d||ea(f),g=0;null!=(e=h[g]);g++)Fa(e,d[g]);else Fa(a,f);return d=ea(f,\"script\"),d.length>0&&fa(d,!i&&ea(a,\"script\")),d=h=e=null,f},cleanData:function(a,b){for(var d,e,f,g,h=0,i=n.expando,j=n.cache,k=l.attributes,m=n.event.special;null!=(d=a[h]);h++)if((b||M(d))&&(f=d[i],g=f&&j[f])){if(g.events)for(e in g.events)m[e]?n.event.remove(d,e):n.removeEvent(d,e,g.handle);j[f]&&(delete j[f],k||\"undefined\"==typeof d.removeAttribute?d[i]=void 0:d.removeAttribute(i),c.push(f))}}}),n.fn.extend({domManip:Ha,detach:function(a){return Ia(this,a,!0)},remove:function(a){return Ia(this,a)},text:function(a){return Y(this,function(a){return void 0===a?n.text(this):this.empty().append((this[0]&&this[0].ownerDocument||d).createTextNode(a))},null,a,arguments.length)},append:function(){return Ha(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ca(this,a);b.appendChild(a)}})},prepend:function(){return Ha(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ca(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return Ha(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return Ha(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},empty:function(){for(var a,b=0;null!=(a=this[b]);b++){1===a.nodeType&&n.cleanData(ea(a,!1));while(a.firstChild)a.removeChild(a.firstChild);a.options&&n.nodeName(a,\"select\")&&(a.options.length=0)}return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return n.clone(this,a,b)})},html:function(a){return Y(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a)return 1===b.nodeType?b.innerHTML.replace(ta,\"\"):void 0;if(\"string\"==typeof a&&!wa.test(a)&&(l.htmlSerialize||!ua.test(a))&&(l.leadingWhitespace||!aa.test(a))&&!da[($.exec(a)||[\"\",\"\"])[1].toLowerCase()]){a=n.htmlPrefilter(a);try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(n.cleanData(ea(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=[];return Ha(this,arguments,function(b){var c=this.parentNode;n.inArray(this,a)<0&&(n.cleanData(ea(this)),c&&c.replaceChild(b,this))},a)}}),n.each({appendTo:\"append\",prependTo:\"prepend\",insertBefore:\"before\",insertAfter:\"after\",replaceAll:\"replaceWith\"},function(a,b){n.fn[a]=function(a){for(var c,d=0,e=[],f=n(a),h=f.length-1;h>=d;d++)c=d===h?this:this.clone(!0),n(f[d])[b](c),g.apply(e,c.get());return this.pushStack(e)}});var Ja,Ka={HTML:\"block\",BODY:\"block\"};function La(a,b){var c=n(b.createElement(a)).appendTo(b.body),d=n.css(c[0],\"display\");return c.detach(),d}function Ma(a){var b=d,c=Ka[a];return c||(c=La(a,b),\"none\"!==c&&c||(Ja=(Ja||n(\"<iframe frameborder='0' width='0' height='0'/>\")).appendTo(b.documentElement),b=(Ja[0].contentWindow||Ja[0].contentDocument).document,b.write(),b.close(),c=La(a,b),Ja.detach()),Ka[a]=c),c}var Na=/^margin/,Oa=new RegExp(\"^(\"+T+\")(?!px)[a-z%]+$\",\"i\"),Pa=function(a,b,c,d){var e,f,g={};for(f in b)g[f]=a.style[f],a.style[f]=b[f];e=c.apply(a,d||[]);for(f in b)a.style[f]=g[f];return e},Qa=d.documentElement;!function(){var b,c,e,f,g,h,i=d.createElement(\"div\"),j=d.createElement(\"div\");if(j.style){j.style.cssText=\"float:left;opacity:.5\",l.opacity=\"0.5\"===j.style.opacity,l.cssFloat=!!j.style.cssFloat,j.style.backgroundClip=\"content-box\",j.cloneNode(!0).style.backgroundClip=\"\",l.clearCloneStyle=\"content-box\"===j.style.backgroundClip,i=d.createElement(\"div\"),i.style.cssText=\"border:0;width:8px;height:0;top:0;left:-9999px;padding:0;margin-top:1px;position:absolute\",j.innerHTML=\"\",i.appendChild(j),l.boxSizing=\"\"===j.style.boxSizing||\"\"===j.style.MozBoxSizing||\"\"===j.style.WebkitBoxSizing,n.extend(l,{reliableHiddenOffsets:function(){return null==b&&k(),f},boxSizingReliable:function(){return null==b&&k(),e},pixelMarginRight:function(){return null==b&&k(),c},pixelPosition:function(){return null==b&&k(),b},reliableMarginRight:function(){return null==b&&k(),g},reliableMarginLeft:function(){return null==b&&k(),h}});function k(){var k,l,m=d.documentElement;m.appendChild(i),j.style.cssText=\"-webkit-box-sizing:border-box;box-sizing:border-box;position:relative;display:block;margin:auto;border:1px;padding:1px;top:1%;width:50%\",b=e=h=!1,c=g=!0,a.getComputedStyle&&(l=a.getComputedStyle(j),b=\"1%\"!==(l||{}).top,h=\"2px\"===(l||{}).marginLeft,e=\"4px\"===(l||{width:\"4px\"}).width,j.style.marginRight=\"50%\",c=\"4px\"===(l||{marginRight:\"4px\"}).marginRight,k=j.appendChild(d.createElement(\"div\")),k.style.cssText=j.style.cssText=\"-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;display:block;margin:0;border:0;padding:0\",k.style.marginRight=k.style.width=\"0\",j.style.width=\"1px\",g=!parseFloat((a.getComputedStyle(k)||{}).marginRight),j.removeChild(k)),j.style.display=\"none\",f=0===j.getClientRects().length,f&&(j.style.display=\"\",j.innerHTML=\"<table><tr><td></td><td>t</td></tr></table>\",j.childNodes[0].style.borderCollapse=\"separate\",k=j.getElementsByTagName(\"td\"),k[0].style.cssText=\"margin:0;border:0;padding:0;display:none\",f=0===k[0].offsetHeight,f&&(k[0].style.display=\"\",k[1].style.display=\"none\",f=0===k[0].offsetHeight)),m.removeChild(i)}}}();var Ra,Sa,Ta=/^(top|right|bottom|left)$/;a.getComputedStyle?(Ra=function(b){var c=b.ownerDocument.defaultView;return c&&c.opener||(c=a),c.getComputedStyle(b)},Sa=function(a,b,c){var d,e,f,g,h=a.style;return c=c||Ra(a),g=c?c.getPropertyValue(b)||c[b]:void 0,\"\"!==g&&void 0!==g||n.contains(a.ownerDocument,a)||(g=n.style(a,b)),c&&!l.pixelMarginRight()&&Oa.test(g)&&Na.test(b)&&(d=h.width,e=h.minWidth,f=h.maxWidth,h.minWidth=h.maxWidth=h.width=g,g=c.width,h.width=d,h.minWidth=e,h.maxWidth=f),void 0===g?g:g+\"\"}):Qa.currentStyle&&(Ra=function(a){return a.currentStyle},Sa=function(a,b,c){var d,e,f,g,h=a.style;return c=c||Ra(a),g=c?c[b]:void 0,null==g&&h&&h[b]&&(g=h[b]),Oa.test(g)&&!Ta.test(b)&&(d=h.left,e=a.runtimeStyle,f=e&&e.left,f&&(e.left=a.currentStyle.left),h.left=\"fontSize\"===b?\"1em\":g,g=h.pixelLeft+\"px\",h.left=d,f&&(e.left=f)),void 0===g?g:g+\"\"||\"auto\"});function Ua(a,b){return{get:function(){return a()?void delete this.get:(this.get=b).apply(this,arguments)}}}var Va=/alpha\\([^)]*\\)/i,Wa=/opacity\\s*=\\s*([^)]*)/i,Xa=/^(none|table(?!-c[ea]).+)/,Ya=new RegExp(\"^(\"+T+\")(.*)$\",\"i\"),Za={position:\"absolute\",visibility:\"hidden\",display:\"block\"},$a={letterSpacing:\"0\",fontWeight:\"400\"},_a=[\"Webkit\",\"O\",\"Moz\",\"ms\"],ab=d.createElement(\"div\").style;function bb(a){if(a in ab)return a;var b=a.charAt(0).toUpperCase()+a.slice(1),c=_a.length;while(c--)if(a=_a[c]+b,a in ab)return a}function cb(a,b){for(var c,d,e,f=[],g=0,h=a.length;h>g;g++)d=a[g],d.style&&(f[g]=n._data(d,\"olddisplay\"),c=d.style.display,b?(f[g]||\"none\"!==c||(d.style.display=\"\"),\"\"===d.style.display&&W(d)&&(f[g]=n._data(d,\"olddisplay\",Ma(d.nodeName)))):(e=W(d),(c&&\"none\"!==c||!e)&&n._data(d,\"olddisplay\",e?c:n.css(d,\"display\"))));for(g=0;h>g;g++)d=a[g],d.style&&(b&&\"none\"!==d.style.display&&\"\"!==d.style.display||(d.style.display=b?f[g]||\"\":\"none\"));return a}function db(a,b,c){var d=Ya.exec(b);return d?Math.max(0,d[1]-(c||0))+(d[2]||\"px\"):b}function eb(a,b,c,d,e){for(var f=c===(d?\"border\":\"content\")?4:\"width\"===b?1:0,g=0;4>f;f+=2)\"margin\"===c&&(g+=n.css(a,c+V[f],!0,e)),d?(\"content\"===c&&(g-=n.css(a,\"padding\"+V[f],!0,e)),\"margin\"!==c&&(g-=n.css(a,\"border\"+V[f]+\"Width\",!0,e))):(g+=n.css(a,\"padding\"+V[f],!0,e),\"padding\"!==c&&(g+=n.css(a,\"border\"+V[f]+\"Width\",!0,e)));return g}function fb(a,b,c){var d=!0,e=\"width\"===b?a.offsetWidth:a.offsetHeight,f=Ra(a),g=l.boxSizing&&\"border-box\"===n.css(a,\"boxSizing\",!1,f);if(0>=e||null==e){if(e=Sa(a,b,f),(0>e||null==e)&&(e=a.style[b]),Oa.test(e))return e;d=g&&(l.boxSizingReliable()||e===a.style[b]),e=parseFloat(e)||0}return e+eb(a,b,c||(g?\"border\":\"content\"),d,f)+\"px\"}n.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=Sa(a,\"opacity\");return\"\"===c?\"1\":c}}}},cssNumber:{animationIterationCount:!0,columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{\"float\":l.cssFloat?\"cssFloat\":\"styleFloat\"},style:function(a,b,c,d){if(a&&3!==a.nodeType&&8!==a.nodeType&&a.style){var e,f,g,h=n.camelCase(b),i=a.style;if(b=n.cssProps[h]||(n.cssProps[h]=bb(h)||h),g=n.cssHooks[b]||n.cssHooks[h],void 0===c)return g&&\"get\"in g&&void 0!==(e=g.get(a,!1,d))?e:i[b];if(f=typeof c,\"string\"===f&&(e=U.exec(c))&&e[1]&&(c=X(a,b,e),f=\"number\"),null!=c&&c===c&&(\"number\"===f&&(c+=e&&e[3]||(n.cssNumber[h]?\"\":\"px\")),l.clearCloneStyle||\"\"!==c||0!==b.indexOf(\"background\")||(i[b]=\"inherit\"),!(g&&\"set\"in g&&void 0===(c=g.set(a,c,d)))))try{i[b]=c}catch(j){}}},css:function(a,b,c,d){var e,f,g,h=n.camelCase(b);return b=n.cssProps[h]||(n.cssProps[h]=bb(h)||h),g=n.cssHooks[b]||n.cssHooks[h],g&&\"get\"in g&&(f=g.get(a,!0,c)),void 0===f&&(f=Sa(a,b,d)),\"normal\"===f&&b in $a&&(f=$a[b]),\"\"===c||c?(e=parseFloat(f),c===!0||isFinite(e)?e||0:f):f}}),n.each([\"height\",\"width\"],function(a,b){n.cssHooks[b]={get:function(a,c,d){return c?Xa.test(n.css(a,\"display\"))&&0===a.offsetWidth?Pa(a,Za,function(){return fb(a,b,d)}):fb(a,b,d):void 0},set:function(a,c,d){var e=d&&Ra(a);return db(a,c,d?eb(a,b,d,l.boxSizing&&\"border-box\"===n.css(a,\"boxSizing\",!1,e),e):0)}}}),l.opacity||(n.cssHooks.opacity={get:function(a,b){return Wa.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||\"\")?.01*parseFloat(RegExp.$1)+\"\":b?\"1\":\"\"},set:function(a,b){var c=a.style,d=a.currentStyle,e=n.isNumeric(b)?\"alpha(opacity=\"+100*b+\")\":\"\",f=d&&d.filter||c.filter||\"\";c.zoom=1,(b>=1||\"\"===b)&&\"\"===n.trim(f.replace(Va,\"\"))&&c.removeAttribute&&(c.removeAttribute(\"filter\"),\"\"===b||d&&!d.filter)||(c.filter=Va.test(f)?f.replace(Va,e):f+\" \"+e)}}),n.cssHooks.marginRight=Ua(l.reliableMarginRight,function(a,b){return b?Pa(a,{display:\"inline-block\"},Sa,[a,\"marginRight\"]):void 0}),n.cssHooks.marginLeft=Ua(l.reliableMarginLeft,function(a,b){return b?(parseFloat(Sa(a,\"marginLeft\"))||(n.contains(a.ownerDocument,a)?a.getBoundingClientRect().left-Pa(a,{\nmarginLeft:0},function(){return a.getBoundingClientRect().left}):0))+\"px\":void 0}),n.each({margin:\"\",padding:\"\",border:\"Width\"},function(a,b){n.cssHooks[a+b]={expand:function(c){for(var d=0,e={},f=\"string\"==typeof c?c.split(\" \"):[c];4>d;d++)e[a+V[d]+b]=f[d]||f[d-2]||f[0];return e}},Na.test(a)||(n.cssHooks[a+b].set=db)}),n.fn.extend({css:function(a,b){return Y(this,function(a,b,c){var d,e,f={},g=0;if(n.isArray(b)){for(d=Ra(a),e=b.length;e>g;g++)f[b[g]]=n.css(a,b[g],!1,d);return f}return void 0!==c?n.style(a,b,c):n.css(a,b)},a,b,arguments.length>1)},show:function(){return cb(this,!0)},hide:function(){return cb(this)},toggle:function(a){return\"boolean\"==typeof a?a?this.show():this.hide():this.each(function(){W(this)?n(this).show():n(this).hide()})}});function gb(a,b,c,d,e){return new gb.prototype.init(a,b,c,d,e)}n.Tween=gb,gb.prototype={constructor:gb,init:function(a,b,c,d,e,f){this.elem=a,this.prop=c,this.easing=e||n.easing._default,this.options=b,this.start=this.now=this.cur(),this.end=d,this.unit=f||(n.cssNumber[c]?\"\":\"px\")},cur:function(){var a=gb.propHooks[this.prop];return a&&a.get?a.get(this):gb.propHooks._default.get(this)},run:function(a){var b,c=gb.propHooks[this.prop];return this.options.duration?this.pos=b=n.easing[this.easing](a,this.options.duration*a,0,1,this.options.duration):this.pos=b=a,this.now=(this.end-this.start)*b+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),c&&c.set?c.set(this):gb.propHooks._default.set(this),this}},gb.prototype.init.prototype=gb.prototype,gb.propHooks={_default:{get:function(a){var b;return 1!==a.elem.nodeType||null!=a.elem[a.prop]&&null==a.elem.style[a.prop]?a.elem[a.prop]:(b=n.css(a.elem,a.prop,\"\"),b&&\"auto\"!==b?b:0)},set:function(a){n.fx.step[a.prop]?n.fx.step[a.prop](a):1!==a.elem.nodeType||null==a.elem.style[n.cssProps[a.prop]]&&!n.cssHooks[a.prop]?a.elem[a.prop]=a.now:n.style(a.elem,a.prop,a.now+a.unit)}}},gb.propHooks.scrollTop=gb.propHooks.scrollLeft={set:function(a){a.elem.nodeType&&a.elem.parentNode&&(a.elem[a.prop]=a.now)}},n.easing={linear:function(a){return a},swing:function(a){return.5-Math.cos(a*Math.PI)/2},_default:\"swing\"},n.fx=gb.prototype.init,n.fx.step={};var hb,ib,jb=/^(?:toggle|show|hide)$/,kb=/queueHooks$/;function lb(){return a.setTimeout(function(){hb=void 0}),hb=n.now()}function mb(a,b){var c,d={height:a},e=0;for(b=b?1:0;4>e;e+=2-b)c=V[e],d[\"margin\"+c]=d[\"padding\"+c]=a;return b&&(d.opacity=d.width=a),d}function nb(a,b,c){for(var d,e=(qb.tweeners[b]||[]).concat(qb.tweeners[\"*\"]),f=0,g=e.length;g>f;f++)if(d=e[f].call(c,b,a))return d}function ob(a,b,c){var d,e,f,g,h,i,j,k,m=this,o={},p=a.style,q=a.nodeType&&W(a),r=n._data(a,\"fxshow\");c.queue||(h=n._queueHooks(a,\"fx\"),null==h.unqueued&&(h.unqueued=0,i=h.empty.fire,h.empty.fire=function(){h.unqueued||i()}),h.unqueued++,m.always(function(){m.always(function(){h.unqueued--,n.queue(a,\"fx\").length||h.empty.fire()})})),1===a.nodeType&&(\"height\"in b||\"width\"in b)&&(c.overflow=[p.overflow,p.overflowX,p.overflowY],j=n.css(a,\"display\"),k=\"none\"===j?n._data(a,\"olddisplay\")||Ma(a.nodeName):j,\"inline\"===k&&\"none\"===n.css(a,\"float\")&&(l.inlineBlockNeedsLayout&&\"inline\"!==Ma(a.nodeName)?p.zoom=1:p.display=\"inline-block\")),c.overflow&&(p.overflow=\"hidden\",l.shrinkWrapBlocks()||m.always(function(){p.overflow=c.overflow[0],p.overflowX=c.overflow[1],p.overflowY=c.overflow[2]}));for(d in b)if(e=b[d],jb.exec(e)){if(delete b[d],f=f||\"toggle\"===e,e===(q?\"hide\":\"show\")){if(\"show\"!==e||!r||void 0===r[d])continue;q=!0}o[d]=r&&r[d]||n.style(a,d)}else j=void 0;if(n.isEmptyObject(o))\"inline\"===(\"none\"===j?Ma(a.nodeName):j)&&(p.display=j);else{r?\"hidden\"in r&&(q=r.hidden):r=n._data(a,\"fxshow\",{}),f&&(r.hidden=!q),q?n(a).show():m.done(function(){n(a).hide()}),m.done(function(){var b;n._removeData(a,\"fxshow\");for(b in o)n.style(a,b,o[b])});for(d in o)g=nb(q?r[d]:0,d,m),d in r||(r[d]=g.start,q&&(g.end=g.start,g.start=\"width\"===d||\"height\"===d?1:0))}}function pb(a,b){var c,d,e,f,g;for(c in a)if(d=n.camelCase(c),e=b[d],f=a[c],n.isArray(f)&&(e=f[1],f=a[c]=f[0]),c!==d&&(a[d]=f,delete a[c]),g=n.cssHooks[d],g&&\"expand\"in g){f=g.expand(f),delete a[d];for(c in f)c in a||(a[c]=f[c],b[c]=e)}else b[d]=e}function qb(a,b,c){var d,e,f=0,g=qb.prefilters.length,h=n.Deferred().always(function(){delete i.elem}),i=function(){if(e)return!1;for(var b=hb||lb(),c=Math.max(0,j.startTime+j.duration-b),d=c/j.duration||0,f=1-d,g=0,i=j.tweens.length;i>g;g++)j.tweens[g].run(f);return h.notifyWith(a,[j,f,c]),1>f&&i?c:(h.resolveWith(a,[j]),!1)},j=h.promise({elem:a,props:n.extend({},b),opts:n.extend(!0,{specialEasing:{},easing:n.easing._default},c),originalProperties:b,originalOptions:c,startTime:hb||lb(),duration:c.duration,tweens:[],createTween:function(b,c){var d=n.Tween(a,j.opts,b,c,j.opts.specialEasing[b]||j.opts.easing);return j.tweens.push(d),d},stop:function(b){var c=0,d=b?j.tweens.length:0;if(e)return this;for(e=!0;d>c;c++)j.tweens[c].run(1);return b?(h.notifyWith(a,[j,1,0]),h.resolveWith(a,[j,b])):h.rejectWith(a,[j,b]),this}}),k=j.props;for(pb(k,j.opts.specialEasing);g>f;f++)if(d=qb.prefilters[f].call(j,a,k,j.opts))return n.isFunction(d.stop)&&(n._queueHooks(j.elem,j.opts.queue).stop=n.proxy(d.stop,d)),d;return n.map(k,nb,j),n.isFunction(j.opts.start)&&j.opts.start.call(a,j),n.fx.timer(n.extend(i,{elem:a,anim:j,queue:j.opts.queue})),j.progress(j.opts.progress).done(j.opts.done,j.opts.complete).fail(j.opts.fail).always(j.opts.always)}n.Animation=n.extend(qb,{tweeners:{\"*\":[function(a,b){var c=this.createTween(a,b);return X(c.elem,a,U.exec(b),c),c}]},tweener:function(a,b){n.isFunction(a)?(b=a,a=[\"*\"]):a=a.match(G);for(var c,d=0,e=a.length;e>d;d++)c=a[d],qb.tweeners[c]=qb.tweeners[c]||[],qb.tweeners[c].unshift(b)},prefilters:[ob],prefilter:function(a,b){b?qb.prefilters.unshift(a):qb.prefilters.push(a)}}),n.speed=function(a,b,c){var d=a&&\"object\"==typeof a?n.extend({},a):{complete:c||!c&&b||n.isFunction(a)&&a,duration:a,easing:c&&b||b&&!n.isFunction(b)&&b};return d.duration=n.fx.off?0:\"number\"==typeof d.duration?d.duration:d.duration in n.fx.speeds?n.fx.speeds[d.duration]:n.fx.speeds._default,null!=d.queue&&d.queue!==!0||(d.queue=\"fx\"),d.old=d.complete,d.complete=function(){n.isFunction(d.old)&&d.old.call(this),d.queue&&n.dequeue(this,d.queue)},d},n.fn.extend({fadeTo:function(a,b,c,d){return this.filter(W).css(\"opacity\",0).show().end().animate({opacity:b},a,c,d)},animate:function(a,b,c,d){var e=n.isEmptyObject(a),f=n.speed(b,c,d),g=function(){var b=qb(this,n.extend({},a),f);(e||n._data(this,\"finish\"))&&b.stop(!0)};return g.finish=g,e||f.queue===!1?this.each(g):this.queue(f.queue,g)},stop:function(a,b,c){var d=function(a){var b=a.stop;delete a.stop,b(c)};return\"string\"!=typeof a&&(c=b,b=a,a=void 0),b&&a!==!1&&this.queue(a||\"fx\",[]),this.each(function(){var b=!0,e=null!=a&&a+\"queueHooks\",f=n.timers,g=n._data(this);if(e)g[e]&&g[e].stop&&d(g[e]);else for(e in g)g[e]&&g[e].stop&&kb.test(e)&&d(g[e]);for(e=f.length;e--;)f[e].elem!==this||null!=a&&f[e].queue!==a||(f[e].anim.stop(c),b=!1,f.splice(e,1));!b&&c||n.dequeue(this,a)})},finish:function(a){return a!==!1&&(a=a||\"fx\"),this.each(function(){var b,c=n._data(this),d=c[a+\"queue\"],e=c[a+\"queueHooks\"],f=n.timers,g=d?d.length:0;for(c.finish=!0,n.queue(this,a,[]),e&&e.stop&&e.stop.call(this,!0),b=f.length;b--;)f[b].elem===this&&f[b].queue===a&&(f[b].anim.stop(!0),f.splice(b,1));for(b=0;g>b;b++)d[b]&&d[b].finish&&d[b].finish.call(this);delete c.finish})}}),n.each([\"toggle\",\"show\",\"hide\"],function(a,b){var c=n.fn[b];n.fn[b]=function(a,d,e){return null==a||\"boolean\"==typeof a?c.apply(this,arguments):this.animate(mb(b,!0),a,d,e)}}),n.each({slideDown:mb(\"show\"),slideUp:mb(\"hide\"),slideToggle:mb(\"toggle\"),fadeIn:{opacity:\"show\"},fadeOut:{opacity:\"hide\"},fadeToggle:{opacity:\"toggle\"}},function(a,b){n.fn[a]=function(a,c,d){return this.animate(b,a,c,d)}}),n.timers=[],n.fx.tick=function(){var a,b=n.timers,c=0;for(hb=n.now();c<b.length;c++)a=b[c],a()||b[c]!==a||b.splice(c--,1);b.length||n.fx.stop(),hb=void 0},n.fx.timer=function(a){n.timers.push(a),a()?n.fx.start():n.timers.pop()},n.fx.interval=13,n.fx.start=function(){ib||(ib=a.setInterval(n.fx.tick,n.fx.interval))},n.fx.stop=function(){a.clearInterval(ib),ib=null},n.fx.speeds={slow:600,fast:200,_default:400},n.fn.delay=function(b,c){return b=n.fx?n.fx.speeds[b]||b:b,c=c||\"fx\",this.queue(c,function(c,d){var e=a.setTimeout(c,b);d.stop=function(){a.clearTimeout(e)}})},function(){var a,b=d.createElement(\"input\"),c=d.createElement(\"div\"),e=d.createElement(\"select\"),f=e.appendChild(d.createElement(\"option\"));c=d.createElement(\"div\"),c.setAttribute(\"className\",\"t\"),c.innerHTML=\"  <link/><table></table><a href='/a'>a</a><input type='checkbox'/>\",a=c.getElementsByTagName(\"a\")[0],b.setAttribute(\"type\",\"checkbox\"),c.appendChild(b),a=c.getElementsByTagName(\"a\")[0],a.style.cssText=\"top:1px\",l.getSetAttribute=\"t\"!==c.className,l.style=/top/.test(a.getAttribute(\"style\")),l.hrefNormalized=\"/a\"===a.getAttribute(\"href\"),l.checkOn=!!b.value,l.optSelected=f.selected,l.enctype=!!d.createElement(\"form\").enctype,e.disabled=!0,l.optDisabled=!f.disabled,b=d.createElement(\"input\"),b.setAttribute(\"value\",\"\"),l.input=\"\"===b.getAttribute(\"value\"),b.value=\"t\",b.setAttribute(\"type\",\"radio\"),l.radioValue=\"t\"===b.value}();var rb=/\\r/g,sb=/[\\x20\\t\\r\\n\\f]+/g;n.fn.extend({val:function(a){var b,c,d,e=this[0];{if(arguments.length)return d=n.isFunction(a),this.each(function(c){var e;1===this.nodeType&&(e=d?a.call(this,c,n(this).val()):a,null==e?e=\"\":\"number\"==typeof e?e+=\"\":n.isArray(e)&&(e=n.map(e,function(a){return null==a?\"\":a+\"\"})),b=n.valHooks[this.type]||n.valHooks[this.nodeName.toLowerCase()],b&&\"set\"in b&&void 0!==b.set(this,e,\"value\")||(this.value=e))});if(e)return b=n.valHooks[e.type]||n.valHooks[e.nodeName.toLowerCase()],b&&\"get\"in b&&void 0!==(c=b.get(e,\"value\"))?c:(c=e.value,\"string\"==typeof c?c.replace(rb,\"\"):null==c?\"\":c)}}}),n.extend({valHooks:{option:{get:function(a){var b=n.find.attr(a,\"value\");return null!=b?b:n.trim(n.text(a)).replace(sb,\" \")}},select:{get:function(a){for(var b,c,d=a.options,e=a.selectedIndex,f=\"select-one\"===a.type||0>e,g=f?null:[],h=f?e+1:d.length,i=0>e?h:f?e:0;h>i;i++)if(c=d[i],(c.selected||i===e)&&(l.optDisabled?!c.disabled:null===c.getAttribute(\"disabled\"))&&(!c.parentNode.disabled||!n.nodeName(c.parentNode,\"optgroup\"))){if(b=n(c).val(),f)return b;g.push(b)}return g},set:function(a,b){var c,d,e=a.options,f=n.makeArray(b),g=e.length;while(g--)if(d=e[g],n.inArray(n.valHooks.option.get(d),f)>-1)try{d.selected=c=!0}catch(h){d.scrollHeight}else d.selected=!1;return c||(a.selectedIndex=-1),e}}}}),n.each([\"radio\",\"checkbox\"],function(){n.valHooks[this]={set:function(a,b){return n.isArray(b)?a.checked=n.inArray(n(a).val(),b)>-1:void 0}},l.checkOn||(n.valHooks[this].get=function(a){return null===a.getAttribute(\"value\")?\"on\":a.value})});var tb,ub,vb=n.expr.attrHandle,wb=/^(?:checked|selected)$/i,xb=l.getSetAttribute,yb=l.input;n.fn.extend({attr:function(a,b){return Y(this,n.attr,a,b,arguments.length>1)},removeAttr:function(a){return this.each(function(){n.removeAttr(this,a)})}}),n.extend({attr:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return\"undefined\"==typeof a.getAttribute?n.prop(a,b,c):(1===f&&n.isXMLDoc(a)||(b=b.toLowerCase(),e=n.attrHooks[b]||(n.expr.match.bool.test(b)?ub:tb)),void 0!==c?null===c?void n.removeAttr(a,b):e&&\"set\"in e&&void 0!==(d=e.set(a,c,b))?d:(a.setAttribute(b,c+\"\"),c):e&&\"get\"in e&&null!==(d=e.get(a,b))?d:(d=n.find.attr(a,b),null==d?void 0:d))},attrHooks:{type:{set:function(a,b){if(!l.radioValue&&\"radio\"===b&&n.nodeName(a,\"input\")){var c=a.value;return a.setAttribute(\"type\",b),c&&(a.value=c),b}}}},removeAttr:function(a,b){var c,d,e=0,f=b&&b.match(G);if(f&&1===a.nodeType)while(c=f[e++])d=n.propFix[c]||c,n.expr.match.bool.test(c)?yb&&xb||!wb.test(c)?a[d]=!1:a[n.camelCase(\"default-\"+c)]=a[d]=!1:n.attr(a,c,\"\"),a.removeAttribute(xb?c:d)}}),ub={set:function(a,b,c){return b===!1?n.removeAttr(a,c):yb&&xb||!wb.test(c)?a.setAttribute(!xb&&n.propFix[c]||c,c):a[n.camelCase(\"default-\"+c)]=a[c]=!0,c}},n.each(n.expr.match.bool.source.match(/\\w+/g),function(a,b){var c=vb[b]||n.find.attr;yb&&xb||!wb.test(b)?vb[b]=function(a,b,d){var e,f;return d||(f=vb[b],vb[b]=e,e=null!=c(a,b,d)?b.toLowerCase():null,vb[b]=f),e}:vb[b]=function(a,b,c){return c?void 0:a[n.camelCase(\"default-\"+b)]?b.toLowerCase():null}}),yb&&xb||(n.attrHooks.value={set:function(a,b,c){return n.nodeName(a,\"input\")?void(a.defaultValue=b):tb&&tb.set(a,b,c)}}),xb||(tb={set:function(a,b,c){var d=a.getAttributeNode(c);return d||a.setAttributeNode(d=a.ownerDocument.createAttribute(c)),d.value=b+=\"\",\"value\"===c||b===a.getAttribute(c)?b:void 0}},vb.id=vb.name=vb.coords=function(a,b,c){var d;return c?void 0:(d=a.getAttributeNode(b))&&\"\"!==d.value?d.value:null},n.valHooks.button={get:function(a,b){var c=a.getAttributeNode(b);return c&&c.specified?c.value:void 0},set:tb.set},n.attrHooks.contenteditable={set:function(a,b,c){tb.set(a,\"\"===b?!1:b,c)}},n.each([\"width\",\"height\"],function(a,b){n.attrHooks[b]={set:function(a,c){return\"\"===c?(a.setAttribute(b,\"auto\"),c):void 0}}})),l.style||(n.attrHooks.style={get:function(a){return a.style.cssText||void 0},set:function(a,b){return a.style.cssText=b+\"\"}});var zb=/^(?:input|select|textarea|button|object)$/i,Ab=/^(?:a|area)$/i;n.fn.extend({prop:function(a,b){return Y(this,n.prop,a,b,arguments.length>1)},removeProp:function(a){return a=n.propFix[a]||a,this.each(function(){try{this[a]=void 0,delete this[a]}catch(b){}})}}),n.extend({prop:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return 1===f&&n.isXMLDoc(a)||(b=n.propFix[b]||b,e=n.propHooks[b]),void 0!==c?e&&\"set\"in e&&void 0!==(d=e.set(a,c,b))?d:a[b]=c:e&&\"get\"in e&&null!==(d=e.get(a,b))?d:a[b]},propHooks:{tabIndex:{get:function(a){var b=n.find.attr(a,\"tabindex\");return b?parseInt(b,10):zb.test(a.nodeName)||Ab.test(a.nodeName)&&a.href?0:-1}}},propFix:{\"for\":\"htmlFor\",\"class\":\"className\"}}),l.hrefNormalized||n.each([\"href\",\"src\"],function(a,b){n.propHooks[b]={get:function(a){return a.getAttribute(b,4)}}}),l.optSelected||(n.propHooks.selected={get:function(a){var b=a.parentNode;return b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex),null},set:function(a){var b=a.parentNode;b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex)}}),n.each([\"tabIndex\",\"readOnly\",\"maxLength\",\"cellSpacing\",\"cellPadding\",\"rowSpan\",\"colSpan\",\"useMap\",\"frameBorder\",\"contentEditable\"],function(){n.propFix[this.toLowerCase()]=this}),l.enctype||(n.propFix.enctype=\"encoding\");var Bb=/[\\t\\r\\n\\f]/g;function Cb(a){return n.attr(a,\"class\")||\"\"}n.fn.extend({addClass:function(a){var b,c,d,e,f,g,h,i=0;if(n.isFunction(a))return this.each(function(b){n(this).addClass(a.call(this,b,Cb(this)))});if(\"string\"==typeof a&&a){b=a.match(G)||[];while(c=this[i++])if(e=Cb(c),d=1===c.nodeType&&(\" \"+e+\" \").replace(Bb,\" \")){g=0;while(f=b[g++])d.indexOf(\" \"+f+\" \")<0&&(d+=f+\" \");h=n.trim(d),e!==h&&n.attr(c,\"class\",h)}}return this},removeClass:function(a){var b,c,d,e,f,g,h,i=0;if(n.isFunction(a))return this.each(function(b){n(this).removeClass(a.call(this,b,Cb(this)))});if(!arguments.length)return this.attr(\"class\",\"\");if(\"string\"==typeof a&&a){b=a.match(G)||[];while(c=this[i++])if(e=Cb(c),d=1===c.nodeType&&(\" \"+e+\" \").replace(Bb,\" \")){g=0;while(f=b[g++])while(d.indexOf(\" \"+f+\" \")>-1)d=d.replace(\" \"+f+\" \",\" \");h=n.trim(d),e!==h&&n.attr(c,\"class\",h)}}return this},toggleClass:function(a,b){var c=typeof a;return\"boolean\"==typeof b&&\"string\"===c?b?this.addClass(a):this.removeClass(a):n.isFunction(a)?this.each(function(c){n(this).toggleClass(a.call(this,c,Cb(this),b),b)}):this.each(function(){var b,d,e,f;if(\"string\"===c){d=0,e=n(this),f=a.match(G)||[];while(b=f[d++])e.hasClass(b)?e.removeClass(b):e.addClass(b)}else void 0!==a&&\"boolean\"!==c||(b=Cb(this),b&&n._data(this,\"__className__\",b),n.attr(this,\"class\",b||a===!1?\"\":n._data(this,\"__className__\")||\"\"))})},hasClass:function(a){var b,c,d=0;b=\" \"+a+\" \";while(c=this[d++])if(1===c.nodeType&&(\" \"+Cb(c)+\" \").replace(Bb,\" \").indexOf(b)>-1)return!0;return!1}}),n.each(\"blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error contextmenu\".split(\" \"),function(a,b){n.fn[b]=function(a,c){return arguments.length>0?this.on(b,null,a,c):this.trigger(b)}}),n.fn.extend({hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}});var Db=a.location,Eb=n.now(),Fb=/\\?/,Gb=/(,)|(\\[|{)|(}|])|\"(?:[^\"\\\\\\r\\n]|\\\\[\"\\\\\\/bfnrt]|\\\\u[\\da-fA-F]{4})*\"\\s*:?|true|false|null|-?(?!0\\d)\\d+(?:\\.\\d+|)(?:[eE][+-]?\\d+|)/g;n.parseJSON=function(b){if(a.JSON&&a.JSON.parse)return a.JSON.parse(b+\"\");var c,d=null,e=n.trim(b+\"\");return e&&!n.trim(e.replace(Gb,function(a,b,e,f){return c&&b&&(d=0),0===d?a:(c=e||b,d+=!f-!e,\"\")}))?Function(\"return \"+e)():n.error(\"Invalid JSON: \"+b)},n.parseXML=function(b){var c,d;if(!b||\"string\"!=typeof b)return null;try{a.DOMParser?(d=new a.DOMParser,c=d.parseFromString(b,\"text/xml\")):(c=new a.ActiveXObject(\"Microsoft.XMLDOM\"),c.async=\"false\",c.loadXML(b))}catch(e){c=void 0}return c&&c.documentElement&&!c.getElementsByTagName(\"parsererror\").length||n.error(\"Invalid XML: \"+b),c};var Hb=/#.*$/,Ib=/([?&])_=[^&]*/,Jb=/^(.*?):[ \\t]*([^\\r\\n]*)\\r?$/gm,Kb=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Lb=/^(?:GET|HEAD)$/,Mb=/^\\/\\//,Nb=/^([\\w.+-]+:)(?:\\/\\/(?:[^\\/?#]*@|)([^\\/?#:]*)(?::(\\d+)|)|)/,Ob={},Pb={},Qb=\"*/\".concat(\"*\"),Rb=Db.href,Sb=Nb.exec(Rb.toLowerCase())||[];function Tb(a){return function(b,c){\"string\"!=typeof b&&(c=b,b=\"*\");var d,e=0,f=b.toLowerCase().match(G)||[];if(n.isFunction(c))while(d=f[e++])\"+\"===d.charAt(0)?(d=d.slice(1)||\"*\",(a[d]=a[d]||[]).unshift(c)):(a[d]=a[d]||[]).push(c)}}function Ub(a,b,c,d){var e={},f=a===Pb;function g(h){var i;return e[h]=!0,n.each(a[h]||[],function(a,h){var j=h(b,c,d);return\"string\"!=typeof j||f||e[j]?f?!(i=j):void 0:(b.dataTypes.unshift(j),g(j),!1)}),i}return g(b.dataTypes[0])||!e[\"*\"]&&g(\"*\")}function Vb(a,b){var c,d,e=n.ajaxSettings.flatOptions||{};for(d in b)void 0!==b[d]&&((e[d]?a:c||(c={}))[d]=b[d]);return c&&n.extend(!0,a,c),a}function Wb(a,b,c){var d,e,f,g,h=a.contents,i=a.dataTypes;while(\"*\"===i[0])i.shift(),void 0===e&&(e=a.mimeType||b.getResponseHeader(\"Content-Type\"));if(e)for(g in h)if(h[g]&&h[g].test(e)){i.unshift(g);break}if(i[0]in c)f=i[0];else{for(g in c){if(!i[0]||a.converters[g+\" \"+i[0]]){f=g;break}d||(d=g)}f=f||d}return f?(f!==i[0]&&i.unshift(f),c[f]):void 0}function Xb(a,b,c,d){var e,f,g,h,i,j={},k=a.dataTypes.slice();if(k[1])for(g in a.converters)j[g.toLowerCase()]=a.converters[g];f=k.shift();while(f)if(a.responseFields[f]&&(c[a.responseFields[f]]=b),!i&&d&&a.dataFilter&&(b=a.dataFilter(b,a.dataType)),i=f,f=k.shift())if(\"*\"===f)f=i;else if(\"*\"!==i&&i!==f){if(g=j[i+\" \"+f]||j[\"* \"+f],!g)for(e in j)if(h=e.split(\" \"),h[1]===f&&(g=j[i+\" \"+h[0]]||j[\"* \"+h[0]])){g===!0?g=j[e]:j[e]!==!0&&(f=h[0],k.unshift(h[1]));break}if(g!==!0)if(g&&a[\"throws\"])b=g(b);else try{b=g(b)}catch(l){return{state:\"parsererror\",error:g?l:\"No conversion from \"+i+\" to \"+f}}}return{state:\"success\",data:b}}n.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:Rb,type:\"GET\",isLocal:Kb.test(Sb[1]),global:!0,processData:!0,async:!0,contentType:\"application/x-www-form-urlencoded; charset=UTF-8\",accepts:{\"*\":Qb,text:\"text/plain\",html:\"text/html\",xml:\"application/xml, text/xml\",json:\"application/json, text/javascript\"},contents:{xml:/\\bxml\\b/,html:/\\bhtml/,json:/\\bjson\\b/},responseFields:{xml:\"responseXML\",text:\"responseText\",json:\"responseJSON\"},converters:{\"* text\":String,\"text html\":!0,\"text json\":n.parseJSON,\"text xml\":n.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(a,b){return b?Vb(Vb(a,n.ajaxSettings),b):Vb(n.ajaxSettings,a)},ajaxPrefilter:Tb(Ob),ajaxTransport:Tb(Pb),ajax:function(b,c){\"object\"==typeof b&&(c=b,b=void 0),c=c||{};var d,e,f,g,h,i,j,k,l=n.ajaxSetup({},c),m=l.context||l,o=l.context&&(m.nodeType||m.jquery)?n(m):n.event,p=n.Deferred(),q=n.Callbacks(\"once memory\"),r=l.statusCode||{},s={},t={},u=0,v=\"canceled\",w={readyState:0,getResponseHeader:function(a){var b;if(2===u){if(!k){k={};while(b=Jb.exec(g))k[b[1].toLowerCase()]=b[2]}b=k[a.toLowerCase()]}return null==b?null:b},getAllResponseHeaders:function(){return 2===u?g:null},setRequestHeader:function(a,b){var c=a.toLowerCase();return u||(a=t[c]=t[c]||a,s[a]=b),this},overrideMimeType:function(a){return u||(l.mimeType=a),this},statusCode:function(a){var b;if(a)if(2>u)for(b in a)r[b]=[r[b],a[b]];else w.always(a[w.status]);return this},abort:function(a){var b=a||v;return j&&j.abort(b),y(0,b),this}};if(p.promise(w).complete=q.add,w.success=w.done,w.error=w.fail,l.url=((b||l.url||Rb)+\"\").replace(Hb,\"\").replace(Mb,Sb[1]+\"//\"),l.type=c.method||c.type||l.method||l.type,l.dataTypes=n.trim(l.dataType||\"*\").toLowerCase().match(G)||[\"\"],null==l.crossDomain&&(d=Nb.exec(l.url.toLowerCase()),l.crossDomain=!(!d||d[1]===Sb[1]&&d[2]===Sb[2]&&(d[3]||(\"http:\"===d[1]?\"80\":\"443\"))===(Sb[3]||(\"http:\"===Sb[1]?\"80\":\"443\")))),l.data&&l.processData&&\"string\"!=typeof l.data&&(l.data=n.param(l.data,l.traditional)),Ub(Ob,l,c,w),2===u)return w;i=n.event&&l.global,i&&0===n.active++&&n.event.trigger(\"ajaxStart\"),l.type=l.type.toUpperCase(),l.hasContent=!Lb.test(l.type),f=l.url,l.hasContent||(l.data&&(f=l.url+=(Fb.test(f)?\"&\":\"?\")+l.data,delete l.data),l.cache===!1&&(l.url=Ib.test(f)?f.replace(Ib,\"$1_=\"+Eb++):f+(Fb.test(f)?\"&\":\"?\")+\"_=\"+Eb++)),l.ifModified&&(n.lastModified[f]&&w.setRequestHeader(\"If-Modified-Since\",n.lastModified[f]),n.etag[f]&&w.setRequestHeader(\"If-None-Match\",n.etag[f])),(l.data&&l.hasContent&&l.contentType!==!1||c.contentType)&&w.setRequestHeader(\"Content-Type\",l.contentType),w.setRequestHeader(\"Accept\",l.dataTypes[0]&&l.accepts[l.dataTypes[0]]?l.accepts[l.dataTypes[0]]+(\"*\"!==l.dataTypes[0]?\", \"+Qb+\"; q=0.01\":\"\"):l.accepts[\"*\"]);for(e in l.headers)w.setRequestHeader(e,l.headers[e]);if(l.beforeSend&&(l.beforeSend.call(m,w,l)===!1||2===u))return w.abort();v=\"abort\";for(e in{success:1,error:1,complete:1})w[e](l[e]);if(j=Ub(Pb,l,c,w)){if(w.readyState=1,i&&o.trigger(\"ajaxSend\",[w,l]),2===u)return w;l.async&&l.timeout>0&&(h=a.setTimeout(function(){w.abort(\"timeout\")},l.timeout));try{u=1,j.send(s,y)}catch(x){if(!(2>u))throw x;y(-1,x)}}else y(-1,\"No Transport\");function y(b,c,d,e){var k,s,t,v,x,y=c;2!==u&&(u=2,h&&a.clearTimeout(h),j=void 0,g=e||\"\",w.readyState=b>0?4:0,k=b>=200&&300>b||304===b,d&&(v=Wb(l,w,d)),v=Xb(l,v,w,k),k?(l.ifModified&&(x=w.getResponseHeader(\"Last-Modified\"),x&&(n.lastModified[f]=x),x=w.getResponseHeader(\"etag\"),x&&(n.etag[f]=x)),204===b||\"HEAD\"===l.type?y=\"nocontent\":304===b?y=\"notmodified\":(y=v.state,s=v.data,t=v.error,k=!t)):(t=y,!b&&y||(y=\"error\",0>b&&(b=0))),w.status=b,w.statusText=(c||y)+\"\",k?p.resolveWith(m,[s,y,w]):p.rejectWith(m,[w,y,t]),w.statusCode(r),r=void 0,i&&o.trigger(k?\"ajaxSuccess\":\"ajaxError\",[w,l,k?s:t]),q.fireWith(m,[w,y]),i&&(o.trigger(\"ajaxComplete\",[w,l]),--n.active||n.event.trigger(\"ajaxStop\")))}return w},getJSON:function(a,b,c){return n.get(a,b,c,\"json\")},getScript:function(a,b){return n.get(a,void 0,b,\"script\")}}),n.each([\"get\",\"post\"],function(a,b){n[b]=function(a,c,d,e){return n.isFunction(c)&&(e=e||d,d=c,c=void 0),n.ajax(n.extend({url:a,type:b,dataType:e,data:c,success:d},n.isPlainObject(a)&&a))}}),n._evalUrl=function(a){return n.ajax({url:a,type:\"GET\",dataType:\"script\",cache:!0,async:!1,global:!1,\"throws\":!0})},n.fn.extend({wrapAll:function(a){if(n.isFunction(a))return this.each(function(b){n(this).wrapAll(a.call(this,b))});if(this[0]){var b=n(a,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstChild&&1===a.firstChild.nodeType)a=a.firstChild;return a}).append(this)}return this},wrapInner:function(a){return n.isFunction(a)?this.each(function(b){n(this).wrapInner(a.call(this,b))}):this.each(function(){var b=n(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=n.isFunction(a);return this.each(function(c){n(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(){return this.parent().each(function(){n.nodeName(this,\"body\")||n(this).replaceWith(this.childNodes)}).end()}});function Yb(a){return a.style&&a.style.display||n.css(a,\"display\")}function Zb(a){if(!n.contains(a.ownerDocument||d,a))return!0;while(a&&1===a.nodeType){if(\"none\"===Yb(a)||\"hidden\"===a.type)return!0;a=a.parentNode}return!1}n.expr.filters.hidden=function(a){return l.reliableHiddenOffsets()?a.offsetWidth<=0&&a.offsetHeight<=0&&!a.getClientRects().length:Zb(a)},n.expr.filters.visible=function(a){return!n.expr.filters.hidden(a)};var $b=/%20/g,_b=/\\[\\]$/,ac=/\\r?\\n/g,bc=/^(?:submit|button|image|reset|file)$/i,cc=/^(?:input|select|textarea|keygen)/i;function dc(a,b,c,d){var e;if(n.isArray(b))n.each(b,function(b,e){c||_b.test(a)?d(a,e):dc(a+\"[\"+(\"object\"==typeof e&&null!=e?b:\"\")+\"]\",e,c,d)});else if(c||\"object\"!==n.type(b))d(a,b);else for(e in b)dc(a+\"[\"+e+\"]\",b[e],c,d)}n.param=function(a,b){var c,d=[],e=function(a,b){b=n.isFunction(b)?b():null==b?\"\":b,d[d.length]=encodeURIComponent(a)+\"=\"+encodeURIComponent(b)};if(void 0===b&&(b=n.ajaxSettings&&n.ajaxSettings.traditional),n.isArray(a)||a.jquery&&!n.isPlainObject(a))n.each(a,function(){e(this.name,this.value)});else for(c in a)dc(c,a[c],b,e);return d.join(\"&\").replace($b,\"+\")},n.fn.extend({serialize:function(){return n.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var a=n.prop(this,\"elements\");return a?n.makeArray(a):this}).filter(function(){var a=this.type;return this.name&&!n(this).is(\":disabled\")&&cc.test(this.nodeName)&&!bc.test(a)&&(this.checked||!Z.test(a))}).map(function(a,b){var c=n(this).val();return null==c?null:n.isArray(c)?n.map(c,function(a){return{name:b.name,value:a.replace(ac,\"\\r\\n\")}}):{name:b.name,value:c.replace(ac,\"\\r\\n\")}}).get()}}),n.ajaxSettings.xhr=void 0!==a.ActiveXObject?function(){return this.isLocal?ic():d.documentMode>8?hc():/^(get|post|head|put|delete|options)$/i.test(this.type)&&hc()||ic()}:hc;var ec=0,fc={},gc=n.ajaxSettings.xhr();a.attachEvent&&a.attachEvent(\"onunload\",function(){for(var a in fc)fc[a](void 0,!0)}),l.cors=!!gc&&\"withCredentials\"in gc,gc=l.ajax=!!gc,gc&&n.ajaxTransport(function(b){if(!b.crossDomain||l.cors){var c;return{send:function(d,e){var f,g=b.xhr(),h=++ec;if(g.open(b.type,b.url,b.async,b.username,b.password),b.xhrFields)for(f in b.xhrFields)g[f]=b.xhrFields[f];b.mimeType&&g.overrideMimeType&&g.overrideMimeType(b.mimeType),b.crossDomain||d[\"X-Requested-With\"]||(d[\"X-Requested-With\"]=\"XMLHttpRequest\");for(f in d)void 0!==d[f]&&g.setRequestHeader(f,d[f]+\"\");g.send(b.hasContent&&b.data||null),c=function(a,d){var f,i,j;if(c&&(d||4===g.readyState))if(delete fc[h],c=void 0,g.onreadystatechange=n.noop,d)4!==g.readyState&&g.abort();else{j={},f=g.status,\"string\"==typeof g.responseText&&(j.text=g.responseText);try{i=g.statusText}catch(k){i=\"\"}f||!b.isLocal||b.crossDomain?1223===f&&(f=204):f=j.text?200:404}j&&e(f,i,j,g.getAllResponseHeaders())},b.async?4===g.readyState?a.setTimeout(c):g.onreadystatechange=fc[h]=c:c()},abort:function(){c&&c(void 0,!0)}}}});function hc(){try{return new a.XMLHttpRequest}catch(b){}}function ic(){try{return new a.ActiveXObject(\"Microsoft.XMLHTTP\")}catch(b){}}n.ajaxSetup({accepts:{script:\"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript\"},contents:{script:/\\b(?:java|ecma)script\\b/},converters:{\"text script\":function(a){return n.globalEval(a),a}}}),n.ajaxPrefilter(\"script\",function(a){void 0===a.cache&&(a.cache=!1),a.crossDomain&&(a.type=\"GET\",a.global=!1)}),n.ajaxTransport(\"script\",function(a){if(a.crossDomain){var b,c=d.head||n(\"head\")[0]||d.documentElement;return{send:function(e,f){b=d.createElement(\"script\"),b.async=!0,a.scriptCharset&&(b.charset=a.scriptCharset),b.src=a.url,b.onload=b.onreadystatechange=function(a,c){(c||!b.readyState||/loaded|complete/.test(b.readyState))&&(b.onload=b.onreadystatechange=null,b.parentNode&&b.parentNode.removeChild(b),b=null,c||f(200,\"success\"))},c.insertBefore(b,c.firstChild)},abort:function(){b&&b.onload(void 0,!0)}}}});var jc=[],kc=/(=)\\?(?=&|$)|\\?\\?/;n.ajaxSetup({jsonp:\"callback\",jsonpCallback:function(){var a=jc.pop()||n.expando+\"_\"+Eb++;return this[a]=!0,a}}),n.ajaxPrefilter(\"json jsonp\",function(b,c,d){var e,f,g,h=b.jsonp!==!1&&(kc.test(b.url)?\"url\":\"string\"==typeof b.data&&0===(b.contentType||\"\").indexOf(\"application/x-www-form-urlencoded\")&&kc.test(b.data)&&\"data\");return h||\"jsonp\"===b.dataTypes[0]?(e=b.jsonpCallback=n.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,h?b[h]=b[h].replace(kc,\"$1\"+e):b.jsonp!==!1&&(b.url+=(Fb.test(b.url)?\"&\":\"?\")+b.jsonp+\"=\"+e),b.converters[\"script json\"]=function(){return g||n.error(e+\" was not called\"),g[0]},b.dataTypes[0]=\"json\",f=a[e],a[e]=function(){g=arguments},d.always(function(){void 0===f?n(a).removeProp(e):a[e]=f,b[e]&&(b.jsonpCallback=c.jsonpCallback,jc.push(e)),g&&n.isFunction(f)&&f(g[0]),g=f=void 0}),\"script\"):void 0}),n.parseHTML=function(a,b,c){if(!a||\"string\"!=typeof a)return null;\"boolean\"==typeof b&&(c=b,b=!1),b=b||d;var e=x.exec(a),f=!c&&[];return e?[b.createElement(e[1])]:(e=ja([a],b,f),f&&f.length&&n(f).remove(),n.merge([],e.childNodes))};var lc=n.fn.load;n.fn.load=function(a,b,c){if(\"string\"!=typeof a&&lc)return lc.apply(this,arguments);var d,e,f,g=this,h=a.indexOf(\" \");return h>-1&&(d=n.trim(a.slice(h,a.length)),a=a.slice(0,h)),n.isFunction(b)?(c=b,b=void 0):b&&\"object\"==typeof b&&(e=\"POST\"),g.length>0&&n.ajax({url:a,type:e||\"GET\",dataType:\"html\",data:b}).done(function(a){f=arguments,g.html(d?n(\"<div>\").append(n.parseHTML(a)).find(d):a)}).always(c&&function(a,b){g.each(function(){c.apply(this,f||[a.responseText,b,a])})}),this},n.each([\"ajaxStart\",\"ajaxStop\",\"ajaxComplete\",\"ajaxError\",\"ajaxSuccess\",\"ajaxSend\"],function(a,b){n.fn[b]=function(a){return this.on(b,a)}}),n.expr.filters.animated=function(a){return n.grep(n.timers,function(b){return a===b.elem}).length};function mc(a){return n.isWindow(a)?a:9===a.nodeType?a.defaultView||a.parentWindow:!1}n.offset={setOffset:function(a,b,c){var d,e,f,g,h,i,j,k=n.css(a,\"position\"),l=n(a),m={};\"static\"===k&&(a.style.position=\"relative\"),h=l.offset(),f=n.css(a,\"top\"),i=n.css(a,\"left\"),j=(\"absolute\"===k||\"fixed\"===k)&&n.inArray(\"auto\",[f,i])>-1,j?(d=l.position(),g=d.top,e=d.left):(g=parseFloat(f)||0,e=parseFloat(i)||0),n.isFunction(b)&&(b=b.call(a,c,n.extend({},h))),null!=b.top&&(m.top=b.top-h.top+g),null!=b.left&&(m.left=b.left-h.left+e),\"using\"in b?b.using.call(a,m):l.css(m)}},n.fn.extend({offset:function(a){if(arguments.length)return void 0===a?this:this.each(function(b){n.offset.setOffset(this,a,b)});var b,c,d={top:0,left:0},e=this[0],f=e&&e.ownerDocument;if(f)return b=f.documentElement,n.contains(b,e)?(\"undefined\"!=typeof e.getBoundingClientRect&&(d=e.getBoundingClientRect()),c=mc(f),{top:d.top+(c.pageYOffset||b.scrollTop)-(b.clientTop||0),left:d.left+(c.pageXOffset||b.scrollLeft)-(b.clientLeft||0)}):d},position:function(){if(this[0]){var a,b,c={top:0,left:0},d=this[0];return\"fixed\"===n.css(d,\"position\")?b=d.getBoundingClientRect():(a=this.offsetParent(),b=this.offset(),n.nodeName(a[0],\"html\")||(c=a.offset()),c.top+=n.css(a[0],\"borderTopWidth\",!0),c.left+=n.css(a[0],\"borderLeftWidth\",!0)),{top:b.top-c.top-n.css(d,\"marginTop\",!0),left:b.left-c.left-n.css(d,\"marginLeft\",!0)}}},offsetParent:function(){return this.map(function(){var a=this.offsetParent;while(a&&!n.nodeName(a,\"html\")&&\"static\"===n.css(a,\"position\"))a=a.offsetParent;return a||Qa})}}),n.each({scrollLeft:\"pageXOffset\",scrollTop:\"pageYOffset\"},function(a,b){var c=/Y/.test(b);n.fn[a]=function(d){return Y(this,function(a,d,e){var f=mc(a);return void 0===e?f?b in f?f[b]:f.document.documentElement[d]:a[d]:void(f?f.scrollTo(c?n(f).scrollLeft():e,c?e:n(f).scrollTop()):a[d]=e)},a,d,arguments.length,null)}}),n.each([\"top\",\"left\"],function(a,b){n.cssHooks[b]=Ua(l.pixelPosition,function(a,c){return c?(c=Sa(a,b),Oa.test(c)?n(a).position()[b]+\"px\":c):void 0})}),n.each({Height:\"height\",Width:\"width\"},function(a,b){n.each({\npadding:\"inner\"+a,content:b,\"\":\"outer\"+a},function(c,d){n.fn[d]=function(d,e){var f=arguments.length&&(c||\"boolean\"!=typeof d),g=c||(d===!0||e===!0?\"margin\":\"border\");return Y(this,function(b,c,d){var e;return n.isWindow(b)?b.document.documentElement[\"client\"+a]:9===b.nodeType?(e=b.documentElement,Math.max(b.body[\"scroll\"+a],e[\"scroll\"+a],b.body[\"offset\"+a],e[\"offset\"+a],e[\"client\"+a])):void 0===d?n.css(b,c,g):n.style(b,c,d,g)},b,f?d:void 0,f,null)}})}),n.fn.extend({bind:function(a,b,c){return this.on(a,null,b,c)},unbind:function(a,b){return this.off(a,null,b)},delegate:function(a,b,c,d){return this.on(b,a,c,d)},undelegate:function(a,b,c){return 1===arguments.length?this.off(a,\"**\"):this.off(b,a||\"**\",c)}}),n.fn.size=function(){return this.length},n.fn.andSelf=n.fn.addBack,\"function\"==typeof define&&define.amd&&define(\"jquery\",[],function(){return n});var nc=a.jQuery,oc=a.$;return n.noConflict=function(b){return a.$===n&&(a.$=oc),b&&a.jQuery===n&&(a.jQuery=nc),n},b||(a.jQuery=a.$=n),n});\n\n/*!\n * Bootstrap v3.3.7 (http://getbootstrap.com)\n * Copyright 2011-2016 Twitter, Inc.\n * Licensed under the MIT license\n */\nif(\"undefined\"==typeof jQuery)throw new Error(\"Bootstrap's JavaScript requires jQuery\");+function(a){\"use strict\";var b=a.fn.jquery.split(\" \")[0].split(\".\");if(b[0]<2&&b[1]<9||1==b[0]&&9==b[1]&&b[2]<1||b[0]>3)throw new Error(\"Bootstrap's JavaScript requires jQuery version 1.9.1 or higher, but lower than version 4\")}(jQuery),+function(a){\"use strict\";function b(){var a=document.createElement(\"bootstrap\"),b={WebkitTransition:\"webkitTransitionEnd\",MozTransition:\"transitionend\",OTransition:\"oTransitionEnd otransitionend\",transition:\"transitionend\"};for(var c in b)if(void 0!==a.style[c])return{end:b[c]};return!1}a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one(\"bsTransitionEnd\",function(){c=!0});var e=function(){c||a(d).trigger(a.support.transition.end)};return setTimeout(e,b),this},a(function(){a.support.transition=b(),a.support.transition&&(a.event.special.bsTransitionEnd={bindType:a.support.transition.end,delegateType:a.support.transition.end,handle:function(b){if(a(b.target).is(this))return b.handleObj.handler.apply(this,arguments)}})})}(jQuery),+function(a){\"use strict\";function b(b){return this.each(function(){var c=a(this),e=c.data(\"bs.alert\");e||c.data(\"bs.alert\",e=new d(this)),\"string\"==typeof b&&e[b].call(c)})}var c='[data-dismiss=\"alert\"]',d=function(b){a(b).on(\"click\",c,this.close)};d.VERSION=\"3.3.7\",d.TRANSITION_DURATION=150,d.prototype.close=function(b){function c(){g.detach().trigger(\"closed.bs.alert\").remove()}var e=a(this),f=e.attr(\"data-target\");f||(f=e.attr(\"href\"),f=f&&f.replace(/.*(?=#[^\\s]*$)/,\"\"));var g=a(\"#\"===f?[]:f);b&&b.preventDefault(),g.length||(g=e.closest(\".alert\")),g.trigger(b=a.Event(\"close.bs.alert\")),b.isDefaultPrevented()||(g.removeClass(\"in\"),a.support.transition&&g.hasClass(\"fade\")?g.one(\"bsTransitionEnd\",c).emulateTransitionEnd(d.TRANSITION_DURATION):c())};var e=a.fn.alert;a.fn.alert=b,a.fn.alert.Constructor=d,a.fn.alert.noConflict=function(){return a.fn.alert=e,this},a(document).on(\"click.bs.alert.data-api\",c,d.prototype.close)}(jQuery),+function(a){\"use strict\";function b(b){return this.each(function(){var d=a(this),e=d.data(\"bs.button\"),f=\"object\"==typeof b&&b;e||d.data(\"bs.button\",e=new c(this,f)),\"toggle\"==b?e.toggle():b&&e.setState(b)})}var c=function(b,d){this.$element=a(b),this.options=a.extend({},c.DEFAULTS,d),this.isLoading=!1};c.VERSION=\"3.3.7\",c.DEFAULTS={loadingText:\"loading...\"},c.prototype.setState=function(b){var c=\"disabled\",d=this.$element,e=d.is(\"input\")?\"val\":\"html\",f=d.data();b+=\"Text\",null==f.resetText&&d.data(\"resetText\",d[e]()),setTimeout(a.proxy(function(){d[e](null==f[b]?this.options[b]:f[b]),\"loadingText\"==b?(this.isLoading=!0,d.addClass(c).attr(c,c).prop(c,!0)):this.isLoading&&(this.isLoading=!1,d.removeClass(c).removeAttr(c).prop(c,!1))},this),0)},c.prototype.toggle=function(){var a=!0,b=this.$element.closest('[data-toggle=\"buttons\"]');if(b.length){var c=this.$element.find(\"input\");\"radio\"==c.prop(\"type\")?(c.prop(\"checked\")&&(a=!1),b.find(\".active\").removeClass(\"active\"),this.$element.addClass(\"active\")):\"checkbox\"==c.prop(\"type\")&&(c.prop(\"checked\")!==this.$element.hasClass(\"active\")&&(a=!1),this.$element.toggleClass(\"active\")),c.prop(\"checked\",this.$element.hasClass(\"active\")),a&&c.trigger(\"change\")}else this.$element.attr(\"aria-pressed\",!this.$element.hasClass(\"active\")),this.$element.toggleClass(\"active\")};var d=a.fn.button;a.fn.button=b,a.fn.button.Constructor=c,a.fn.button.noConflict=function(){return a.fn.button=d,this},a(document).on(\"click.bs.button.data-api\",'[data-toggle^=\"button\"]',function(c){var d=a(c.target).closest(\".btn\");b.call(d,\"toggle\"),a(c.target).is('input[type=\"radio\"], input[type=\"checkbox\"]')||(c.preventDefault(),d.is(\"input,button\")?d.trigger(\"focus\"):d.find(\"input:visible,button:visible\").first().trigger(\"focus\"))}).on(\"focus.bs.button.data-api blur.bs.button.data-api\",'[data-toggle^=\"button\"]',function(b){a(b.target).closest(\".btn\").toggleClass(\"focus\",/^focus(in)?$/.test(b.type))})}(jQuery),+function(a){\"use strict\";function b(b){return this.each(function(){var d=a(this),e=d.data(\"bs.carousel\"),f=a.extend({},c.DEFAULTS,d.data(),\"object\"==typeof b&&b),g=\"string\"==typeof b?b:f.slide;e||d.data(\"bs.carousel\",e=new c(this,f)),\"number\"==typeof b?e.to(b):g?e[g]():f.interval&&e.pause().cycle()})}var c=function(b,c){this.$element=a(b),this.$indicators=this.$element.find(\".carousel-indicators\"),this.options=c,this.paused=null,this.sliding=null,this.interval=null,this.$active=null,this.$items=null,this.options.keyboard&&this.$element.on(\"keydown.bs.carousel\",a.proxy(this.keydown,this)),\"hover\"==this.options.pause&&!(\"ontouchstart\"in document.documentElement)&&this.$element.on(\"mouseenter.bs.carousel\",a.proxy(this.pause,this)).on(\"mouseleave.bs.carousel\",a.proxy(this.cycle,this))};c.VERSION=\"3.3.7\",c.TRANSITION_DURATION=600,c.DEFAULTS={interval:5e3,pause:\"hover\",wrap:!0,keyboard:!0},c.prototype.keydown=function(a){if(!/input|textarea/i.test(a.target.tagName)){switch(a.which){case 37:this.prev();break;case 39:this.next();break;default:return}a.preventDefault()}},c.prototype.cycle=function(b){return b||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},c.prototype.getItemIndex=function(a){return this.$items=a.parent().children(\".item\"),this.$items.index(a||this.$active)},c.prototype.getItemForDirection=function(a,b){var c=this.getItemIndex(b),d=\"prev\"==a&&0===c||\"next\"==a&&c==this.$items.length-1;if(d&&!this.options.wrap)return b;var e=\"prev\"==a?-1:1,f=(c+e)%this.$items.length;return this.$items.eq(f)},c.prototype.to=function(a){var b=this,c=this.getItemIndex(this.$active=this.$element.find(\".item.active\"));if(!(a>this.$items.length-1||a<0))return this.sliding?this.$element.one(\"slid.bs.carousel\",function(){b.to(a)}):c==a?this.pause().cycle():this.slide(a>c?\"next\":\"prev\",this.$items.eq(a))},c.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(\".next, .prev\").length&&a.support.transition&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},c.prototype.next=function(){if(!this.sliding)return this.slide(\"next\")},c.prototype.prev=function(){if(!this.sliding)return this.slide(\"prev\")},c.prototype.slide=function(b,d){var e=this.$element.find(\".item.active\"),f=d||this.getItemForDirection(b,e),g=this.interval,h=\"next\"==b?\"left\":\"right\",i=this;if(f.hasClass(\"active\"))return this.sliding=!1;var j=f[0],k=a.Event(\"slide.bs.carousel\",{relatedTarget:j,direction:h});if(this.$element.trigger(k),!k.isDefaultPrevented()){if(this.sliding=!0,g&&this.pause(),this.$indicators.length){this.$indicators.find(\".active\").removeClass(\"active\");var l=a(this.$indicators.children()[this.getItemIndex(f)]);l&&l.addClass(\"active\")}var m=a.Event(\"slid.bs.carousel\",{relatedTarget:j,direction:h});return a.support.transition&&this.$element.hasClass(\"slide\")?(f.addClass(b),f[0].offsetWidth,e.addClass(h),f.addClass(h),e.one(\"bsTransitionEnd\",function(){f.removeClass([b,h].join(\" \")).addClass(\"active\"),e.removeClass([\"active\",h].join(\" \")),i.sliding=!1,setTimeout(function(){i.$element.trigger(m)},0)}).emulateTransitionEnd(c.TRANSITION_DURATION)):(e.removeClass(\"active\"),f.addClass(\"active\"),this.sliding=!1,this.$element.trigger(m)),g&&this.cycle(),this}};var d=a.fn.carousel;a.fn.carousel=b,a.fn.carousel.Constructor=c,a.fn.carousel.noConflict=function(){return a.fn.carousel=d,this};var e=function(c){var d,e=a(this),f=a(e.attr(\"data-target\")||(d=e.attr(\"href\"))&&d.replace(/.*(?=#[^\\s]+$)/,\"\"));if(f.hasClass(\"carousel\")){var g=a.extend({},f.data(),e.data()),h=e.attr(\"data-slide-to\");h&&(g.interval=!1),b.call(f,g),h&&f.data(\"bs.carousel\").to(h),c.preventDefault()}};a(document).on(\"click.bs.carousel.data-api\",\"[data-slide]\",e).on(\"click.bs.carousel.data-api\",\"[data-slide-to]\",e),a(window).on(\"load\",function(){a('[data-ride=\"carousel\"]').each(function(){var c=a(this);b.call(c,c.data())})})}(jQuery),+function(a){\"use strict\";function b(b){var c,d=b.attr(\"data-target\")||(c=b.attr(\"href\"))&&c.replace(/.*(?=#[^\\s]+$)/,\"\");return a(d)}function c(b){return this.each(function(){var c=a(this),e=c.data(\"bs.collapse\"),f=a.extend({},d.DEFAULTS,c.data(),\"object\"==typeof b&&b);!e&&f.toggle&&/show|hide/.test(b)&&(f.toggle=!1),e||c.data(\"bs.collapse\",e=new d(this,f)),\"string\"==typeof b&&e[b]()})}var d=function(b,c){this.$element=a(b),this.options=a.extend({},d.DEFAULTS,c),this.$trigger=a('[data-toggle=\"collapse\"][href=\"#'+b.id+'\"],[data-toggle=\"collapse\"][data-target=\"#'+b.id+'\"]'),this.transitioning=null,this.options.parent?this.$parent=this.getParent():this.addAriaAndCollapsedClass(this.$element,this.$trigger),this.options.toggle&&this.toggle()};d.VERSION=\"3.3.7\",d.TRANSITION_DURATION=350,d.DEFAULTS={toggle:!0},d.prototype.dimension=function(){var a=this.$element.hasClass(\"width\");return a?\"width\":\"height\"},d.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass(\"in\")){var b,e=this.$parent&&this.$parent.children(\".panel\").children(\".in, .collapsing\");if(!(e&&e.length&&(b=e.data(\"bs.collapse\"),b&&b.transitioning))){var f=a.Event(\"show.bs.collapse\");if(this.$element.trigger(f),!f.isDefaultPrevented()){e&&e.length&&(c.call(e,\"hide\"),b||e.data(\"bs.collapse\",null));var g=this.dimension();this.$element.removeClass(\"collapse\").addClass(\"collapsing\")[g](0).attr(\"aria-expanded\",!0),this.$trigger.removeClass(\"collapsed\").attr(\"aria-expanded\",!0),this.transitioning=1;var h=function(){this.$element.removeClass(\"collapsing\").addClass(\"collapse in\")[g](\"\"),this.transitioning=0,this.$element.trigger(\"shown.bs.collapse\")};if(!a.support.transition)return h.call(this);var i=a.camelCase([\"scroll\",g].join(\"-\"));this.$element.one(\"bsTransitionEnd\",a.proxy(h,this)).emulateTransitionEnd(d.TRANSITION_DURATION)[g](this.$element[0][i])}}}},d.prototype.hide=function(){if(!this.transitioning&&this.$element.hasClass(\"in\")){var b=a.Event(\"hide.bs.collapse\");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.dimension();this.$element[c](this.$element[c]())[0].offsetHeight,this.$element.addClass(\"collapsing\").removeClass(\"collapse in\").attr(\"aria-expanded\",!1),this.$trigger.addClass(\"collapsed\").attr(\"aria-expanded\",!1),this.transitioning=1;var e=function(){this.transitioning=0,this.$element.removeClass(\"collapsing\").addClass(\"collapse\").trigger(\"hidden.bs.collapse\")};return a.support.transition?void this.$element[c](0).one(\"bsTransitionEnd\",a.proxy(e,this)).emulateTransitionEnd(d.TRANSITION_DURATION):e.call(this)}}},d.prototype.toggle=function(){this[this.$element.hasClass(\"in\")?\"hide\":\"show\"]()},d.prototype.getParent=function(){return a(this.options.parent).find('[data-toggle=\"collapse\"][data-parent=\"'+this.options.parent+'\"]').each(a.proxy(function(c,d){var e=a(d);this.addAriaAndCollapsedClass(b(e),e)},this)).end()},d.prototype.addAriaAndCollapsedClass=function(a,b){var c=a.hasClass(\"in\");a.attr(\"aria-expanded\",c),b.toggleClass(\"collapsed\",!c).attr(\"aria-expanded\",c)};var e=a.fn.collapse;a.fn.collapse=c,a.fn.collapse.Constructor=d,a.fn.collapse.noConflict=function(){return a.fn.collapse=e,this},a(document).on(\"click.bs.collapse.data-api\",'[data-toggle=\"collapse\"]',function(d){var e=a(this);e.attr(\"data-target\")||d.preventDefault();var f=b(e),g=f.data(\"bs.collapse\"),h=g?\"toggle\":e.data();c.call(f,h)})}(jQuery),+function(a){\"use strict\";function b(b){var c=b.attr(\"data-target\");c||(c=b.attr(\"href\"),c=c&&/#[A-Za-z]/.test(c)&&c.replace(/.*(?=#[^\\s]*$)/,\"\"));var d=c&&a(c);return d&&d.length?d:b.parent()}function c(c){c&&3===c.which||(a(e).remove(),a(f).each(function(){var d=a(this),e=b(d),f={relatedTarget:this};e.hasClass(\"open\")&&(c&&\"click\"==c.type&&/input|textarea/i.test(c.target.tagName)&&a.contains(e[0],c.target)||(e.trigger(c=a.Event(\"hide.bs.dropdown\",f)),c.isDefaultPrevented()||(d.attr(\"aria-expanded\",\"false\"),e.removeClass(\"open\").trigger(a.Event(\"hidden.bs.dropdown\",f)))))}))}function d(b){return this.each(function(){var c=a(this),d=c.data(\"bs.dropdown\");d||c.data(\"bs.dropdown\",d=new g(this)),\"string\"==typeof b&&d[b].call(c)})}var e=\".dropdown-backdrop\",f='[data-toggle=\"dropdown\"]',g=function(b){a(b).on(\"click.bs.dropdown\",this.toggle)};g.VERSION=\"3.3.7\",g.prototype.toggle=function(d){var e=a(this);if(!e.is(\".disabled, :disabled\")){var f=b(e),g=f.hasClass(\"open\");if(c(),!g){\"ontouchstart\"in document.documentElement&&!f.closest(\".navbar-nav\").length&&a(document.createElement(\"div\")).addClass(\"dropdown-backdrop\").insertAfter(a(this)).on(\"click\",c);var h={relatedTarget:this};if(f.trigger(d=a.Event(\"show.bs.dropdown\",h)),d.isDefaultPrevented())return;e.trigger(\"focus\").attr(\"aria-expanded\",\"true\"),f.toggleClass(\"open\").trigger(a.Event(\"shown.bs.dropdown\",h))}return!1}},g.prototype.keydown=function(c){if(/(38|40|27|32)/.test(c.which)&&!/input|textarea/i.test(c.target.tagName)){var d=a(this);if(c.preventDefault(),c.stopPropagation(),!d.is(\".disabled, :disabled\")){var e=b(d),g=e.hasClass(\"open\");if(!g&&27!=c.which||g&&27==c.which)return 27==c.which&&e.find(f).trigger(\"focus\"),d.trigger(\"click\");var h=\" li:not(.disabled):visible a\",i=e.find(\".dropdown-menu\"+h);if(i.length){var j=i.index(c.target);38==c.which&&j>0&&j--,40==c.which&&j<i.length-1&&j++,~j||(j=0),i.eq(j).trigger(\"focus\")}}}};var h=a.fn.dropdown;a.fn.dropdown=d,a.fn.dropdown.Constructor=g,a.fn.dropdown.noConflict=function(){return a.fn.dropdown=h,this},a(document).on(\"click.bs.dropdown.data-api\",c).on(\"click.bs.dropdown.data-api\",\".dropdown form\",function(a){a.stopPropagation()}).on(\"click.bs.dropdown.data-api\",f,g.prototype.toggle).on(\"keydown.bs.dropdown.data-api\",f,g.prototype.keydown).on(\"keydown.bs.dropdown.data-api\",\".dropdown-menu\",g.prototype.keydown)}(jQuery),+function(a){\"use strict\";function b(b,d){return this.each(function(){var e=a(this),f=e.data(\"bs.modal\"),g=a.extend({},c.DEFAULTS,e.data(),\"object\"==typeof b&&b);f||e.data(\"bs.modal\",f=new c(this,g)),\"string\"==typeof b?f[b](d):g.show&&f.show(d)})}var c=function(b,c){this.options=c,this.$body=a(document.body),this.$element=a(b),this.$dialog=this.$element.find(\".modal-dialog\"),this.$backdrop=null,this.isShown=null,this.originalBodyPad=null,this.scrollbarWidth=0,this.ignoreBackdropClick=!1,this.options.remote&&this.$element.find(\".modal-content\").load(this.options.remote,a.proxy(function(){this.$element.trigger(\"loaded.bs.modal\")},this))};c.VERSION=\"3.3.7\",c.TRANSITION_DURATION=300,c.BACKDROP_TRANSITION_DURATION=150,c.DEFAULTS={backdrop:!0,keyboard:!0,show:!0},c.prototype.toggle=function(a){return this.isShown?this.hide():this.show(a)},c.prototype.show=function(b){var d=this,e=a.Event(\"show.bs.modal\",{relatedTarget:b});this.$element.trigger(e),this.isShown||e.isDefaultPrevented()||(this.isShown=!0,this.checkScrollbar(),this.setScrollbar(),this.$body.addClass(\"modal-open\"),this.escape(),this.resize(),this.$element.on(\"click.dismiss.bs.modal\",'[data-dismiss=\"modal\"]',a.proxy(this.hide,this)),this.$dialog.on(\"mousedown.dismiss.bs.modal\",function(){d.$element.one(\"mouseup.dismiss.bs.modal\",function(b){a(b.target).is(d.$element)&&(d.ignoreBackdropClick=!0)})}),this.backdrop(function(){var e=a.support.transition&&d.$element.hasClass(\"fade\");d.$element.parent().length||d.$element.appendTo(d.$body),d.$element.show().scrollTop(0),d.adjustDialog(),e&&d.$element[0].offsetWidth,d.$element.addClass(\"in\"),d.enforceFocus();var f=a.Event(\"shown.bs.modal\",{relatedTarget:b});e?d.$dialog.one(\"bsTransitionEnd\",function(){d.$element.trigger(\"focus\").trigger(f)}).emulateTransitionEnd(c.TRANSITION_DURATION):d.$element.trigger(\"focus\").trigger(f)}))},c.prototype.hide=function(b){b&&b.preventDefault(),b=a.Event(\"hide.bs.modal\"),this.$element.trigger(b),this.isShown&&!b.isDefaultPrevented()&&(this.isShown=!1,this.escape(),this.resize(),a(document).off(\"focusin.bs.modal\"),this.$element.removeClass(\"in\").off(\"click.dismiss.bs.modal\").off(\"mouseup.dismiss.bs.modal\"),this.$dialog.off(\"mousedown.dismiss.bs.modal\"),a.support.transition&&this.$element.hasClass(\"fade\")?this.$element.one(\"bsTransitionEnd\",a.proxy(this.hideModal,this)).emulateTransitionEnd(c.TRANSITION_DURATION):this.hideModal())},c.prototype.enforceFocus=function(){a(document).off(\"focusin.bs.modal\").on(\"focusin.bs.modal\",a.proxy(function(a){document===a.target||this.$element[0]===a.target||this.$element.has(a.target).length||this.$element.trigger(\"focus\")},this))},c.prototype.escape=function(){this.isShown&&this.options.keyboard?this.$element.on(\"keydown.dismiss.bs.modal\",a.proxy(function(a){27==a.which&&this.hide()},this)):this.isShown||this.$element.off(\"keydown.dismiss.bs.modal\")},c.prototype.resize=function(){this.isShown?a(window).on(\"resize.bs.modal\",a.proxy(this.handleUpdate,this)):a(window).off(\"resize.bs.modal\")},c.prototype.hideModal=function(){var a=this;this.$element.hide(),this.backdrop(function(){a.$body.removeClass(\"modal-open\"),a.resetAdjustments(),a.resetScrollbar(),a.$element.trigger(\"hidden.bs.modal\")})},c.prototype.removeBackdrop=function(){this.$backdrop&&this.$backdrop.remove(),this.$backdrop=null},c.prototype.backdrop=function(b){var d=this,e=this.$element.hasClass(\"fade\")?\"fade\":\"\";if(this.isShown&&this.options.backdrop){var f=a.support.transition&&e;if(this.$backdrop=a(document.createElement(\"div\")).addClass(\"modal-backdrop \"+e).appendTo(this.$body),this.$element.on(\"click.dismiss.bs.modal\",a.proxy(function(a){return this.ignoreBackdropClick?void(this.ignoreBackdropClick=!1):void(a.target===a.currentTarget&&(\"static\"==this.options.backdrop?this.$element[0].focus():this.hide()))},this)),f&&this.$backdrop[0].offsetWidth,this.$backdrop.addClass(\"in\"),!b)return;f?this.$backdrop.one(\"bsTransitionEnd\",b).emulateTransitionEnd(c.BACKDROP_TRANSITION_DURATION):b()}else if(!this.isShown&&this.$backdrop){this.$backdrop.removeClass(\"in\");var g=function(){d.removeBackdrop(),b&&b()};a.support.transition&&this.$element.hasClass(\"fade\")?this.$backdrop.one(\"bsTransitionEnd\",g).emulateTransitionEnd(c.BACKDROP_TRANSITION_DURATION):g()}else b&&b()},c.prototype.handleUpdate=function(){this.adjustDialog()},c.prototype.adjustDialog=function(){var a=this.$element[0].scrollHeight>document.documentElement.clientHeight;this.$element.css({paddingLeft:!this.bodyIsOverflowing&&a?this.scrollbarWidth:\"\",paddingRight:this.bodyIsOverflowing&&!a?this.scrollbarWidth:\"\"})},c.prototype.resetAdjustments=function(){this.$element.css({paddingLeft:\"\",paddingRight:\"\"})},c.prototype.checkScrollbar=function(){var a=window.innerWidth;if(!a){var b=document.documentElement.getBoundingClientRect();a=b.right-Math.abs(b.left)}this.bodyIsOverflowing=document.body.clientWidth<a,this.scrollbarWidth=this.measureScrollbar()},c.prototype.setScrollbar=function(){var a=parseInt(this.$body.css(\"padding-right\")||0,10);this.originalBodyPad=document.body.style.paddingRight||\"\",this.bodyIsOverflowing&&this.$body.css(\"padding-right\",a+this.scrollbarWidth)},c.prototype.resetScrollbar=function(){this.$body.css(\"padding-right\",this.originalBodyPad)},c.prototype.measureScrollbar=function(){var a=document.createElement(\"div\");a.className=\"modal-scrollbar-measure\",this.$body.append(a);var b=a.offsetWidth-a.clientWidth;return this.$body[0].removeChild(a),b};var d=a.fn.modal;a.fn.modal=b,a.fn.modal.Constructor=c,a.fn.modal.noConflict=function(){return a.fn.modal=d,this},a(document).on(\"click.bs.modal.data-api\",'[data-toggle=\"modal\"]',function(c){var d=a(this),e=d.attr(\"href\"),f=a(d.attr(\"data-target\")||e&&e.replace(/.*(?=#[^\\s]+$)/,\"\")),g=f.data(\"bs.modal\")?\"toggle\":a.extend({remote:!/#/.test(e)&&e},f.data(),d.data());d.is(\"a\")&&c.preventDefault(),f.one(\"show.bs.modal\",function(a){a.isDefaultPrevented()||f.one(\"hidden.bs.modal\",function(){d.is(\":visible\")&&d.trigger(\"focus\")})}),b.call(f,g,this)})}(jQuery),+function(a){\"use strict\";function b(b){return this.each(function(){var d=a(this),e=d.data(\"bs.tooltip\"),f=\"object\"==typeof b&&b;!e&&/destroy|hide/.test(b)||(e||d.data(\"bs.tooltip\",e=new c(this,f)),\"string\"==typeof b&&e[b]())})}var c=function(a,b){this.type=null,this.options=null,this.enabled=null,this.timeout=null,this.hoverState=null,this.$element=null,this.inState=null,this.init(\"tooltip\",a,b)};c.VERSION=\"3.3.7\",c.TRANSITION_DURATION=150,c.DEFAULTS={animation:!0,placement:\"top\",selector:!1,template:'<div class=\"tooltip\" role=\"tooltip\"><div class=\"tooltip-arrow\"></div><div class=\"tooltip-inner\"></div></div>',trigger:\"hover focus\",title:\"\",delay:0,html:!1,container:!1,viewport:{selector:\"body\",padding:0}},c.prototype.init=function(b,c,d){if(this.enabled=!0,this.type=b,this.$element=a(c),this.options=this.getOptions(d),this.$viewport=this.options.viewport&&a(a.isFunction(this.options.viewport)?this.options.viewport.call(this,this.$element):this.options.viewport.selector||this.options.viewport),this.inState={click:!1,hover:!1,focus:!1},this.$element[0]instanceof document.constructor&&!this.options.selector)throw new Error(\"`selector` option must be specified when initializing \"+this.type+\" on the window.document object!\");for(var e=this.options.trigger.split(\" \"),f=e.length;f--;){var g=e[f];if(\"click\"==g)this.$element.on(\"click.\"+this.type,this.options.selector,a.proxy(this.toggle,this));else if(\"manual\"!=g){var h=\"hover\"==g?\"mouseenter\":\"focusin\",i=\"hover\"==g?\"mouseleave\":\"focusout\";this.$element.on(h+\".\"+this.type,this.options.selector,a.proxy(this.enter,this)),this.$element.on(i+\".\"+this.type,this.options.selector,a.proxy(this.leave,this))}}this.options.selector?this._options=a.extend({},this.options,{trigger:\"manual\",selector:\"\"}):this.fixTitle()},c.prototype.getDefaults=function(){return c.DEFAULTS},c.prototype.getOptions=function(b){return b=a.extend({},this.getDefaults(),this.$element.data(),b),b.delay&&\"number\"==typeof b.delay&&(b.delay={show:b.delay,hide:b.delay}),b},c.prototype.getDelegateOptions=function(){var b={},c=this.getDefaults();return this._options&&a.each(this._options,function(a,d){c[a]!=d&&(b[a]=d)}),b},c.prototype.enter=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget).data(\"bs.\"+this.type);return c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data(\"bs.\"+this.type,c)),b instanceof a.Event&&(c.inState[\"focusin\"==b.type?\"focus\":\"hover\"]=!0),c.tip().hasClass(\"in\")||\"in\"==c.hoverState?void(c.hoverState=\"in\"):(clearTimeout(c.timeout),c.hoverState=\"in\",c.options.delay&&c.options.delay.show?void(c.timeout=setTimeout(function(){\"in\"==c.hoverState&&c.show()},c.options.delay.show)):c.show())},c.prototype.isInStateTrue=function(){for(var a in this.inState)if(this.inState[a])return!0;return!1},c.prototype.leave=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget).data(\"bs.\"+this.type);if(c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data(\"bs.\"+this.type,c)),b instanceof a.Event&&(c.inState[\"focusout\"==b.type?\"focus\":\"hover\"]=!1),!c.isInStateTrue())return clearTimeout(c.timeout),c.hoverState=\"out\",c.options.delay&&c.options.delay.hide?void(c.timeout=setTimeout(function(){\"out\"==c.hoverState&&c.hide()},c.options.delay.hide)):c.hide()},c.prototype.show=function(){var b=a.Event(\"show.bs.\"+this.type);if(this.hasContent()&&this.enabled){this.$element.trigger(b);var d=a.contains(this.$element[0].ownerDocument.documentElement,this.$element[0]);if(b.isDefaultPrevented()||!d)return;var e=this,f=this.tip(),g=this.getUID(this.type);this.setContent(),f.attr(\"id\",g),this.$element.attr(\"aria-describedby\",g),this.options.animation&&f.addClass(\"fade\");var h=\"function\"==typeof this.options.placement?this.options.placement.call(this,f[0],this.$element[0]):this.options.placement,i=/\\s?auto?\\s?/i,j=i.test(h);j&&(h=h.replace(i,\"\")||\"top\"),f.detach().css({top:0,left:0,display:\"block\"}).addClass(h).data(\"bs.\"+this.type,this),this.options.container?f.appendTo(this.options.container):f.insertAfter(this.$element),this.$element.trigger(\"inserted.bs.\"+this.type);var k=this.getPosition(),l=f[0].offsetWidth,m=f[0].offsetHeight;if(j){var n=h,o=this.getPosition(this.$viewport);h=\"bottom\"==h&&k.bottom+m>o.bottom?\"top\":\"top\"==h&&k.top-m<o.top?\"bottom\":\"right\"==h&&k.right+l>o.width?\"left\":\"left\"==h&&k.left-l<o.left?\"right\":h,f.removeClass(n).addClass(h)}var p=this.getCalculatedOffset(h,k,l,m);this.applyPlacement(p,h);var q=function(){var a=e.hoverState;e.$element.trigger(\"shown.bs.\"+e.type),e.hoverState=null,\"out\"==a&&e.leave(e)};a.support.transition&&this.$tip.hasClass(\"fade\")?f.one(\"bsTransitionEnd\",q).emulateTransitionEnd(c.TRANSITION_DURATION):q()}},c.prototype.applyPlacement=function(b,c){var d=this.tip(),e=d[0].offsetWidth,f=d[0].offsetHeight,g=parseInt(d.css(\"margin-top\"),10),h=parseInt(d.css(\"margin-left\"),10);isNaN(g)&&(g=0),isNaN(h)&&(h=0),b.top+=g,b.left+=h,a.offset.setOffset(d[0],a.extend({using:function(a){d.css({top:Math.round(a.top),left:Math.round(a.left)})}},b),0),d.addClass(\"in\");var i=d[0].offsetWidth,j=d[0].offsetHeight;\"top\"==c&&j!=f&&(b.top=b.top+f-j);var k=this.getViewportAdjustedDelta(c,b,i,j);k.left?b.left+=k.left:b.top+=k.top;var l=/top|bottom/.test(c),m=l?2*k.left-e+i:2*k.top-f+j,n=l?\"offsetWidth\":\"offsetHeight\";d.offset(b),this.replaceArrow(m,d[0][n],l)},c.prototype.replaceArrow=function(a,b,c){this.arrow().css(c?\"left\":\"top\",50*(1-a/b)+\"%\").css(c?\"top\":\"left\",\"\")},c.prototype.setContent=function(){var a=this.tip(),b=this.getTitle();a.find(\".tooltip-inner\")[this.options.html?\"html\":\"text\"](b),a.removeClass(\"fade in top bottom left right\")},c.prototype.hide=function(b){function d(){\"in\"!=e.hoverState&&f.detach(),e.$element&&e.$element.removeAttr(\"aria-describedby\").trigger(\"hidden.bs.\"+e.type),b&&b()}var e=this,f=a(this.$tip),g=a.Event(\"hide.bs.\"+this.type);if(this.$element.trigger(g),!g.isDefaultPrevented())return f.removeClass(\"in\"),a.support.transition&&f.hasClass(\"fade\")?f.one(\"bsTransitionEnd\",d).emulateTransitionEnd(c.TRANSITION_DURATION):d(),this.hoverState=null,this},c.prototype.fixTitle=function(){var a=this.$element;(a.attr(\"title\")||\"string\"!=typeof a.attr(\"data-original-title\"))&&a.attr(\"data-original-title\",a.attr(\"title\")||\"\").attr(\"title\",\"\")},c.prototype.hasContent=function(){return this.getTitle()},c.prototype.getPosition=function(b){b=b||this.$element;var c=b[0],d=\"BODY\"==c.tagName,e=c.getBoundingClientRect();null==e.width&&(e=a.extend({},e,{width:e.right-e.left,height:e.bottom-e.top}));var f=window.SVGElement&&c instanceof window.SVGElement,g=d?{top:0,left:0}:f?null:b.offset(),h={scroll:d?document.documentElement.scrollTop||document.body.scrollTop:b.scrollTop()},i=d?{width:a(window).width(),height:a(window).height()}:null;return a.extend({},e,h,i,g)},c.prototype.getCalculatedOffset=function(a,b,c,d){return\"bottom\"==a?{top:b.top+b.height,left:b.left+b.width/2-c/2}:\"top\"==a?{top:b.top-d,left:b.left+b.width/2-c/2}:\"left\"==a?{top:b.top+b.height/2-d/2,left:b.left-c}:{top:b.top+b.height/2-d/2,left:b.left+b.width}},c.prototype.getViewportAdjustedDelta=function(a,b,c,d){var e={top:0,left:0};if(!this.$viewport)return e;var f=this.options.viewport&&this.options.viewport.padding||0,g=this.getPosition(this.$viewport);if(/right|left/.test(a)){var h=b.top-f-g.scroll,i=b.top+f-g.scroll+d;h<g.top?e.top=g.top-h:i>g.top+g.height&&(e.top=g.top+g.height-i)}else{var j=b.left-f,k=b.left+f+c;j<g.left?e.left=g.left-j:k>g.right&&(e.left=g.left+g.width-k)}return e},c.prototype.getTitle=function(){var a,b=this.$element,c=this.options;return a=b.attr(\"data-original-title\")||(\"function\"==typeof c.title?c.title.call(b[0]):c.title)},c.prototype.getUID=function(a){do a+=~~(1e6*Math.random());while(document.getElementById(a));return a},c.prototype.tip=function(){if(!this.$tip&&(this.$tip=a(this.options.template),1!=this.$tip.length))throw new Error(this.type+\" `template` option must consist of exactly 1 top-level element!\");return this.$tip},c.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(\".tooltip-arrow\")},c.prototype.enable=function(){this.enabled=!0},c.prototype.disable=function(){this.enabled=!1},c.prototype.toggleEnabled=function(){this.enabled=!this.enabled},c.prototype.toggle=function(b){var c=this;b&&(c=a(b.currentTarget).data(\"bs.\"+this.type),c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data(\"bs.\"+this.type,c))),b?(c.inState.click=!c.inState.click,c.isInStateTrue()?c.enter(c):c.leave(c)):c.tip().hasClass(\"in\")?c.leave(c):c.enter(c)},c.prototype.destroy=function(){var a=this;clearTimeout(this.timeout),this.hide(function(){a.$element.off(\".\"+a.type).removeData(\"bs.\"+a.type),a.$tip&&a.$tip.detach(),a.$tip=null,a.$arrow=null,a.$viewport=null,a.$element=null})};var d=a.fn.tooltip;a.fn.tooltip=b,a.fn.tooltip.Constructor=c,a.fn.tooltip.noConflict=function(){return a.fn.tooltip=d,this}}(jQuery),+function(a){\"use strict\";function b(b){return this.each(function(){var d=a(this),e=d.data(\"bs.popover\"),f=\"object\"==typeof b&&b;!e&&/destroy|hide/.test(b)||(e||d.data(\"bs.popover\",e=new c(this,f)),\"string\"==typeof b&&e[b]())})}var c=function(a,b){this.init(\"popover\",a,b)};if(!a.fn.tooltip)throw new Error(\"Popover requires tooltip.js\");c.VERSION=\"3.3.7\",c.DEFAULTS=a.extend({},a.fn.tooltip.Constructor.DEFAULTS,{placement:\"right\",trigger:\"click\",content:\"\",template:'<div class=\"popover\" role=\"tooltip\"><div class=\"arrow\"></div><h3 class=\"popover-title\"></h3><div class=\"popover-content\"></div></div>'}),c.prototype=a.extend({},a.fn.tooltip.Constructor.prototype),c.prototype.constructor=c,c.prototype.getDefaults=function(){return c.DEFAULTS},c.prototype.setContent=function(){var a=this.tip(),b=this.getTitle(),c=this.getContent();a.find(\".popover-title\")[this.options.html?\"html\":\"text\"](b),a.find(\".popover-content\").children().detach().end()[this.options.html?\"string\"==typeof c?\"html\":\"append\":\"text\"](c),a.removeClass(\"fade top bottom left right in\"),a.find(\".popover-title\").html()||a.find(\".popover-title\").hide()},c.prototype.hasContent=function(){return this.getTitle()||this.getContent()},c.prototype.getContent=function(){var a=this.$element,b=this.options;return a.attr(\"data-content\")||(\"function\"==typeof b.content?b.content.call(a[0]):b.content)},c.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(\".arrow\")};var d=a.fn.popover;a.fn.popover=b,a.fn.popover.Constructor=c,a.fn.popover.noConflict=function(){return a.fn.popover=d,this}}(jQuery),+function(a){\"use strict\";function b(c,d){this.$body=a(document.body),this.$scrollElement=a(a(c).is(document.body)?window:c),this.options=a.extend({},b.DEFAULTS,d),this.selector=(this.options.target||\"\")+\" .nav li > a\",this.offsets=[],this.targets=[],this.activeTarget=null,this.scrollHeight=0,this.$scrollElement.on(\"scroll.bs.scrollspy\",a.proxy(this.process,this)),this.refresh(),this.process()}function c(c){return this.each(function(){var d=a(this),e=d.data(\"bs.scrollspy\"),f=\"object\"==typeof c&&c;e||d.data(\"bs.scrollspy\",e=new b(this,f)),\"string\"==typeof c&&e[c]()})}b.VERSION=\"3.3.7\",b.DEFAULTS={offset:10},b.prototype.getScrollHeight=function(){return this.$scrollElement[0].scrollHeight||Math.max(this.$body[0].scrollHeight,document.documentElement.scrollHeight)},b.prototype.refresh=function(){var b=this,c=\"offset\",d=0;this.offsets=[],this.targets=[],this.scrollHeight=this.getScrollHeight(),a.isWindow(this.$scrollElement[0])||(c=\"position\",d=this.$scrollElement.scrollTop()),this.$body.find(this.selector).map(function(){var b=a(this),e=b.data(\"target\")||b.attr(\"href\"),f=/^#./.test(e)&&a(e);return f&&f.length&&f.is(\":visible\")&&[[f[c]().top+d,e]]||null}).sort(function(a,b){return a[0]-b[0]}).each(function(){b.offsets.push(this[0]),b.targets.push(this[1])})},b.prototype.process=function(){var a,b=this.$scrollElement.scrollTop()+this.options.offset,c=this.getScrollHeight(),d=this.options.offset+c-this.$scrollElement.height(),e=this.offsets,f=this.targets,g=this.activeTarget;if(this.scrollHeight!=c&&this.refresh(),b>=d)return g!=(a=f[f.length-1])&&this.activate(a);if(g&&b<e[0])return this.activeTarget=null,this.clear();for(a=e.length;a--;)g!=f[a]&&b>=e[a]&&(void 0===e[a+1]||b<e[a+1])&&this.activate(f[a])},b.prototype.activate=function(b){\nthis.activeTarget=b,this.clear();var c=this.selector+'[data-target=\"'+b+'\"],'+this.selector+'[href=\"'+b+'\"]',d=a(c).parents(\"li\").addClass(\"active\");d.parent(\".dropdown-menu\").length&&(d=d.closest(\"li.dropdown\").addClass(\"active\")),d.trigger(\"activate.bs.scrollspy\")},b.prototype.clear=function(){a(this.selector).parentsUntil(this.options.target,\".active\").removeClass(\"active\")};var d=a.fn.scrollspy;a.fn.scrollspy=c,a.fn.scrollspy.Constructor=b,a.fn.scrollspy.noConflict=function(){return a.fn.scrollspy=d,this},a(window).on(\"load.bs.scrollspy.data-api\",function(){a('[data-spy=\"scroll\"]').each(function(){var b=a(this);c.call(b,b.data())})})}(jQuery),+function(a){\"use strict\";function b(b){return this.each(function(){var d=a(this),e=d.data(\"bs.tab\");e||d.data(\"bs.tab\",e=new c(this)),\"string\"==typeof b&&e[b]()})}var c=function(b){this.element=a(b)};c.VERSION=\"3.3.7\",c.TRANSITION_DURATION=150,c.prototype.show=function(){var b=this.element,c=b.closest(\"ul:not(.dropdown-menu)\"),d=b.data(\"target\");if(d||(d=b.attr(\"href\"),d=d&&d.replace(/.*(?=#[^\\s]*$)/,\"\")),!b.parent(\"li\").hasClass(\"active\")){var e=c.find(\".active:last a\"),f=a.Event(\"hide.bs.tab\",{relatedTarget:b[0]}),g=a.Event(\"show.bs.tab\",{relatedTarget:e[0]});if(e.trigger(f),b.trigger(g),!g.isDefaultPrevented()&&!f.isDefaultPrevented()){var h=a(d);this.activate(b.closest(\"li\"),c),this.activate(h,h.parent(),function(){e.trigger({type:\"hidden.bs.tab\",relatedTarget:b[0]}),b.trigger({type:\"shown.bs.tab\",relatedTarget:e[0]})})}}},c.prototype.activate=function(b,d,e){function f(){g.removeClass(\"active\").find(\"> .dropdown-menu > .active\").removeClass(\"active\").end().find('[data-toggle=\"tab\"]').attr(\"aria-expanded\",!1),b.addClass(\"active\").find('[data-toggle=\"tab\"]').attr(\"aria-expanded\",!0),h?(b[0].offsetWidth,b.addClass(\"in\")):b.removeClass(\"fade\"),b.parent(\".dropdown-menu\").length&&b.closest(\"li.dropdown\").addClass(\"active\").end().find('[data-toggle=\"tab\"]').attr(\"aria-expanded\",!0),e&&e()}var g=d.find(\"> .active\"),h=e&&a.support.transition&&(g.length&&g.hasClass(\"fade\")||!!d.find(\"> .fade\").length);g.length&&h?g.one(\"bsTransitionEnd\",f).emulateTransitionEnd(c.TRANSITION_DURATION):f(),g.removeClass(\"in\")};var d=a.fn.tab;a.fn.tab=b,a.fn.tab.Constructor=c,a.fn.tab.noConflict=function(){return a.fn.tab=d,this};var e=function(c){c.preventDefault(),b.call(a(this),\"show\")};a(document).on(\"click.bs.tab.data-api\",'[data-toggle=\"tab\"]',e).on(\"click.bs.tab.data-api\",'[data-toggle=\"pill\"]',e)}(jQuery),+function(a){\"use strict\";function b(b){return this.each(function(){var d=a(this),e=d.data(\"bs.affix\"),f=\"object\"==typeof b&&b;e||d.data(\"bs.affix\",e=new c(this,f)),\"string\"==typeof b&&e[b]()})}var c=function(b,d){this.options=a.extend({},c.DEFAULTS,d),this.$target=a(this.options.target).on(\"scroll.bs.affix.data-api\",a.proxy(this.checkPosition,this)).on(\"click.bs.affix.data-api\",a.proxy(this.checkPositionWithEventLoop,this)),this.$element=a(b),this.affixed=null,this.unpin=null,this.pinnedOffset=null,this.checkPosition()};c.VERSION=\"3.3.7\",c.RESET=\"affix affix-top affix-bottom\",c.DEFAULTS={offset:0,target:window},c.prototype.getState=function(a,b,c,d){var e=this.$target.scrollTop(),f=this.$element.offset(),g=this.$target.height();if(null!=c&&\"top\"==this.affixed)return e<c&&\"top\";if(\"bottom\"==this.affixed)return null!=c?!(e+this.unpin<=f.top)&&\"bottom\":!(e+g<=a-d)&&\"bottom\";var h=null==this.affixed,i=h?e:f.top,j=h?g:b;return null!=c&&e<=c?\"top\":null!=d&&i+j>=a-d&&\"bottom\"},c.prototype.getPinnedOffset=function(){if(this.pinnedOffset)return this.pinnedOffset;this.$element.removeClass(c.RESET).addClass(\"affix\");var a=this.$target.scrollTop(),b=this.$element.offset();return this.pinnedOffset=b.top-a},c.prototype.checkPositionWithEventLoop=function(){setTimeout(a.proxy(this.checkPosition,this),1)},c.prototype.checkPosition=function(){if(this.$element.is(\":visible\")){var b=this.$element.height(),d=this.options.offset,e=d.top,f=d.bottom,g=Math.max(a(document).height(),a(document.body).height());\"object\"!=typeof d&&(f=e=d),\"function\"==typeof e&&(e=d.top(this.$element)),\"function\"==typeof f&&(f=d.bottom(this.$element));var h=this.getState(g,b,e,f);if(this.affixed!=h){null!=this.unpin&&this.$element.css(\"top\",\"\");var i=\"affix\"+(h?\"-\"+h:\"\"),j=a.Event(i+\".bs.affix\");if(this.$element.trigger(j),j.isDefaultPrevented())return;this.affixed=h,this.unpin=\"bottom\"==h?this.getPinnedOffset():null,this.$element.removeClass(c.RESET).addClass(i).trigger(i.replace(\"affix\",\"affixed\")+\".bs.affix\")}\"bottom\"==h&&this.$element.offset({top:g-b-f})}};var d=a.fn.affix;a.fn.affix=b,a.fn.affix.Constructor=c,a.fn.affix.noConflict=function(){return a.fn.affix=d,this},a(window).on(\"load\",function(){a('[data-spy=\"affix\"]').each(function(){var c=a(this),d=c.data();d.offset=d.offset||{},null!=d.offsetBottom&&(d.offset.bottom=d.offsetBottom),null!=d.offsetTop&&(d.offset.top=d.offsetTop),b.call(c,d)})})}(jQuery);\n\n/* Anchor JS */\n/**\n * AnchorJS - v4.1.1 - 2018-07-01\n * https://github.com/bryanbraun/anchorjs\n * Copyright (c) 2018 Bryan Braun; Licensed MIT\n */\n!function(A,e){\"use strict\";\"function\"==typeof define&&define.amd?define([],e):\"object\"==typeof module&&module.exports?module.exports=e():(A.AnchorJS=e(),A.anchors=new A.AnchorJS)}(this,function(){\"use strict\";return function(A){function d(A){A.icon=A.hasOwnProperty(\"icon\")?A.icon:\"\",A.visible=A.hasOwnProperty(\"visible\")?A.visible:\"hover\",A.placement=A.hasOwnProperty(\"placement\")?A.placement:\"right\",A.ariaLabel=A.hasOwnProperty(\"ariaLabel\")?A.ariaLabel:\"Anchor\",A.class=A.hasOwnProperty(\"class\")?A.class:\"\",A.truncate=A.hasOwnProperty(\"truncate\")?Math.floor(A.truncate):64}function f(A){var e;if(\"string\"==typeof A||A instanceof String)e=[].slice.call(document.querySelectorAll(A));else{if(!(Array.isArray(A)||A instanceof NodeList))throw new Error(\"The selector provided to AnchorJS was invalid.\");e=[].slice.call(A)}return e}this.options=A||{},this.elements=[],d(this.options),this.isTouchDevice=function(){return!!(\"ontouchstart\"in window||window.DocumentTouch&&document instanceof DocumentTouch)},this.add=function(A){var e,t,i,n,o,s,r,a,c,h,l,u=[];if(d(this.options),\"touch\"===(l=this.options.visible)&&(l=this.isTouchDevice()?\"always\":\"hover\"),A||(A=\"h2, h3, h4, h5, h6\"),0===(e=f(A)).length)return this;for(function(){if(null===document.head.querySelector(\"style.anchorjs\")){var A,e=document.createElement(\"style\");e.className=\"anchorjs\",e.appendChild(document.createTextNode(\"\")),void 0===(A=document.head.querySelector('[rel=\"stylesheet\"], style'))?document.head.appendChild(e):document.head.insertBefore(e,A),e.sheet.insertRule(\" .anchorjs-link {   opacity: 0;   text-decoration: none;   -webkit-font-smoothing: antialiased;   -moz-osx-font-smoothing: grayscale; }\",e.sheet.cssRules.length),e.sheet.insertRule(\" *:hover > .anchorjs-link, .anchorjs-link:focus  {   opacity: 1; }\",e.sheet.cssRules.length),e.sheet.insertRule(\" [data-anchorjs-icon]::after {   content: attr(data-anchorjs-icon); }\",e.sheet.cssRules.length),e.sheet.insertRule(' @font-face {   font-family: \"anchorjs-icons\";   src: url(data:n/a;base64,AAEAAAALAIAAAwAwT1MvMg8yG2cAAAE4AAAAYGNtYXDp3gC3AAABpAAAAExnYXNwAAAAEAAAA9wAAAAIZ2x5ZlQCcfwAAAH4AAABCGhlYWQHFvHyAAAAvAAAADZoaGVhBnACFwAAAPQAAAAkaG10eASAADEAAAGYAAAADGxvY2EACACEAAAB8AAAAAhtYXhwAAYAVwAAARgAAAAgbmFtZQGOH9cAAAMAAAAAunBvc3QAAwAAAAADvAAAACAAAQAAAAEAAHzE2p9fDzz1AAkEAAAAAADRecUWAAAAANQA6R8AAAAAAoACwAAAAAgAAgAAAAAAAAABAAADwP/AAAACgAAA/9MCrQABAAAAAAAAAAAAAAAAAAAAAwABAAAAAwBVAAIAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAMCQAGQAAUAAAKZAswAAACPApkCzAAAAesAMwEJAAAAAAAAAAAAAAAAAAAAARAAAAAAAAAAAAAAAAAAAAAAQAAg//0DwP/AAEADwABAAAAAAQAAAAAAAAAAAAAAIAAAAAAAAAIAAAACgAAxAAAAAwAAAAMAAAAcAAEAAwAAABwAAwABAAAAHAAEADAAAAAIAAgAAgAAACDpy//9//8AAAAg6cv//f///+EWNwADAAEAAAAAAAAAAAAAAAAACACEAAEAAAAAAAAAAAAAAAAxAAACAAQARAKAAsAAKwBUAAABIiYnJjQ3NzY2MzIWFxYUBwcGIicmNDc3NjQnJiYjIgYHBwYUFxYUBwYGIwciJicmNDc3NjIXFhQHBwYUFxYWMzI2Nzc2NCcmNDc2MhcWFAcHBgYjARQGDAUtLXoWOR8fORYtLTgKGwoKCjgaGg0gEhIgDXoaGgkJBQwHdR85Fi0tOAobCgoKOBoaDSASEiANehoaCQkKGwotLXoWOR8BMwUFLYEuehYXFxYugC44CQkKGwo4GkoaDQ0NDXoaShoKGwoFBe8XFi6ALjgJCQobCjgaShoNDQ0NehpKGgobCgoKLYEuehYXAAAADACWAAEAAAAAAAEACAAAAAEAAAAAAAIAAwAIAAEAAAAAAAMACAAAAAEAAAAAAAQACAAAAAEAAAAAAAUAAQALAAEAAAAAAAYACAAAAAMAAQQJAAEAEAAMAAMAAQQJAAIABgAcAAMAAQQJAAMAEAAMAAMAAQQJAAQAEAAMAAMAAQQJAAUAAgAiAAMAAQQJAAYAEAAMYW5jaG9yanM0MDBAAGEAbgBjAGgAbwByAGoAcwA0ADAAMABAAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAH//wAP) format(\"truetype\"); }',e.sheet.cssRules.length)}}(),t=document.querySelectorAll(\"[id]\"),i=[].map.call(t,function(A){return A.id}),o=0;o<e.length;o++)if(this.hasAnchorJSLink(e[o]))u.push(o);else{if(e[o].hasAttribute(\"id\"))n=e[o].getAttribute(\"id\");else if(e[o].hasAttribute(\"data-anchor-id\"))n=e[o].getAttribute(\"data-anchor-id\");else{for(c=a=this.urlify(e[o].textContent),r=0;void 0!==s&&(c=a+\"-\"+r),r+=1,-1!==(s=i.indexOf(c)););s=void 0,i.push(c),e[o].setAttribute(\"id\",c),n=c}n.replace(/-/g,\" \"),(h=document.createElement(\"a\")).className=\"anchorjs-link \"+this.options.class,h.href=\"#\"+n,h.setAttribute(\"aria-label\",this.options.ariaLabel),h.setAttribute(\"data-anchorjs-icon\",this.options.icon),\"always\"===l&&(h.style.opacity=\"1\"),\"\"===this.options.icon&&(h.style.font=\"1em/1 anchorjs-icons\",\"left\"===this.options.placement&&(h.style.lineHeight=\"inherit\")),\"left\"===this.options.placement?(h.style.position=\"absolute\",h.style.marginLeft=\"-1em\",h.style.paddingRight=\"0.5em\",e[o].insertBefore(h,e[o].firstChild)):(h.style.paddingLeft=\"0.375em\",e[o].appendChild(h))}for(o=0;o<u.length;o++)e.splice(u[o]-o,1);return this.elements=this.elements.concat(e),this},this.remove=function(A){for(var e,t,i=f(A),n=0;n<i.length;n++)(t=i[n].querySelector(\".anchorjs-link\"))&&(-1!==(e=this.elements.indexOf(i[n]))&&this.elements.splice(e,1),i[n].removeChild(t));return this},this.removeAll=function(){this.remove(this.elements)},this.urlify=function(A){return this.options.truncate||d(this.options),A.trim().replace(/\\'/gi,\"\").replace(/[& +$,:;=?@\"#{}|^~[`%!'<>\\]\\.\\/\\(\\)\\*\\\\\\n\\t\\b\\v]/g,\"-\").replace(/-{2,}/g,\"-\").substring(0,this.options.truncate).replace(/^-+|-+$/gm,\"\").toLowerCase()},this.hasAnchorJSLink=function(A){var e=A.firstChild&&-1<(\" \"+A.firstChild.className+\" \").indexOf(\" anchorjs-link \"),t=A.lastChild&&-1<(\" \"+A.lastChild.className+\" \").indexOf(\" anchorjs-link \");return e||t||!1}}});\n\n/*!\n * jQuery throttle / debounce - v1.1 - 3/7/2010\n * http://benalman.com/projects/jquery-throttle-debounce-plugin/\n *\n * Copyright (c) 2010 \"Cowboy\" Ben Alman\n * Dual licensed under the MIT and GPL licenses.\n * http://benalman.com/about/license/\n */\n!function(t,n){var o,u=t.jQuery||t.Cowboy||(t.Cowboy={});u.throttle=o=function(t,o,e,i){var r,a=0;function c(){var u=this,c=+new Date-a,f=arguments;function d(){a=+new Date,e.apply(u,f)}i&&!r&&d(),r&&clearTimeout(r),i===n&&c>t?d():!0!==o&&(r=setTimeout(i?function(){r=n}:d,i===n?t-c:t))}return\"boolean\"!=typeof o&&(i=e,e=o,o=n),u.guid&&(c.guid=e.guid=e.guid||u.guid++),c},u.debounce=function(t,u,e){return e===n?o(t,u,!1):o(t,e,!1!==u)}}(this);\n\n/* Smooth scrolling for anchor links */\n$(\"a[href^='#']:not(.no-scroll)\").on('click', function(e) {\n\n  e.preventDefault();\n\n  var hash = this.hash;\n  var el = $(hash);\n  if (el.length === 0) {\n    return true;\n  }\n\n  $('html, body').animate({\n    scrollTop: el.offset().top\n  }, 500, function(){\n    window.location.hash = hash;\n  });\n});\n\n/* This adds hover anchor links to all headers */\n$(function() {\n  anchors.options = {\n    placement: 'left'\n  };\n  anchors.add().remove('.no-anchor');\n});\n\n// Use regular expression to find the cookie we made. Inspired from https://stackoverflow.com/a/25490531\nconst getCookiebyName = function (name){\n  const pair = document.cookie.match(new RegExp(name + '=([^;]+)'));\n  return !!(pair && pair.length >= 2) ? pair[1] : null;\n};\n\n// Sets a cookie that expires after a year.\nconst setCookie = function (name, value, days) {\n  let expires = '';\n  if (days) {\n    const date = new Date();\n    date.setTime(date.getTime() + (days*24*60*60*1000));\n    expires = '; expires=' + date.toUTCString();\n  }\n  document.cookie = name + '=' + (value || '') + expires + '; path=/';\n};\n"
  },
  {
    "path": "docs/assets/js/prism.js",
    "content": "/* PrismJS 1.17.1\nhttps://prismjs.com/download#themes=prism-solarizedlight&languages=markup+css+clike+javascript+bash+ruby+docker+go+hcl+java+json+python+yaml */\nvar _self=\"undefined\"!=typeof window?window:\"undefined\"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?self:{},Prism=function(u){var c=/\\blang(?:uage)?-([\\w-]+)\\b/i,n=0,C={manual:u.Prism&&u.Prism.manual,disableWorkerMessageHandler:u.Prism&&u.Prism.disableWorkerMessageHandler,util:{encode:function(e){return e instanceof _?new _(e.type,C.util.encode(e.content),e.alias):Array.isArray(e)?e.map(C.util.encode):e.replace(/&/g,\"&amp;\").replace(/</g,\"&lt;\").replace(/\\u00a0/g,\" \")},type:function(e){return Object.prototype.toString.call(e).slice(8,-1)},objId:function(e){return e.__id||Object.defineProperty(e,\"__id\",{value:++n}),e.__id},clone:function r(e,t){var a,n,i=C.util.type(e);switch(t=t||{},i){case\"Object\":if(n=C.util.objId(e),t[n])return t[n];for(var o in a={},t[n]=a,e)e.hasOwnProperty(o)&&(a[o]=r(e[o],t));return a;case\"Array\":return n=C.util.objId(e),t[n]?t[n]:(a=[],t[n]=a,e.forEach(function(e,n){a[n]=r(e,t)}),a);default:return e}},getLanguage:function(e){for(;e&&!c.test(e.className);)e=e.parentElement;return e?(e.className.match(c)||[,\"none\"])[1].toLowerCase():\"none\"},currentScript:function(){if(\"undefined\"==typeof document)return null;if(\"currentScript\"in document)return document.currentScript;try{throw new Error}catch(e){var n=(/at [^(\\r\\n]*\\((.*):.+:.+\\)$/i.exec(e.stack)||[])[1];if(n){var r=document.getElementsByTagName(\"script\");for(var t in r)if(r[t].src==n)return r[t]}return null}}},languages:{extend:function(e,n){var r=C.util.clone(C.languages[e]);for(var t in n)r[t]=n[t];return r},insertBefore:function(r,e,n,t){var a=(t=t||C.languages)[r],i={};for(var o in a)if(a.hasOwnProperty(o)){if(o==e)for(var l in n)n.hasOwnProperty(l)&&(i[l]=n[l]);n.hasOwnProperty(o)||(i[o]=a[o])}var s=t[r];return t[r]=i,C.languages.DFS(C.languages,function(e,n){n===s&&e!=r&&(this[e]=i)}),i},DFS:function e(n,r,t,a){a=a||{};var i=C.util.objId;for(var o in n)if(n.hasOwnProperty(o)){r.call(n,o,n[o],t||o);var l=n[o],s=C.util.type(l);\"Object\"!==s||a[i(l)]?\"Array\"!==s||a[i(l)]||(a[i(l)]=!0,e(l,r,o,a)):(a[i(l)]=!0,e(l,r,null,a))}}},plugins:{},highlightAll:function(e,n){C.highlightAllUnder(document,e,n)},highlightAllUnder:function(e,n,r){var t={callback:r,container:e,selector:'code[class*=\"language-\"], [class*=\"language-\"] code, code[class*=\"lang-\"], [class*=\"lang-\"] code'};C.hooks.run(\"before-highlightall\",t),t.elements=Array.prototype.slice.apply(t.container.querySelectorAll(t.selector)),C.hooks.run(\"before-all-elements-highlight\",t);for(var a,i=0;a=t.elements[i++];)C.highlightElement(a,!0===n,t.callback)},highlightElement:function(e,n,r){var t=C.util.getLanguage(e),a=C.languages[t];e.className=e.className.replace(c,\"\").replace(/\\s+/g,\" \")+\" language-\"+t;var i=e.parentNode;i&&\"pre\"===i.nodeName.toLowerCase()&&(i.className=i.className.replace(c,\"\").replace(/\\s+/g,\" \")+\" language-\"+t);var o={element:e,language:t,grammar:a,code:e.textContent};function l(e){o.highlightedCode=e,C.hooks.run(\"before-insert\",o),o.element.innerHTML=o.highlightedCode,C.hooks.run(\"after-highlight\",o),C.hooks.run(\"complete\",o),r&&r.call(o.element)}if(C.hooks.run(\"before-sanity-check\",o),!o.code)return C.hooks.run(\"complete\",o),void(r&&r.call(o.element));if(C.hooks.run(\"before-highlight\",o),o.grammar)if(n&&u.Worker){var s=new Worker(C.filename);s.onmessage=function(e){l(e.data)},s.postMessage(JSON.stringify({language:o.language,code:o.code,immediateClose:!0}))}else l(C.highlight(o.code,o.grammar,o.language));else l(C.util.encode(o.code))},highlight:function(e,n,r){var t={code:e,grammar:n,language:r};return C.hooks.run(\"before-tokenize\",t),t.tokens=C.tokenize(t.code,t.grammar),C.hooks.run(\"after-tokenize\",t),_.stringify(C.util.encode(t.tokens),t.language)},matchGrammar:function(e,n,r,t,a,i,o){for(var l in r)if(r.hasOwnProperty(l)&&r[l]){var s=r[l];s=Array.isArray(s)?s:[s];for(var u=0;u<s.length;++u){if(o&&o==l+\",\"+u)return;var c=s[u],g=c.inside,f=!!c.lookbehind,h=!!c.greedy,d=0,m=c.alias;if(h&&!c.pattern.global){var p=c.pattern.toString().match(/[imsuy]*$/)[0];c.pattern=RegExp(c.pattern.source,p+\"g\")}c=c.pattern||c;for(var y=t,v=a;y<n.length;v+=n[y].length,++y){var k=n[y];if(n.length>e.length)return;if(!(k instanceof _)){if(h&&y!=n.length-1){if(c.lastIndex=v,!(O=c.exec(e)))break;for(var b=O.index+(f&&O[1]?O[1].length:0),w=O.index+O[0].length,A=y,P=v,x=n.length;A<x&&(P<w||!n[A].type&&!n[A-1].greedy);++A)(P+=n[A].length)<=b&&(++y,v=P);if(n[y]instanceof _)continue;S=A-y,k=e.slice(v,P),O.index-=v}else{c.lastIndex=0;var O=c.exec(k),S=1}if(O){f&&(d=O[1]?O[1].length:0);w=(b=O.index+d)+(O=O[0].slice(d)).length;var j=k.slice(0,b),N=k.slice(w),E=[y,S];j&&(++y,v+=j.length,E.push(j));var L=new _(l,g?C.tokenize(O,g):O,m,O,h);if(E.push(L),N&&E.push(N),Array.prototype.splice.apply(n,E),1!=S&&C.matchGrammar(e,n,r,y,v,!0,l+\",\"+u),i)break}else if(i)break}}}}},tokenize:function(e,n){var r=[e],t=n.rest;if(t){for(var a in t)n[a]=t[a];delete n.rest}return C.matchGrammar(e,r,n,0,0,!1),r},hooks:{all:{},add:function(e,n){var r=C.hooks.all;r[e]=r[e]||[],r[e].push(n)},run:function(e,n){var r=C.hooks.all[e];if(r&&r.length)for(var t,a=0;t=r[a++];)t(n)}},Token:_};function _(e,n,r,t,a){this.type=e,this.content=n,this.alias=r,this.length=0|(t||\"\").length,this.greedy=!!a}if(u.Prism=C,_.stringify=function(e,n){if(\"string\"==typeof e)return e;if(Array.isArray(e))return e.map(function(e){return _.stringify(e,n)}).join(\"\");var r={type:e.type,content:_.stringify(e.content,n),tag:\"span\",classes:[\"token\",e.type],attributes:{},language:n};if(e.alias){var t=Array.isArray(e.alias)?e.alias:[e.alias];Array.prototype.push.apply(r.classes,t)}C.hooks.run(\"wrap\",r);var a=Object.keys(r.attributes).map(function(e){return e+'=\"'+(r.attributes[e]||\"\").replace(/\"/g,\"&quot;\")+'\"'}).join(\" \");return\"<\"+r.tag+' class=\"'+r.classes.join(\" \")+'\"'+(a?\" \"+a:\"\")+\">\"+r.content+\"</\"+r.tag+\">\"},!u.document)return u.addEventListener&&(C.disableWorkerMessageHandler||u.addEventListener(\"message\",function(e){var n=JSON.parse(e.data),r=n.language,t=n.code,a=n.immediateClose;u.postMessage(C.highlight(t,C.languages[r],r)),a&&u.close()},!1)),C;var e=C.util.currentScript();if(e&&(C.filename=e.src,e.hasAttribute(\"data-manual\")&&(C.manual=!0)),!C.manual){function r(){C.manual||C.highlightAll()}var t=document.readyState;\"loading\"===t||\"interactive\"===t&&e&&e.defer?document.addEventListener(\"DOMContentLoaded\",r):window.requestAnimationFrame?window.requestAnimationFrame(r):window.setTimeout(r,16)}return C}(_self);\"undefined\"!=typeof module&&module.exports&&(module.exports=Prism),\"undefined\"!=typeof global&&(global.Prism=Prism);\nPrism.languages.markup={comment:/<!--[\\s\\S]*?-->/,prolog:/<\\?[\\s\\S]+?\\?>/,doctype:{pattern:/<!DOCTYPE(?:[^>\"'[\\]]|\"[^\"]*\"|'[^']*')+(?:\\[(?:(?!<!--)[^\"'\\]]|\"[^\"]*\"|'[^']*'|<!--[\\s\\S]*?-->)*\\]\\s*)?>/i,greedy:!0},cdata:/<!\\[CDATA\\[[\\s\\S]*?]]>/i,tag:{pattern:/<\\/?(?!\\d)[^\\s>\\/=$<%]+(?:\\s(?:\\s*[^\\s>\\/=]+(?:\\s*=\\s*(?:\"[^\"]*\"|'[^']*'|[^\\s'\">=]+(?=[\\s>]))|(?=[\\s/>])))+)?\\s*\\/?>/i,greedy:!0,inside:{tag:{pattern:/^<\\/?[^\\s>\\/]+/i,inside:{punctuation:/^<\\/?/,namespace:/^[^\\s>\\/:]+:/}},\"attr-value\":{pattern:/=\\s*(?:\"[^\"]*\"|'[^']*'|[^\\s'\">=]+)/i,inside:{punctuation:[/^=/,{pattern:/^(\\s*)[\"']|[\"']$/,lookbehind:!0}]}},punctuation:/\\/?>/,\"attr-name\":{pattern:/[^\\s>\\/]+/,inside:{namespace:/^[^\\s>\\/:]+:/}}}},entity:/&#?[\\da-z]{1,8};/i},Prism.languages.markup.tag.inside[\"attr-value\"].inside.entity=Prism.languages.markup.entity,Prism.hooks.add(\"wrap\",function(a){\"entity\"===a.type&&(a.attributes.title=a.content.replace(/&amp;/,\"&\"))}),Object.defineProperty(Prism.languages.markup.tag,\"addInlined\",{value:function(a,e){var s={};s[\"language-\"+e]={pattern:/(^<!\\[CDATA\\[)[\\s\\S]+?(?=\\]\\]>$)/i,lookbehind:!0,inside:Prism.languages[e]},s.cdata=/^<!\\[CDATA\\[|\\]\\]>$/i;var n={\"included-cdata\":{pattern:/<!\\[CDATA\\[[\\s\\S]*?\\]\\]>/i,inside:s}};n[\"language-\"+e]={pattern:/[\\s\\S]+/,inside:Prism.languages[e]};var t={};t[a]={pattern:RegExp(\"(<__[\\\\s\\\\S]*?>)(?:<!\\\\[CDATA\\\\[[\\\\s\\\\S]*?\\\\]\\\\]>\\\\s*|[\\\\s\\\\S])*?(?=<\\\\/__>)\".replace(/__/g,a),\"i\"),lookbehind:!0,greedy:!0,inside:n},Prism.languages.insertBefore(\"markup\",\"cdata\",t)}}),Prism.languages.xml=Prism.languages.extend(\"markup\",{}),Prism.languages.html=Prism.languages.markup,Prism.languages.mathml=Prism.languages.markup,Prism.languages.svg=Prism.languages.markup;\n!function(s){var t=/(\"|')(?:\\\\(?:\\r\\n|[\\s\\S])|(?!\\1)[^\\\\\\r\\n])*\\1/;s.languages.css={comment:/\\/\\*[\\s\\S]*?\\*\\//,atrule:{pattern:/@[\\w-]+[\\s\\S]*?(?:;|(?=\\s*\\{))/,inside:{rule:/@[\\w-]+/}},url:{pattern:RegExp(\"url\\\\((?:\"+t.source+\"|[^\\n\\r()]*)\\\\)\",\"i\"),inside:{function:/^url/i,punctuation:/^\\(|\\)$/}},selector:RegExp(\"[^{}\\\\s](?:[^{};\\\"']|\"+t.source+\")*?(?=\\\\s*\\\\{)\"),string:{pattern:t,greedy:!0},property:/[-_a-z\\xA0-\\uFFFF][-\\w\\xA0-\\uFFFF]*(?=\\s*:)/i,important:/!important\\b/i,function:/[-a-z0-9]+(?=\\()/i,punctuation:/[(){};:,]/},s.languages.css.atrule.inside.rest=s.languages.css;var e=s.languages.markup;e&&(e.tag.addInlined(\"style\",\"css\"),s.languages.insertBefore(\"inside\",\"attr-value\",{\"style-attr\":{pattern:/\\s*style=(\"|')(?:\\\\[\\s\\S]|(?!\\1)[^\\\\])*\\1/i,inside:{\"attr-name\":{pattern:/^\\s*style/i,inside:e.tag.inside},punctuation:/^\\s*=\\s*['\"]|['\"]\\s*$/,\"attr-value\":{pattern:/.+/i,inside:s.languages.css}},alias:\"language-css\"}},e.tag))}(Prism);\nPrism.languages.clike={comment:[{pattern:/(^|[^\\\\])\\/\\*[\\s\\S]*?(?:\\*\\/|$)/,lookbehind:!0},{pattern:/(^|[^\\\\:])\\/\\/.*/,lookbehind:!0,greedy:!0}],string:{pattern:/([\"'])(?:\\\\(?:\\r\\n|[\\s\\S])|(?!\\1)[^\\\\\\r\\n])*\\1/,greedy:!0},\"class-name\":{pattern:/(\\b(?:class|interface|extends|implements|trait|instanceof|new)\\s+|\\bcatch\\s+\\()[\\w.\\\\]+/i,lookbehind:!0,inside:{punctuation:/[.\\\\]/}},keyword:/\\b(?:if|else|while|do|for|return|in|instanceof|function|new|try|throw|catch|finally|null|break|continue)\\b/,boolean:/\\b(?:true|false)\\b/,function:/\\w+(?=\\()/,number:/\\b0x[\\da-f]+\\b|(?:\\b\\d+\\.?\\d*|\\B\\.\\d+)(?:e[+-]?\\d+)?/i,operator:/[<>]=?|[!=]=?=?|--?|\\+\\+?|&&?|\\|\\|?|[?*/~^%]/,punctuation:/[{}[\\];(),.:]/};\nPrism.languages.javascript=Prism.languages.extend(\"clike\",{\"class-name\":[Prism.languages.clike[\"class-name\"],{pattern:/(^|[^$\\w\\xA0-\\uFFFF])[_$A-Z\\xA0-\\uFFFF][$\\w\\xA0-\\uFFFF]*(?=\\.(?:prototype|constructor))/,lookbehind:!0}],keyword:[{pattern:/((?:^|})\\s*)(?:catch|finally)\\b/,lookbehind:!0},{pattern:/(^|[^.])\\b(?:as|async(?=\\s*(?:function\\b|\\(|[$\\w\\xA0-\\uFFFF]|$))|await|break|case|class|const|continue|debugger|default|delete|do|else|enum|export|extends|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)\\b/,lookbehind:!0}],number:/\\b(?:(?:0[xX](?:[\\dA-Fa-f](?:_[\\dA-Fa-f])?)+|0[bB](?:[01](?:_[01])?)+|0[oO](?:[0-7](?:_[0-7])?)+)n?|(?:\\d(?:_\\d)?)+n|NaN|Infinity)\\b|(?:\\b(?:\\d(?:_\\d)?)+\\.?(?:\\d(?:_\\d)?)*|\\B\\.(?:\\d(?:_\\d)?)+)(?:[Ee][+-]?(?:\\d(?:_\\d)?)+)?/,function:/#?[_$a-zA-Z\\xA0-\\uFFFF][$\\w\\xA0-\\uFFFF]*(?=\\s*(?:\\.\\s*(?:apply|bind|call)\\s*)?\\()/,operator:/--|\\+\\+|\\*\\*=?|=>|&&|\\|\\||[!=]==|<<=?|>>>?=?|[-+*/%&|^!=<>]=?|\\.{3}|\\?[.?]?|[~:]/}),Prism.languages.javascript[\"class-name\"][0].pattern=/(\\b(?:class|interface|extends|implements|instanceof|new)\\s+)[\\w.\\\\]+/,Prism.languages.insertBefore(\"javascript\",\"keyword\",{regex:{pattern:/((?:^|[^$\\w\\xA0-\\uFFFF.\"'\\])\\s])\\s*)\\/(?:\\[(?:[^\\]\\\\\\r\\n]|\\\\.)*]|\\\\.|[^/\\\\\\[\\r\\n])+\\/[gimyus]{0,6}(?=\\s*(?:$|[\\r\\n,.;})\\]]))/,lookbehind:!0,greedy:!0},\"function-variable\":{pattern:/#?[_$a-zA-Z\\xA0-\\uFFFF][$\\w\\xA0-\\uFFFF]*(?=\\s*[=:]\\s*(?:async\\s*)?(?:\\bfunction\\b|(?:\\((?:[^()]|\\([^()]*\\))*\\)|[_$a-zA-Z\\xA0-\\uFFFF][$\\w\\xA0-\\uFFFF]*)\\s*=>))/,alias:\"function\"},parameter:[{pattern:/(function(?:\\s+[_$A-Za-z\\xA0-\\uFFFF][$\\w\\xA0-\\uFFFF]*)?\\s*\\(\\s*)(?!\\s)(?:[^()]|\\([^()]*\\))+?(?=\\s*\\))/,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/[_$a-z\\xA0-\\uFFFF][$\\w\\xA0-\\uFFFF]*(?=\\s*=>)/i,inside:Prism.languages.javascript},{pattern:/(\\(\\s*)(?!\\s)(?:[^()]|\\([^()]*\\))+?(?=\\s*\\)\\s*=>)/,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/((?:\\b|\\s|^)(?!(?:as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)(?![$\\w\\xA0-\\uFFFF]))(?:[_$A-Za-z\\xA0-\\uFFFF][$\\w\\xA0-\\uFFFF]*\\s*)\\(\\s*)(?!\\s)(?:[^()]|\\([^()]*\\))+?(?=\\s*\\)\\s*\\{)/,lookbehind:!0,inside:Prism.languages.javascript}],constant:/\\b[A-Z](?:[A-Z_]|\\dx?)*\\b/}),Prism.languages.insertBefore(\"javascript\",\"string\",{\"template-string\":{pattern:/`(?:\\\\[\\s\\S]|\\${(?:[^{}]|{(?:[^{}]|{[^}]*})*})+}|(?!\\${)[^\\\\`])*`/,greedy:!0,inside:{\"template-punctuation\":{pattern:/^`|`$/,alias:\"string\"},interpolation:{pattern:/((?:^|[^\\\\])(?:\\\\{2})*)\\${(?:[^{}]|{(?:[^{}]|{[^}]*})*})+}/,lookbehind:!0,inside:{\"interpolation-punctuation\":{pattern:/^\\${|}$/,alias:\"punctuation\"},rest:Prism.languages.javascript}},string:/[\\s\\S]+/}}}),Prism.languages.markup&&Prism.languages.markup.tag.addInlined(\"script\",\"javascript\"),Prism.languages.js=Prism.languages.javascript;\n!function(e){var t=\"\\\\b(?:BASH|BASHOPTS|BASH_ALIASES|BASH_ARGC|BASH_ARGV|BASH_CMDS|BASH_COMPLETION_COMPAT_DIR|BASH_LINENO|BASH_REMATCH|BASH_SOURCE|BASH_VERSINFO|BASH_VERSION|COLORTERM|COLUMNS|COMP_WORDBREAKS|DBUS_SESSION_BUS_ADDRESS|DEFAULTS_PATH|DESKTOP_SESSION|DIRSTACK|DISPLAY|EUID|GDMSESSION|GDM_LANG|GNOME_KEYRING_CONTROL|GNOME_KEYRING_PID|GPG_AGENT_INFO|GROUPS|HISTCONTROL|HISTFILE|HISTFILESIZE|HISTSIZE|HOME|HOSTNAME|HOSTTYPE|IFS|INSTANCE|JOB|LANG|LANGUAGE|LC_ADDRESS|LC_ALL|LC_IDENTIFICATION|LC_MEASUREMENT|LC_MONETARY|LC_NAME|LC_NUMERIC|LC_PAPER|LC_TELEPHONE|LC_TIME|LESSCLOSE|LESSOPEN|LINES|LOGNAME|LS_COLORS|MACHTYPE|MAILCHECK|MANDATORY_PATH|NO_AT_BRIDGE|OLDPWD|OPTERR|OPTIND|ORBIT_SOCKETDIR|OSTYPE|PAPERSIZE|PATH|PIPESTATUS|PPID|PS1|PS2|PS3|PS4|PWD|RANDOM|REPLY|SECONDS|SELINUX_INIT|SESSION|SESSIONTYPE|SESSION_MANAGER|SHELL|SHELLOPTS|SHLVL|SSH_AUTH_SOCK|TERM|UID|UPSTART_EVENTS|UPSTART_INSTANCE|UPSTART_JOB|UPSTART_SESSION|USER|WINDOWID|XAUTHORITY|XDG_CONFIG_DIRS|XDG_CURRENT_DESKTOP|XDG_DATA_DIRS|XDG_GREETER_DATA_DIR|XDG_MENU_PREFIX|XDG_RUNTIME_DIR|XDG_SEAT|XDG_SEAT_PATH|XDG_SESSION_DESKTOP|XDG_SESSION_ID|XDG_SESSION_PATH|XDG_SESSION_TYPE|XDG_VTNR|XMODIFIERS)\\\\b\",n={environment:{pattern:RegExp(\"\\\\$\"+t),alias:\"constant\"},variable:[{pattern:/\\$?\\(\\([\\s\\S]+?\\)\\)/,greedy:!0,inside:{variable:[{pattern:/(^\\$\\(\\([\\s\\S]+)\\)\\)/,lookbehind:!0},/^\\$\\(\\(/],number:/\\b0x[\\dA-Fa-f]+\\b|(?:\\b\\d+\\.?\\d*|\\B\\.\\d+)(?:[Ee]-?\\d+)?/,operator:/--?|-=|\\+\\+?|\\+=|!=?|~|\\*\\*?|\\*=|\\/=?|%=?|<<=?|>>=?|<=?|>=?|==?|&&?|&=|\\^=?|\\|\\|?|\\|=|\\?|:/,punctuation:/\\(\\(?|\\)\\)?|,|;/}},{pattern:/\\$\\((?:\\([^)]+\\)|[^()])+\\)|`[^`]+`/,greedy:!0,inside:{variable:/^\\$\\(|^`|\\)$|`$/}},{pattern:/\\$\\{[^}]+\\}/,greedy:!0,inside:{operator:/:[-=?+]?|[!\\/]|##?|%%?|\\^\\^?|,,?/,punctuation:/[\\[\\]]/,environment:{pattern:RegExp(\"(\\\\{)\"+t),lookbehind:!0,alias:\"constant\"}}},/\\$(?:\\w+|[#?*!@$])/],entity:/\\\\(?:[abceEfnrtv\\\\\"]|O?[0-7]{1,3}|x[0-9a-fA-F]{1,2}|u[0-9a-fA-F]{4}|U[0-9a-fA-F]{8})/};e.languages.bash={shebang:{pattern:/^#!\\s*\\/.*/,alias:\"important\"},comment:{pattern:/(^|[^\"{\\\\$])#.*/,lookbehind:!0},\"function-name\":[{pattern:/(\\bfunction\\s+)\\w+(?=(?:\\s*\\(?:\\s*\\))?\\s*\\{)/,lookbehind:!0,alias:\"function\"},{pattern:/\\b\\w+(?=\\s*\\(\\s*\\)\\s*\\{)/,alias:\"function\"}],\"for-or-select\":{pattern:/(\\b(?:for|select)\\s+)\\w+(?=\\s+in\\s)/,alias:\"variable\",lookbehind:!0},\"assign-left\":{pattern:/(^|[\\s;|&]|[<>]\\()\\w+(?=\\+?=)/,inside:{environment:{pattern:RegExp(\"(^|[\\\\s;|&]|[<>]\\\\()\"+t),lookbehind:!0,alias:\"constant\"}},alias:\"variable\",lookbehind:!0},string:[{pattern:/((?:^|[^<])<<-?\\s*)(\\w+?)\\s*(?:\\r?\\n|\\r)(?:[\\s\\S])*?(?:\\r?\\n|\\r)\\2/,lookbehind:!0,greedy:!0,inside:n},{pattern:/((?:^|[^<])<<-?\\s*)([\"'])(\\w+)\\2\\s*(?:\\r?\\n|\\r)(?:[\\s\\S])*?(?:\\r?\\n|\\r)\\3/,lookbehind:!0,greedy:!0},{pattern:/([\"'])(?:\\\\[\\s\\S]|\\$\\([^)]+\\)|`[^`]+`|(?!\\1)[^\\\\])*\\1/,greedy:!0,inside:n}],environment:{pattern:RegExp(\"\\\\$?\"+t),alias:\"constant\"},variable:n.variable,function:{pattern:/(^|[\\s;|&]|[<>]\\()(?:add|apropos|apt|aptitude|apt-cache|apt-get|aspell|automysqlbackup|awk|basename|bash|bc|bconsole|bg|bzip2|cal|cat|cfdisk|chgrp|chkconfig|chmod|chown|chroot|cksum|clear|cmp|column|comm|cp|cron|crontab|csplit|curl|cut|date|dc|dd|ddrescue|debootstrap|df|diff|diff3|dig|dir|dircolors|dirname|dirs|dmesg|du|egrep|eject|env|ethtool|expand|expect|expr|fdformat|fdisk|fg|fgrep|file|find|fmt|fold|format|free|fsck|ftp|fuser|gawk|git|gparted|grep|groupadd|groupdel|groupmod|groups|grub-mkconfig|gzip|halt|head|hg|history|host|hostname|htop|iconv|id|ifconfig|ifdown|ifup|import|install|ip|jobs|join|kill|killall|less|link|ln|locate|logname|logrotate|look|lpc|lpr|lprint|lprintd|lprintq|lprm|ls|lsof|lynx|make|man|mc|mdadm|mkconfig|mkdir|mke2fs|mkfifo|mkfs|mkisofs|mknod|mkswap|mmv|more|most|mount|mtools|mtr|mutt|mv|nano|nc|netstat|nice|nl|nohup|notify-send|npm|nslookup|op|open|parted|passwd|paste|pathchk|ping|pkill|pnpm|popd|pr|printcap|printenv|ps|pushd|pv|quota|quotacheck|quotactl|ram|rar|rcp|reboot|remsync|rename|renice|rev|rm|rmdir|rpm|rsync|scp|screen|sdiff|sed|sendmail|seq|service|sftp|sh|shellcheck|shuf|shutdown|sleep|slocate|sort|split|ssh|stat|strace|su|sudo|sum|suspend|swapon|sync|tac|tail|tar|tee|time|timeout|top|touch|tr|traceroute|tsort|tty|umount|uname|unexpand|uniq|units|unrar|unshar|unzip|update-grub|uptime|useradd|userdel|usermod|users|uudecode|uuencode|v|vdir|vi|vim|virsh|vmstat|wait|watch|wc|wget|whereis|which|who|whoami|write|xargs|xdg-open|yarn|yes|zenity|zip|zsh|zypper)(?=$|[)\\s;|&])/,lookbehind:!0},keyword:{pattern:/(^|[\\s;|&]|[<>]\\()(?:if|then|else|elif|fi|for|while|in|case|esac|function|select|do|done|until)(?=$|[)\\s;|&])/,lookbehind:!0},builtin:{pattern:/(^|[\\s;|&]|[<>]\\()(?:\\.|:|break|cd|continue|eval|exec|exit|export|getopts|hash|pwd|readonly|return|shift|test|times|trap|umask|unset|alias|bind|builtin|caller|command|declare|echo|enable|help|let|local|logout|mapfile|printf|read|readarray|source|type|typeset|ulimit|unalias|set|shopt)(?=$|[)\\s;|&])/,lookbehind:!0,alias:\"class-name\"},boolean:{pattern:/(^|[\\s;|&]|[<>]\\()(?:true|false)(?=$|[)\\s;|&])/,lookbehind:!0},\"file-descriptor\":{pattern:/\\B&\\d\\b/,alias:\"important\"},operator:{pattern:/\\d?<>|>\\||\\+=|==?|!=?|=~|<<[<-]?|[&\\d]?>>|\\d?[<>]&?|&[>&]?|\\|[&|]?|<=?|>=?/,inside:{\"file-descriptor\":{pattern:/^\\d/,alias:\"important\"}}},punctuation:/\\$?\\(\\(?|\\)\\)?|\\.\\.|[{}[\\];\\\\]/,number:{pattern:/(^|\\s)(?:[1-9]\\d*|0)(?:[.,]\\d+)?\\b/,lookbehind:!0}};for(var a=[\"comment\",\"function-name\",\"for-or-select\",\"assign-left\",\"string\",\"environment\",\"function\",\"keyword\",\"builtin\",\"boolean\",\"file-descriptor\",\"operator\",\"punctuation\",\"number\"],r=n.variable[1].inside,s=0;s<a.length;s++)r[a[s]]=e.languages.bash[a[s]];e.languages.shell=e.languages.bash}(Prism);\n!function(e){e.languages.ruby=e.languages.extend(\"clike\",{comment:[/#.*/,{pattern:/^=begin\\s[\\s\\S]*?^=end/m,greedy:!0}],keyword:/\\b(?:alias|and|BEGIN|begin|break|case|class|def|define_method|defined|do|each|else|elsif|END|end|ensure|extend|for|if|in|include|module|new|next|nil|not|or|prepend|protected|private|public|raise|redo|require|rescue|retry|return|self|super|then|throw|undef|unless|until|when|while|yield)\\b/});var n={pattern:/#\\{[^}]+\\}/,inside:{delimiter:{pattern:/^#\\{|\\}$/,alias:\"tag\"},rest:e.languages.ruby}};delete e.languages.ruby.function,e.languages.insertBefore(\"ruby\",\"keyword\",{regex:[{pattern:/%r([^a-zA-Z0-9\\s{(\\[<])(?:(?!\\1)[^\\\\]|\\\\[\\s\\S])*\\1[gim]{0,3}/,greedy:!0,inside:{interpolation:n}},{pattern:/%r\\((?:[^()\\\\]|\\\\[\\s\\S])*\\)[gim]{0,3}/,greedy:!0,inside:{interpolation:n}},{pattern:/%r\\{(?:[^#{}\\\\]|#(?:\\{[^}]+\\})?|\\\\[\\s\\S])*\\}[gim]{0,3}/,greedy:!0,inside:{interpolation:n}},{pattern:/%r\\[(?:[^\\[\\]\\\\]|\\\\[\\s\\S])*\\][gim]{0,3}/,greedy:!0,inside:{interpolation:n}},{pattern:/%r<(?:[^<>\\\\]|\\\\[\\s\\S])*>[gim]{0,3}/,greedy:!0,inside:{interpolation:n}},{pattern:/(^|[^/])\\/(?!\\/)(?:\\[.+?]|\\\\.|[^/\\\\\\r\\n])+\\/[gim]{0,3}(?=\\s*(?:$|[\\r\\n,.;})]))/,lookbehind:!0,greedy:!0}],variable:/[@$]+[a-zA-Z_]\\w*(?:[?!]|\\b)/,symbol:{pattern:/(^|[^:]):[a-zA-Z_]\\w*(?:[?!]|\\b)/,lookbehind:!0},\"method-definition\":{pattern:/(\\bdef\\s+)[\\w.]+/,lookbehind:!0,inside:{function:/\\w+$/,rest:e.languages.ruby}}}),e.languages.insertBefore(\"ruby\",\"number\",{builtin:/\\b(?:Array|Bignum|Binding|Class|Continuation|Dir|Exception|FalseClass|File|Stat|Fixnum|Float|Hash|Integer|IO|MatchData|Method|Module|NilClass|Numeric|Object|Proc|Range|Regexp|String|Struct|TMS|Symbol|ThreadGroup|Thread|Time|TrueClass)\\b/,constant:/\\b[A-Z]\\w*(?:[?!]|\\b)/}),e.languages.ruby.string=[{pattern:/%[qQiIwWxs]?([^a-zA-Z0-9\\s{(\\[<])(?:(?!\\1)[^\\\\]|\\\\[\\s\\S])*\\1/,greedy:!0,inside:{interpolation:n}},{pattern:/%[qQiIwWxs]?\\((?:[^()\\\\]|\\\\[\\s\\S])*\\)/,greedy:!0,inside:{interpolation:n}},{pattern:/%[qQiIwWxs]?\\{(?:[^#{}\\\\]|#(?:\\{[^}]+\\})?|\\\\[\\s\\S])*\\}/,greedy:!0,inside:{interpolation:n}},{pattern:/%[qQiIwWxs]?\\[(?:[^\\[\\]\\\\]|\\\\[\\s\\S])*\\]/,greedy:!0,inside:{interpolation:n}},{pattern:/%[qQiIwWxs]?<(?:[^<>\\\\]|\\\\[\\s\\S])*>/,greedy:!0,inside:{interpolation:n}},{pattern:/(\"|')(?:#\\{[^}]+\\}|\\\\(?:\\r\\n|[\\s\\S])|(?!\\1)[^\\\\\\r\\n])*\\1/,greedy:!0,inside:{interpolation:n}}],e.languages.rb=e.languages.ruby}(Prism);\nPrism.languages.docker={keyword:{pattern:/(^\\s*)(?:ADD|ARG|CMD|COPY|ENTRYPOINT|ENV|EXPOSE|FROM|HEALTHCHECK|LABEL|MAINTAINER|ONBUILD|RUN|SHELL|STOPSIGNAL|USER|VOLUME|WORKDIR)(?=\\s)/im,lookbehind:!0},string:/(\"|')(?:(?!\\1)[^\\\\\\r\\n]|\\\\(?:\\r\\n|[\\s\\S]))*\\1/,comment:/#.*/,punctuation:/---|\\.\\.\\.|[:[\\]{}\\-,|>?]/},Prism.languages.dockerfile=Prism.languages.docker;\nPrism.languages.go=Prism.languages.extend(\"clike\",{keyword:/\\b(?:break|case|chan|const|continue|default|defer|else|fallthrough|for|func|go(?:to)?|if|import|interface|map|package|range|return|select|struct|switch|type|var)\\b/,builtin:/\\b(?:bool|byte|complex(?:64|128)|error|float(?:32|64)|rune|string|u?int(?:8|16|32|64)?|uintptr|append|cap|close|complex|copy|delete|imag|len|make|new|panic|print(?:ln)?|real|recover)\\b/,boolean:/\\b(?:_|iota|nil|true|false)\\b/,operator:/[*\\/%^!=]=?|\\+[=+]?|-[=-]?|\\|[=|]?|&(?:=|&|\\^=?)?|>(?:>=?|=)?|<(?:<=?|=|-)?|:=|\\.\\.\\./,number:/(?:\\b0x[a-f\\d]+|(?:\\b\\d+\\.?\\d*|\\B\\.\\d+)(?:e[-+]?\\d+)?)i?/i,string:{pattern:/([\"'`])(?:\\\\[\\s\\S]|(?!\\1)[^\\\\])*\\1/,greedy:!0}}),delete Prism.languages.go[\"class-name\"];\nPrism.languages.hcl={comment:/(?:\\/\\/|#).*|\\/\\*[\\s\\S]*?(?:\\*\\/|$)/,heredoc:{pattern:/<<-?(\\w+)[\\s\\S]*?^\\s*\\1/m,greedy:!0,alias:\"string\"},keyword:[{pattern:/(?:resource|data)\\s+(?:\"(?:\\\\[\\s\\S]|[^\\\\\"])*\")(?=\\s+\"[\\w-]+\"\\s+{)/i,inside:{type:{pattern:/(resource|data|\\s+)(?:\"(?:\\\\[\\s\\S]|[^\\\\\"])*\")/i,lookbehind:!0,alias:\"variable\"}}},{pattern:/(?:provider|provisioner|variable|output|module|backend)\\s+(?:[\\w-]+|\"(?:\\\\[\\s\\S]|[^\\\\\"])*\")\\s+(?={)/i,inside:{type:{pattern:/(provider|provisioner|variable|output|module|backend)\\s+(?:[\\w-]+|\"(?:\\\\[\\s\\S]|[^\\\\\"])*\")\\s+/i,lookbehind:!0,alias:\"variable\"}}},{pattern:/[\\w-]+(?=\\s+{)/}],property:[/[\\w-\\.]+(?=\\s*=(?!=))/,/\"(?:\\\\[\\s\\S]|[^\\\\\"])+\"(?=\\s*[:=])/],string:{pattern:/\"(?:[^\\\\$\"]|\\\\[\\s\\S]|\\$(?:(?=\")|\\$+|[^\"${])|\\$\\{(?:[^{}\"]|\"(?:[^\\\\\"]|\\\\[\\s\\S])*\")*\\})*\"/,greedy:!0,inside:{interpolation:{pattern:/(^|[^$])\\$\\{(?:[^{}\"]|\"(?:[^\\\\\"]|\\\\[\\s\\S])*\")*\\}/,lookbehind:!0,inside:{type:{pattern:/(\\b(?:terraform|var|self|count|module|path|data|local)\\b\\.)[\\w\\*]+/i,lookbehind:!0,alias:\"variable\"},keyword:/\\b(?:terraform|var|self|count|module|path|data|local)\\b/i,function:/\\w+(?=\\()/,string:{pattern:/\"(?:\\\\[\\s\\S]|[^\\\\\"])*\"/,greedy:!0},number:/\\b0x[\\da-f]+|\\d+\\.?\\d*(?:e[+-]?\\d+)?/i,punctuation:/[!\\$#%&'()*+,.\\/;<=>@\\[\\\\\\]^`{|}~?:]/}}}},number:/\\b0x[\\da-f]+|\\d+\\.?\\d*(?:e[+-]?\\d+)?/i,boolean:/\\b(?:true|false)\\b/i,punctuation:/[=\\[\\]{}]/};\n!function(e){var t=/\\b(?:abstract|assert|boolean|break|byte|case|catch|char|class|const|continue|default|do|double|else|enum|exports|extends|final|finally|float|for|goto|if|implements|import|instanceof|int|interface|long|module|native|new|null|open|opens|package|private|protected|provides|public|requires|return|short|static|strictfp|super|switch|synchronized|this|throw|throws|to|transient|transitive|try|uses|var|void|volatile|while|with|yield)\\b/,a=/\\b[A-Z](?:\\w*[a-z]\\w*)?\\b/;e.languages.java=e.languages.extend(\"clike\",{\"class-name\":[a,/\\b[A-Z]\\w*(?=\\s+\\w+\\s*[;,=())])/],keyword:t,function:[e.languages.clike.function,{pattern:/(\\:\\:)[a-z_]\\w*/,lookbehind:!0}],number:/\\b0b[01][01_]*L?\\b|\\b0x[\\da-f_]*\\.?[\\da-f_p+-]+\\b|(?:\\b\\d[\\d_]*\\.?[\\d_]*|\\B\\.\\d[\\d_]*)(?:e[+-]?\\d[\\d_]*)?[dfl]?/i,operator:{pattern:/(^|[^.])(?:<<=?|>>>?=?|->|--|\\+\\+|&&|\\|\\||::|[?:~]|[-+*/%&|^!=<>]=?)/m,lookbehind:!0}}),e.languages.insertBefore(\"java\",\"string\",{\"triple-quoted-string\":{pattern:/\"\"\"[ \\t]*[\\r\\n](?:(?:\"|\"\")?(?:\\\\.|[^\"\\\\]))*\"\"\"/,greedy:!0,alias:\"string\"}}),e.languages.insertBefore(\"java\",\"class-name\",{annotation:{alias:\"punctuation\",pattern:/(^|[^.])@\\w+/,lookbehind:!0},namespace:{pattern:/(\\b(?:exports|import(?:\\s+static)?|module|open|opens|package|provides|requires|to|transitive|uses|with)\\s+)[a-z]\\w*(?:\\.[a-z]\\w*)+/,lookbehind:!0,inside:{punctuation:/\\./}},generics:{pattern:/<(?:[\\w\\s,.&?]|<(?:[\\w\\s,.&?]|<(?:[\\w\\s,.&?]|<[\\w\\s,.&?]*>)*>)*>)*>/,inside:{\"class-name\":a,keyword:t,punctuation:/[<>(),.:]/,operator:/[?&|]/}}})}(Prism);\nPrism.languages.json={property:{pattern:/\"(?:\\\\.|[^\\\\\"\\r\\n])*\"(?=\\s*:)/,greedy:!0},string:{pattern:/\"(?:\\\\.|[^\\\\\"\\r\\n])*\"(?!\\s*:)/,greedy:!0},comment:/\\/\\/.*|\\/\\*[\\s\\S]*?(?:\\*\\/|$)/,number:/-?\\d+\\.?\\d*(?:e[+-]?\\d+)?/i,punctuation:/[{}[\\],]/,operator:/:/,boolean:/\\b(?:true|false)\\b/,null:{pattern:/\\bnull\\b/,alias:\"keyword\"}};\nPrism.languages.python={comment:{pattern:/(^|[^\\\\])#.*/,lookbehind:!0},\"string-interpolation\":{pattern:/(?:f|rf|fr)(?:(\"\"\"|''')[\\s\\S]+?\\1|(\"|')(?:\\\\.|(?!\\2)[^\\\\\\r\\n])*\\2)/i,greedy:!0,inside:{interpolation:{pattern:/((?:^|[^{])(?:{{)*){(?!{)(?:[^{}]|{(?!{)(?:[^{}]|{(?!{)(?:[^{}])+})+})+}/,lookbehind:!0,inside:{\"format-spec\":{pattern:/(:)[^:(){}]+(?=}$)/,lookbehind:!0},\"conversion-option\":{pattern:/![sra](?=[:}]$)/,alias:\"punctuation\"},rest:null}},string:/[\\s\\S]+/}},\"triple-quoted-string\":{pattern:/(?:[rub]|rb|br)?(\"\"\"|''')[\\s\\S]+?\\1/i,greedy:!0,alias:\"string\"},string:{pattern:/(?:[rub]|rb|br)?(\"|')(?:\\\\.|(?!\\1)[^\\\\\\r\\n])*\\1/i,greedy:!0},function:{pattern:/((?:^|\\s)def[ \\t]+)[a-zA-Z_]\\w*(?=\\s*\\()/g,lookbehind:!0},\"class-name\":{pattern:/(\\bclass\\s+)\\w+/i,lookbehind:!0},decorator:{pattern:/(^\\s*)@\\w+(?:\\.\\w+)*/im,lookbehind:!0,alias:[\"annotation\",\"punctuation\"],inside:{punctuation:/\\./}},keyword:/\\b(?:and|as|assert|async|await|break|class|continue|def|del|elif|else|except|exec|finally|for|from|global|if|import|in|is|lambda|nonlocal|not|or|pass|print|raise|return|try|while|with|yield)\\b/,builtin:/\\b(?:__import__|abs|all|any|apply|ascii|basestring|bin|bool|buffer|bytearray|bytes|callable|chr|classmethod|cmp|coerce|compile|complex|delattr|dict|dir|divmod|enumerate|eval|execfile|file|filter|float|format|frozenset|getattr|globals|hasattr|hash|help|hex|id|input|int|intern|isinstance|issubclass|iter|len|list|locals|long|map|max|memoryview|min|next|object|oct|open|ord|pow|property|range|raw_input|reduce|reload|repr|reversed|round|set|setattr|slice|sorted|staticmethod|str|sum|super|tuple|type|unichr|unicode|vars|xrange|zip)\\b/,boolean:/\\b(?:True|False|None)\\b/,number:/(?:\\b(?=\\d)|\\B(?=\\.))(?:0[bo])?(?:(?:\\d|0x[\\da-f])[\\da-f]*\\.?\\d*|\\.\\d+)(?:e[+-]?\\d+)?j?\\b/i,operator:/[-+%=]=?|!=|\\*\\*?=?|\\/\\/?=?|<[<=>]?|>[=>]?|[&|^~]/,punctuation:/[{}[\\];(),.:]/},Prism.languages.python[\"string-interpolation\"].inside.interpolation.inside.rest=Prism.languages.python,Prism.languages.py=Prism.languages.python;\nPrism.languages.yaml={scalar:{pattern:/([\\-:]\\s*(?:![^\\s]+)?[ \\t]*[|>])[ \\t]*(?:((?:\\r?\\n|\\r)[ \\t]+)[^\\r\\n]+(?:\\2[^\\r\\n]+)*)/,lookbehind:!0,alias:\"string\"},comment:/#.*/,key:{pattern:/(\\s*(?:^|[:\\-,[{\\r\\n?])[ \\t]*(?:![^\\s]+)?[ \\t]*)[^\\r\\n{[\\]},#\\s]+?(?=\\s*:\\s)/,lookbehind:!0,alias:\"atrule\"},directive:{pattern:/(^[ \\t]*)%.+/m,lookbehind:!0,alias:\"important\"},datetime:{pattern:/([:\\-,[{]\\s*(?:![^\\s]+)?[ \\t]*)(?:\\d{4}-\\d\\d?-\\d\\d?(?:[tT]|[ \\t]+)\\d\\d?:\\d{2}:\\d{2}(?:\\.\\d*)?[ \\t]*(?:Z|[-+]\\d\\d?(?::\\d{2})?)?|\\d{4}-\\d{2}-\\d{2}|\\d\\d?:\\d{2}(?::\\d{2}(?:\\.\\d*)?)?)(?=[ \\t]*(?:$|,|]|}))/m,lookbehind:!0,alias:\"number\"},boolean:{pattern:/([:\\-,[{]\\s*(?:![^\\s]+)?[ \\t]*)(?:true|false)[ \\t]*(?=$|,|]|})/im,lookbehind:!0,alias:\"important\"},null:{pattern:/([:\\-,[{]\\s*(?:![^\\s]+)?[ \\t]*)(?:null|~)[ \\t]*(?=$|,|]|})/im,lookbehind:!0,alias:\"important\"},string:{pattern:/([:\\-,[{]\\s*(?:![^\\s]+)?[ \\t]*)(\"|')(?:(?!\\2)[^\\\\\\r\\n]|\\\\.)*\\2(?=[ \\t]*(?:$|,|]|}|\\s*#))/m,lookbehind:!0,greedy:!0},number:{pattern:/([:\\-,[{]\\s*(?:![^\\s]+)?[ \\t]*)[+-]?(?:0x[\\da-f]+|0o[0-7]+|(?:\\d+\\.?\\d*|\\.?\\d+)(?:e[+-]?\\d+)?|\\.inf|\\.nan)[ \\t]*(?=$|,|]|})/im,lookbehind:!0},tag:/![^\\s]+/,important:/[&*][\\w]+/,punctuation:/---|[:[\\]{}\\-,|>?]|\\.\\.\\./},Prism.languages.yml=Prism.languages.yaml;\n"
  },
  {
    "path": "docs/assets/js/video-player.js",
    "content": "$(document).ready(function () {\n  $('.video-player').on('click', function() {\n    if ($(this).find('.frame').length > 0) {\n      $(this).addClass('played')\n      const video_url = $(this).data('video-url')\n      $(this).append('<iframe ' +\n        'width=\"' + $(this).width() + 'px\"' +\n        'height=\"' + $(this).height() + 'px\"' +\n        'allowfullscreen ' +\n        'src=\"'+ video_url +'\"></iframe>')\n      $(this).find('.frame').remove()\n      $(this).find('.btn-video').remove()\n    }\n  })\n})\n"
  },
  {
    "path": "docs/docker-compose.yml",
    "content": "version: '3'\n\nservices:\n  web:\n    build: .\n    volumes:\n      # Bind mount the working dir so the app reloads automatically every time you make a change\n      # Note that we use 'delegated' to improve perf on OSX: https://docs.docker.com/docker-for-mac/osxfs-caching/\n      - .:/src:delegated\n      # Bind a Docker-only volume to the generated _site folder to make it clear we don't need to sync that folder\n      # back to the host OS. That makes Jekyll in Docker work much faster.\n      - generated_site:/src/_site\n    ports:\n      - \"4000:4000\"\n      # Expose port for jekyll livereload\n      - \"35729:35729\"\n    environment:\n      - JEKYLL_ENV=development\n\nvolumes:\n  generated_site:\n"
  },
  {
    "path": "docs/jekyll-serve.sh",
    "content": "#!/bin/bash\n\nset -e\n\necho -e \"\\e[1;31mRun Jekyll serve to watch for changes\"\nbundle exec jekyll serve --no-watch --livereload --drafts --host 0.0.0.0\n"
  },
  {
    "path": "docs/scripts/convert_adoc_to_md.sh",
    "content": "# Required:\n#  - asciidoctor\n#  - pandoc\n#\n# Install Asciidoctor:\n# $ sudo apt-get install asciidoctor\n#\n# Install Pandoc\n# https://pandoc.org/installing.html\n#\n# 1. Create input.adoc file\n# 2. paste adoc-formatted content to input.adoc\n# 3. run script\n# 4. The output will be printed to the output.md file.\nasciidoctor -b docbook input.adoc && pandoc -f docbook -t gfm input.xml -o output.md --wrap=none --atx-headers\n"
  },
  {
    "path": "docs/scripts/convert_md_to_adoc.sh",
    "content": "# Create input.md file, paste markdown text, and run script. The output will be printed to the output.adoc file.\npandoc --from=gfm --to=asciidoc --wrap=none --atx-headers  input.md > output.adoc\n"
  },
  {
    "path": "examples/azure/README.md",
    "content": "# Terratest Configuration and Setup\n\nTerratest uses Go to make calls to Azure through the azure-sdk-for-go library and independently confirm the actual Azure resource property matches the expected state provided by Terraform output variables.\n\n- Instructions for running each Azure Terratest module are included in each Terraform example sub-folder:\n  - examples/azure/terraform-azure-\\*-example/README.md\n- Tests which assert against expected Terraform output values are located in the the respective go files of the folder:\n  - [test/azure/terraform-azure-\\*-example_test.go](../../test/azure)\n- Test APIs which provide the actual Azure resource property values via the azure-sdk-for-go are located in the folder:\n  - [modules/azure](../../modules/azure)\n\n## Go Dependencies\n\nInstall [Golang](https://golang.org/) and make sure this code is checked out into your `GOPATH`\n\nThese modules are currently using the latest version of Go and was tested with **go1.14.4**.\n\n## Azure-sdk-for-go version\n\nLet's make sure [go.mod](https://github.com/gruntwork-io/terratest/blob/main/go.mod) includes the appropriate [azure-sdk-for-go version](https://github.com/Azure/azure-sdk-for-go/releases/tag/v46.1.0):\n\n```go\nrequire (\n    ...\n    github.com/Azure/azure-sdk-for-go v46.1.0+incompatible\n    ...\n)\n```\n\nIf we make changes to either the **go.mod** or the **go test file**, we should make sure that the go build command works still.\n\n```powershell\ngo build terraform_azure_*_test.go\n```\n\n## Review Environment Variables\n\nAs part of configuring terraform for Azure, we'll want to check that we have set the appropriate [credentials](https://docs.microsoft.com/azure/terraform/terraform-install-configure?toc=https%3A%2F%2Fdocs.microsoft.com%2Fen-us%2Fazure%2Fterraform%2Ftoc.json&bc=https%3A%2F%2Fdocs.microsoft.com%2Fen-us%2Fazure%2Fbread%2Ftoc.json#set-up-terraform-access-to-azure) and also that we set the [environment variables](https://docs.microsoft.com/azure/terraform/terraform-install-configure?toc=https%3A%2F%2Fdocs.microsoft.com%2Fen-us%2Fazure%2Fterraform%2Ftoc.json&bc=https%3A%2F%2Fdocs.microsoft.com%2Fen-us%2Fazure%2Fbread%2Ftoc.json#configure-terraform-environment-variables) on the testing host.\n\nFor non-commercial cloud deployments, set the \"AZURE_ENVIRONMENT\" environment variable to the appropriate cloud environment (this is used by the [client_factory](../../modules/azure/client_factory.go) to set the correct azure endpoints):\n\n```bash\nexport ARM_CLIENT_ID=your_app_id\nexport ARM_CLIENT_SECRET=your_password\nexport ARM_SUBSCRIPTION_ID=your_subscription_id\nexport ARM_TENANT_ID=your_tenant_id\n\n# AZURE_ENVIRONMENT is the name of the Azure environment to use. Set to one of the following:\nexport AZURE_ENVIRONMENT=AzureUSGovernmentCloud\nexport AZURE_ENVIRONMENT=AzureChinaCloud\nexport AZURE_ENVIRONMENT=AzureGermanCloud\nexport AZURE_ENVIRONMENT=AzurePublicCloud\nexport AZURE_ENVIRONMENT=AzureStackCloud\n```\n\nNote, in a Windows environment, these should be set as **system environment variables**. We can use a PowerShell console with administrative rights to update these environment variables:\n\n```powershell\n[System.Environment]::SetEnvironmentVariable(\"ARM_CLIENT_ID\",$your_app_id,[System.EnvironmentVariableTarget]::Machine)\n[System.Environment]::SetEnvironmentVariable(\"ARM_CLIENT_SECRET\",$your_password,[System.EnvironmentVariableTarget]::Machine)\n[System.Environment]::SetEnvironmentVariable(\"ARM_SUBSCRIPTION_ID\",$your_subscription_id,[System.EnvironmentVariableTarget]::Machine)\n[System.Environment]::SetEnvironmentVariable(\"ARM_TENANT_ID\",$your_tenant_id,[System.EnvironmentVariableTarget]::Machine)\n[System.Environment]::SetEnvironmentVariable(\"AZURE_ENVIRONMENT\",$your_azure_env,[System.EnvironmentVariableTarget]::Machine)\n```\n"
  },
  {
    "path": "examples/azure/terraform-azure-aci-example/README.md",
    "content": "# Terraform Azure Example\n\nThis folder contains a Terraform module that deploys resources in [Azure](https://azure.microsoft.com/) to demonstrate\nhow you can use Terratest to write automated tests for your Azure Terraform code. This module deploys an [Azure Container Instance](https://azure.microsoft.com/en-us/services/container-instances/).\n\nCheck out [test/azure/terraform_azure_aci_example_test.go](/test/azure/terraform_azure_aci_example_test.go) to see how you can write\nautomated tests for this module.\n\n**WARNING**: This module and the automated tests for it deploy real resources into your Azure account which can cost you\nmoney. The resources are all part of the [Azure Free Account](https://azure.microsoft.com/en-us/free/), so if you haven't used that up,\nit should be free, but you are completely responsible for all Azure charges.\n\n## Running this module manually\n\n1. Sign up for [Azure](https://azure.microsoft.com/)\n1. Configure your Azure credentials using one of the [supported methods for Azure CLI\n   tools](https://docs.microsoft.com/cli/azure/azure-cli-configuration?view=azure-cli-latest)\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`\n1. Ensure [environment variables](../README.md#review-environment-variables) are available\n1. Run `terraform init`\n1. Run `terraform apply`\n1. When you're done, run `terraform destroy`\n\n## Running automated tests against this module\n\n1. Sign up for [Azure](https://azure.microsoft.com/)\n1. Configure your Azure credentials using one of the [supported methods for Azure CLI\n   tools](https://docs.microsoft.com/en-us/cli/azure/azure-cli-configuration?view=azure-cli-latest)\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`\n1. Configure your Terratest [Go test environment](../README.md) \n1. `cd test/azure`\n1. `go build terraform_azure_aci_example_test.go`\n1. `go test -v -timeout 60m -tags azure -run TestTerraformAzureACIExample`\n\n"
  },
  {
    "path": "examples/azure/terraform-azure-aci-example/main.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY AN AZURE CONTAINER Instance\n# This is an example of how to deploy an Azure Container Instance\n# See test/terraform_azure_aci_example_test.go for how to write automated tests for this code.\n# ---------------------------------------------------------------------------------------------------------------------\n\n# ------------------------------------------------------------------------------\n# CONFIGURE OUR AZURE CONNECTION\n# ------------------------------------------------------------------------------\n\nterraform {\n  required_providers {\n    azurerm = {\n      version = \"~>2.29.0\"\n      source  = \"hashicorp/azurerm\"\n    }\n  }\n}\n\nprovider \"azurerm\" {\n  features {}\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY A RESOURCE GROUP\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"azurerm_resource_group\" \"rg\" {\n  name     = \"terratest-aci-rg-${var.postfix}\"\n  location = var.location\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY AN AZURE CONTAINER INSTANCE\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"azurerm_container_group\" \"aci\" {\n  name                = \"aci${var.postfix}\"\n  location            = azurerm_resource_group.rg.location\n  resource_group_name = azurerm_resource_group.rg.name\n\n  ip_address_type = \"public\"\n  dns_name_label  = \"aci${var.postfix}\"\n  os_type         = \"Linux\"\n\n  container {\n    name   = \"hello-world\"\n    image  = \"mcr.microsoft.com/azuredocs/aci-helloworld:latest\"\n    cpu    = \"0.5\"\n    memory = \"1.5\"\n\n    ports {\n      port     = 443\n      protocol = \"TCP\"\n    }\n  }\n\n  tags = {\n    Environment = \"Development\"\n  }\n}\n"
  },
  {
    "path": "examples/azure/terraform-azure-aci-example/output.tf",
    "content": "output \"resource_group_name\" {\n  value = azurerm_resource_group.rg.name\n}\n\noutput \"ip_address\" {\n  value = azurerm_container_group.aci.ip_address\n}\n\noutput \"fqdn\" {\n  value = azurerm_container_group.aci.fqdn\n}\n\noutput \"container_instance_name\" {\n  value = azurerm_container_group.aci.name\n}\n"
  },
  {
    "path": "examples/azure/terraform-azure-aci-example/variables.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# ENVIRONMENT VARIABLES\n# Define these secrets as environment variables\n# ---------------------------------------------------------------------------------------------------------------------\n\n# ARM_CLIENT_ID\n# ARM_CLIENT_SECRET\n# ARM_SUBSCRIPTION_ID\n# ARM_TENANT_ID\n\n# ---------------------------------------------------------------------------------------------------------------------\n# REQUIRED PARAMETERS\n# You must provide a value for each of these parameters.\n# ---------------------------------------------------------------------------------------------------------------------\n\n# ---------------------------------------------------------------------------------------------------------------------\n# OPTIONAL PARAMETERS\n# These parameters have reasonable defaults.\n# ---------------------------------------------------------------------------------------------------------------------\n\nvariable \"location\" {\n  description = \"The supported azure location where the resource exists\"\n  type        = string\n  default     = \"West US2\"\n}\n\nvariable \"postfix\" {\n  description = \"A postfix string to centrally mitigate resource name collisions.\"\n  type        = string\n  default     = \"1276\"\n}\n"
  },
  {
    "path": "examples/azure/terraform-azure-acr-example/README.md",
    "content": "# Terraform Azure Container Registry Example\n\nThis folder contains a Terraform module that deploys resources in [Azure](https://azure.microsoft.com/) to demonstrate\nhow you can use Terratest to write automated tests for your Azure Terraform code. This module deploys an [Azure Container Registry](https://azure.microsoft.com/en-us/services/container-registry/).\n\nCheck out [test/azure/terraform_azure_acr_example_test.go](/test/azure/terraform_azure_acr_example_test.go) to see how you can write\nautomated tests for this module.\n\n**WARNING**: This module and the automated tests for it deploy real resources into your Azure account which can cost you\nmoney. The resources are all part of the [Azure Free Account](https://azure.microsoft.com/en-us/free/), so if you haven't used that up,\nit should be free, but you are completely responsible for all Azure charges.\n\n## Running this module manually\n\n1. Sign up for [Azure](https://azure.microsoft.com/)\n1. Configure your Azure credentials using one of the [supported methods for Azure CLI\n   tools](https://docs.microsoft.com/cli/azure/azure-cli-configuration?view=azure-cli-latest)\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`\n1. Ensure [environment variables](../README.md#review-environment-variables) are available\n1. Run `terraform init`\n1. Run `terraform apply`\n1. When you're done, run `terraform destroy`\n\n\n## Running automated tests against this module\n\n1. Sign up for [Azure](https://azure.microsoft.com/)\n1. Configure your Azure credentials using one of the [supported methods for Azure CLI\n   tools](https://docs.microsoft.com/en-us/cli/azure/azure-cli-configuration?view=azure-cli-latest)\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`\n1. Configure your Terratest [Go test environment](../README.md)\n1. `cd test/azure`\n1. `go build terraform_azure_acr_example_test.go`\n1. `go test -v -timeout 60m -tags azure -run TestTerraformAzureACRExample`\n"
  },
  {
    "path": "examples/azure/terraform-azure-acr-example/main.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY AN AZURE CONTAINER REGISTRY\n# This is an example of how to deploy an Azure Container Registry\n# See test/terraform_azure_acr_example_test.go for how to write automated tests for this code.\n# ---------------------------------------------------------------------------------------------------------------------\n\n# ------------------------------------------------------------------------------\n# CONFIGURE OUR AZURE CONNECTION\n# ------------------------------------------------------------------------------\n\nterraform {\n  required_providers {\n    azurerm = {\n      version = \"~>2.29.0\"\n      source  = \"hashicorp/azurerm\"\n    }\n  }\n}\n\nprovider \"azurerm\" {\n  features {}\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY A RESOURCE GROUP\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"azurerm_resource_group\" \"rg\" {\n  name     = \"terratest-acr-rg-${var.postfix}\"\n  location = var.location\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY AN AZURE CONTAINER REGISTRY\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"azurerm_container_registry\" \"acr\" {\n  name                = \"acr${var.postfix}\"\n  location            = azurerm_resource_group.rg.location\n  resource_group_name = azurerm_resource_group.rg.name\n\n  sku           = var.sku\n  admin_enabled = true\n\n  tags = {\n    Environment = \"Development\"\n  }\n}\n"
  },
  {
    "path": "examples/azure/terraform-azure-acr-example/output.tf",
    "content": "output \"resource_group_name\" {\n  value = azurerm_resource_group.rg.name\n}\n\noutput \"container_registry_name\" {\n  value = azurerm_container_registry.acr.name\n}\n\noutput \"login_server\" {\n  value = azurerm_container_registry.acr.login_server\n}\n\noutput \"admin_username\" {\n  value     = azurerm_container_registry.acr.admin_username\n  sensitive = true\n}\n\noutput \"admin_password\" {\n  value     = azurerm_container_registry.acr.admin_password\n  sensitive = true\n}\n"
  },
  {
    "path": "examples/azure/terraform-azure-acr-example/variables.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# ENVIRONMENT VARIABLES\n# Define these secrets as environment variables\n# ---------------------------------------------------------------------------------------------------------------------\n\n# ARM_CLIENT_ID\n# ARM_CLIENT_SECRET\n# ARM_SUBSCRIPTION_ID\n# ARM_TENANT_ID\n\n# ---------------------------------------------------------------------------------------------------------------------\n# REQUIRED PARAMETERS\n# You must provide a value for each of these parameters.\n# ---------------------------------------------------------------------------------------------------------------------\n\n# ---------------------------------------------------------------------------------------------------------------------\n# OPTIONAL PARAMETERS\n# These parameters have reasonable defaults.\n# ---------------------------------------------------------------------------------------------------------------------\n\nvariable \"sku\" {\n  description = \"SKU tier for the ACR.\"\n  default     = \"Premium\"\n}\n\n\nvariable \"location\" {\n  description = \"The supported azure location where the resource exists\"\n  type        = string\n  default     = \"West US2\"\n}\n\nvariable \"postfix\" {\n  description = \"A postfix string to centrally mitigate resource name collisions.\"\n  type        = string\n  default     = \"1276\"\n}\n"
  },
  {
    "path": "examples/azure/terraform-azure-actiongroup-example/README.md",
    "content": "# Terraform Azure Action Group Example\n\nThis folder contains a Terraform module that deploys an [Azure Action Group](https://docs.microsoft.com/en-us/azure/azure-monitor/platform/action-groups) in [Azure](https://azure.microsoft.com/) to demonstrate how you can use Terratest to write automated tests for your Azure Terraform code. \n\nCheck out [test/azure/terraform_azure_actiongroup_example_test.go](/test/azure/terraform/azure_actiongroup_example_test.go) to see how you can write automated tests for this module and validate the configuration of the parameters and options. \n\n**WARNING**: This module and the automated tests for it deploy real resources into your Azure account which can cost you money. \n\n## Prerequisite: Setup Azure CLI access\n1. Sign up for [Azure](https://azure.microsoft.com/).\n1. Install [Azure CLI](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli?view=azure-cli-latest)\n2. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`.\n3. Login to Azure on the CLI with `az login` or `az login --use-device`, and then configure the CLI.\n\n## Running this module manually\n1. Create [Service Principal](https://docs.microsoft.com/en-us/cli/azure/create-an-azure-service-principal-azure-cli?view=azure-cli-latest) then set the value to the environment variables. \n1. Run `terraform init`.\n2. Run `terraform apply`.\n3. Log into Azure to validate resource was created.\n4. When you're done, run `terraform destroy`.\n\n### Example\n\n```bash\n$ az login \n$ export ARM_SUBSCRIPTION_ID={YOUR_SUBSCRIPTION_ID} \n$ az ad sp create-for-rbac\n$ export TF_VAR_client_id={YOUR_SERVICE_PRINCIPAL_APP_ID}\n$ export TF_VAR_client_secret={YOUR_SERVICE_PRINCIPAL_PASSWORD}\n$ terraform init\n$ terraform apply\n$ terraform destroy\n```\n\n## Running automated tests against this module\n1. Create [Service Principal](https://docs.microsoft.com/en-us/cli/azure/create-an-azure-service-principal-azure-cli?view=azure-cli-latest) then set the value to the environment variables. \n1. Install [Golang](https://golang.org/) version `1.13+` required. \n1. `cd test/azure`\n1. `go test -v -timeout 60m -tags azure -run TestTerraformAzureActionGroupExample`\n\n\n### Example\n\n```bash\n$ az login \n$ export ARM_SUBSCRIPTION_ID={YOUR_SUBSCRIPTION_ID} \n$ export TF_VAR_client_id={YOUR_SERVICE_PRINCIPAL_APP_ID}\n$ export TF_VAR_client_secret={YOUR_SERVICE_PRINCIPAL_PASSWORD}\n$ cd test/azure\n$ go test -v -timeout 60m -tags azure -run TestTerraformAzureActionGroupExample\n```"
  },
  {
    "path": "examples/azure/terraform-azure-actiongroup-example/main.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY AN ACTION GROUP\n# This is an example of how to deploy an Azure Action Group to be used for Azure Alerts\n# ---------------------------------------------------------------------------------------------------------------------\n\n# ---------------------------------------------------------------------------------------------------------------------\n# CONFIGURE OUR AZURE CONNECTION\n# ---------------------------------------------------------------------------------------------------------------------\n\nprovider \"azurerm\" {\n  features {}\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY A RESOURCE GROUP\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"azurerm_resource_group\" \"rg\" {\n  name     = var.resource_group_name\n  location = var.location\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY AN AZURE APP SERVICE PLAN\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"azurerm_monitor_action_group\" \"actionGroup\" {\n  name                = var.app_name\n  resource_group_name = azurerm_resource_group.rg.name\n  short_name          = var.short_name\n  tags                = azurerm_resource_group.rg.tags\n\n  dynamic \"email_receiver\" {\n    for_each = var.enable_email ? [\"email_receiver\"] : []\n    content {\n      name                    = var.email_name\n      email_address           = var.email_address\n      use_common_alert_schema = true\n    }\n  }\n\n  dynamic \"sms_receiver\" {\n    for_each = var.enable_sms ? [\"sms_receiver\"] : []\n    content {\n      name         = var.sms_name\n      country_code = var.sms_country_code\n      phone_number = var.sms_phone_number\n    }\n  }\n\n  dynamic \"webhook_receiver\" {\n    for_each = var.enable_webhook ? [\"webhook_receiver\"] : []\n    content {\n      name                    = var.webhook_name\n      service_uri             = var.webhook_service_uri\n      use_common_alert_schema = true\n    }\n  }\n\n}"
  },
  {
    "path": "examples/azure/terraform-azure-actiongroup-example/output.tf",
    "content": "output \"action_group_id\" {\n  value = azurerm_monitor_action_group.actionGroup.id\n}"
  },
  {
    "path": "examples/azure/terraform-azure-actiongroup-example/variables.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# ENVIRONMENT VARIABLES\n# Define these secrets as environment variables\n# ---------------------------------------------------------------------------------------------------------------------\n\n# ---------------------------------------------------------------------------------------------------------------------\n# REQUIRED PARAMETERS\n# You must provide a value for each of these parameters.\n# ---------------------------------------------------------------------------------------------------------------------\n\nvariable \"resource_group_name\" {\n  description = \"Name of the resource group that exists in Azure\"\n  type        = string\n}\n\nvariable \"app_name\" {\n  description = \"The base name of the application used in the naming convention.\"\n  type        = string\n}\n\nvariable \"location\" {\n  description = \"Specifies the supported Azure location where the resource exists. Changing this forces a new resource to be created.\"\n  type        = string\n}\n\nvariable \"short_name\" {\n  description = \"Shorthand name for SMS texts.\"\n  type        = string\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# OPTIONAL PARAMETERS\n# These parameters have reasonable defaults.\n# ---------------------------------------------------------------------------------------------------------------------\n\nvariable \"enable_email\" {\n  description = \"Enable email alert capabilities\"\n  type        = bool\n  default     = false\n}\n\nvariable \"email_name\" {\n  description = \"Friendly Name for email address\"\n  type        = string\n  default     = \"\"\n}\n\nvariable \"email_address\" {\n  description = \"email address to send alerts to\"\n  type        = string\n  default     = \"\"\n}\n\nvariable \"enable_sms\" {\n  description = \"Enable Texting Alerts\"\n  type        = bool\n  default     = false\n}\n\nvariable \"sms_name\" {\n  description = \"Friendly Name for phone number\"\n  type        = string\n  default     = \"\"\n}\n\nvariable \"sms_country_code\" {\n  description = \"Country Code for phone number\"\n  type        = number\n  default     = 1\n}\n\nvariable \"sms_phone_number\" {\n  description = \"Phone number for text alerts\"\n  type        = number\n  default     = 0\n}\n\nvariable \"enable_webhook\" {\n  description = \"Enable Web Hook Alerts\"\n  type        = bool\n  default     = false\n}\n\nvariable \"webhook_name\" {\n  description = \"Friendly Name for web hook\"\n  type        = string\n  default     = \"\"\n}\n\nvariable \"webhook_service_uri\" {\n  description = \"The full URI for the webhook\"\n  type        = string\n  default     = \"\"\n}"
  },
  {
    "path": "examples/azure/terraform-azure-aks-example/README.md",
    "content": "# Terraform Azure AKS Example\n\nThis folder contains a Terraform module that deploys a basic AKS cluster in [Azure](https://azure.microsoft.com/) to demonstrate how you can use Terratest to write automated tests for your Azure Terraform code. \n\nThis module deploys [Azure Kubenetes Service](https://azure.microsoft.com/en-us/services/kubernetes-service/), then deploys nginx by a kubernetes yaml file with a Public IP Address using the `Service` resource.\n\nCheck out [test/azure/terraform_azure_aks_example_test.go](/test/azure/terraform_azure_aks_example_test.go) to see how you can write automated tests for this module and validate the configuration of the parameters and options. \n\n**WARNING**: This module and the automated tests for it deploy real resources into your Azure account which can cost you money. \n\n## Prerequisite: Setup Azure CLI access\n1. Sign up for [Azure](https://azure.microsoft.com/).\n1. Install [Azure CLI](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli?view=azure-cli-latest)\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`.\n1. Install [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/) and make sure it's on your `PATH`.\n1. Login to Azure on the CLI with `az login` or `az login --use-device`, and then configure the CLI.\n\n## Running this module manually\n1. Create [Service Principal](https://docs.microsoft.com/en-us/cli/azure/create-an-azure-service-principal-azure-cli?view=azure-cli-latest) then set the value to the environment variables. \n1. Run `terraform init`\n1. Run `terraform apply`\n1. Apply `nginx-deployment.yml`\n1. Watch the service until Public IPAddress is assigned.\n1. Send http request to the Public IPAddress, make sure it returns 200.\n1. When you're done, run `terraform destroy`.\n\n### Example\n\n```bash\n$ az login \n$ export ARM_SUBSCRIPTION_ID={YOUR_SUBSCRIPTION_ID} \n$ az ad sp create-for-rbac\n$ export TF_VAR_client_id={YOUR_SERVICE_PRINCIPAL_APP_ID}\n$ export TF_VAR_client_secret={YOUR_SERVICE_PRINCIPAL_PASSWORD}\n$ terraform init\n$ terraform apply\n$ kubectl --kubeconfig ./kubeconfig -f ./nginx-deployment.yml\n$ kubectl --kubeconfig ./kubeconfig get svc -w\n// Open browser and access the Nginx Service IPAddress\n$ terraform destroy\n```\n\n## Running automated tests against this module\n1. Create [Service Principal](https://docs.microsoft.com/en-us/cli/azure/create-an-azure-service-principal-azure-cli?view=azure-cli-latest) then set the value to the environment variables. \n1. Install [Golang](https://golang.org/) version `1.13+` required. \n1. `cd test`\n1. `go test -v -timeout 60m -tags azure -run TestTerraformAzureAKS`\n\n### Example\n\n```bash\n$ az login \n$ export ARM_SUBSCRIPTION_ID={YOUR_SUBSCRIPTION_ID} \n$ export TF_VAR_client_id={YOUR_SERVICE_PRINCIPAL_APP_ID}\n$ export TF_VAR_client_secret={YOUR_SERVICE_PRINCIPAL_PASSWORD}\n$ cd test\n$ go test -v -timeout 60m -tags azure -run TestTerraformAzureAKS\n```\n"
  },
  {
    "path": "examples/azure/terraform-azure-aks-example/main.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY AN AZURE AKS CLUSTER\n# This is an example of how to deploy an Azure AKS cluster with load balancer in front of the service \n# to handle providing the public interface into the cluster.\n# ---------------------------------------------------------------------------------------------------------------------\n# See test/azure/terraform_azure_aks_example_test.go for how to write automated tests for this code.\n# ---------------------------------------------------------------------------------------------------------------------\n\nterraform {\n  # This module is now only being tested with Terraform 0.13.x. However, to make upgrading easier, we are setting\n  # 0.12.26 as the minimum version, as that version added support for required_providers with source URLs, making it\n  # forwards compatible with 0.13.x code.\n  required_version = \">= 0.12.26\"\n}\n\n# ------------------------------------------------------------------------------\n# CONFIGURE OUR AZURE CONNECTION\n# ------------------------------------------------------------------------------\n\nprovider \"azurerm\" {\n  features {}\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY A RESOURCE GROUP\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"azurerm_resource_group\" \"k8s\" {\n  name     = var.resource_group_name\n  location = var.location\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY AN AZURE KUBERNETES CLUSTER\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"azurerm_kubernetes_cluster\" \"k8s\" {\n  name                = var.cluster_name\n  location            = azurerm_resource_group.k8s.location\n  resource_group_name = azurerm_resource_group.k8s.name\n  dns_prefix          = var.dns_prefix\n\n  linux_profile {\n    admin_username = \"ubuntu\"\n\n    ssh_key {\n      key_data = file(var.ssh_public_key)\n    }\n  }\n\n  default_node_pool {\n    name       = \"agentpool\"\n    node_count = var.agent_count\n    vm_size    = \"Standard_DS2_v2\"\n  }\n\n  service_principal {\n    client_id     = var.client_id\n    client_secret = var.client_secret\n  }\n  automatic_upgrade_channel = \"stable\"\n  tags = {\n    Environment = \"Development\"\n  }\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# CREATE KUBECONFIG FILE\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"local_file\" \"kubeconfig\" {\n  content  = azurerm_kubernetes_cluster.k8s.kube_config_raw\n  filename = \"kubeconfig\"\n\n  depends_on = [\n    azurerm_kubernetes_cluster.k8s\n  ]\n}\n"
  },
  {
    "path": "examples/azure/terraform-azure-aks-example/nginx-deployment.yml",
    "content": "---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: nginx-deployment\nspec:\n  selector:\n    matchLabels:\n      app: nginx\n  replicas: 1\n  template:\n    metadata:\n      labels:\n        app: nginx\n    spec:\n      containers:\n      - name: nginx\n        image: nginx:1.15.7\n        ports:\n        - containerPort: 80\n---\nkind: Service\napiVersion: v1\nmetadata:\n  name: nginx-service\nspec:\n  selector:\n    app: nginx\n  ports:\n  - protocol: TCP\n    targetPort: 80\n    port: 80\n  type: LoadBalancer\n"
  },
  {
    "path": "examples/azure/terraform-azure-aks-example/output.tf",
    "content": "output \"client_key\" {\n  value = azurerm_kubernetes_cluster.k8s.kube_config.0.client_key\n}\n\noutput \"client_certificate\" {\n  value     = azurerm_kubernetes_cluster.k8s.kube_config.0.client_certificate\n  sensitive = true\n}\n\noutput \"cluster_ca_certificate\" {\n  value     = azurerm_kubernetes_cluster.k8s.kube_config.0.cluster_ca_certificate\n  sensitive = true\n}\n\noutput \"cluster_username\" {\n  value     = azurerm_kubernetes_cluster.k8s.kube_config.0.username\n  sensitive = true\n}\n\noutput \"cluster_password\" {\n  value     = azurerm_kubernetes_cluster.k8s.kube_config.0.password\n  sensitive = true\n}\n\noutput \"kube_config\" {\n  value     = azurerm_kubernetes_cluster.k8s.kube_config_raw\n  sensitive = true\n}\n\noutput \"host\" {\n  value     = azurerm_kubernetes_cluster.k8s.kube_config.0.host\n  sensitive = true\n}"
  },
  {
    "path": "examples/azure/terraform-azure-aks-example/variables.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# REQUIRED PARAMETERS\n# You must provide a value for each of these parameters.\n# ---------------------------------------------------------------------------------------------------------------------\n\nvariable \"client_id\" {\n  description = \"The Service Principal Client Id for AKS to modify Azure resources.\"\n}\nvariable \"client_secret\" {\n  description = \"The Service Principal Client Password for AKS to modify Azure resources.\"\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# OPTIONAL PARAMETERS\n# These parameters have reasonable defaults.\n# ---------------------------------------------------------------------------------------------------------------------\n\nvariable \"agent_count\" {\n  description = \"The number of the nodes of the AKS cluster.\"\n  default     = 3\n}\n\nvariable \"ssh_public_key\" {\n  description = \"The public key for the ssh connection to the nodes.\"\n  default     = \"~/.ssh/id_rsa.pub\"\n}\n\nvariable \"dns_prefix\" {\n  description = \"The prefix to set for the AKS cluster resoureces names.\"\n  default     = \"k8stest\"\n}\n\nvariable \"cluster_name\" {\n  description = \"The name to set for the AKS cluster.\"\n  default     = \"k8stest\"\n}\n\nvariable \"resource_group_name\" {\n  description = \"The name to set for the resource group.\"\n  default     = \"azure-k8stest\"\n}\n\nvariable \"location\" {\n  description = \"The location to set for the AKS cluster.\"\n  default     = \"Central US\"\n}"
  },
  {
    "path": "examples/azure/terraform-azure-availabilityset-example/README.md",
    "content": "# Terraform Azure Availability Set Example\n\nThis folder contains a simple Terraform module that deploys resources in [Azure](https://azure.microsoft.com/) to demonstrate\nhow you can use Terratest to write automated tests for your Azure Terraform code. This module deploys an Availability Set with one attched Virtual Machine.\n\n- An [Availability Set](https://docs.microsoft.com/en-us/azure/virtual-machines/availability) that gives the module the following:\n  - `Availability Set` with the name specified in the `availability_set_name` output variable.\n  - `Fault Domain Count` with the value specified in the `availability_set_fdc` output variable.\n- A [Virtual Machine](https://azure.microsoft.com/en-us/services/virtual-machines/) that gives the Availability Set the following:\n  - [Virtual Machine](https://docs.microsoft.com/en-us/azure/virtual-machines/) with the name specified in the `vm_name` output variable.\n\nCheck out [test/azure/terraform_azure_availabilityset_example_test.go](/test/azure/terraform_azure_availabilityset_example_test.go) to see how you can write\nautomated tests for this module.\n\nNote that the Availability Set and VM in this module don't actually do anything; it just runs the resources for\ndemonstration purposes.\n\n**WARNING**: This module and the automated tests for it deploy real resources into your Azure account which can cost you\nmoney. The resources are all part of the [Azure Free Account](https://azure.microsoft.com/en-us/free/), so if you haven't used that up,\nit should be free, but you are completely responsible for all Azure charges.\n\n## Running this module manually\n\n1. Sign up for [Azure](https://azure.microsoft.com/)\n1. Configure your Azure credentials using one of the [supported methods for Azure CLI\n   tools](https://docs.microsoft.com/en-us/cli/azure/azure-cli-configuration?view=azure-cli-latest)\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`\n1. Ensure [environment variables](../README.md#review-environment-variables) are available\n1. Run `terraform init`\n1. Run `terraform apply`\n1. When you're done, run `terraform destroy`\n\n## Running automated tests against this module\n\n1. Sign up for [Azure](https://azure.microsoft.com/)\n1. Configure your Azure credentials using one of the [supported methods for Azure CLI\n   tools](https://docs.microsoft.com/en-us/cli/azure/azure-cli-configuration?view=azure-cli-latest)\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`\n1. Configure your Terratest [Go test environment](../README.md)\n1. `cd test/azure`\n1. `go build terraform_azure_availabilityset_example_test.go`\n1. `go test -v -run TestTerraformAzureAvailabilitySetExample`\n"
  },
  {
    "path": "examples/azure/terraform-azure-availabilityset-example/main.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY AN AZURE AVAILABILITY SET\n# This is an example of how to deploy an Azure Availability Set with a Virtual Machine in the availability set \n# and the minimum network resources for the VM.\n# ---------------------------------------------------------------------------------------------------------------------\n# See test/azure/terraform_azure_availabilityset_example_test.go for how to write automated tests for this code.\n# ---------------------------------------------------------------------------------------------------------------------\nprovider \"azurerm\" {\n  features {}\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# PIN TERRAFORM VERSION TO >= 0.12\n# The examples have been upgraded to 0.12 syntax\n# ---------------------------------------------------------------------------------------------------------------------\nterraform {\n  # This module is now only being tested with Terraform 0.13.x. However, to make upgrading easier, we are setting\n  # 0.12.26 as the minimum version, as that version added support for required_providers with source URLs, making it\n  # forwards compatible with 0.13.x code.\n  required_version = \">= 0.12.26\"\n  required_providers {\n    azurerm = {\n      version = \"~> 2.50\"\n      source  = \"hashicorp/azurerm\"\n    }\n  }\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY A RESOURCE GROUP\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"azurerm_resource_group\" \"avs\" {\n  name     = \"terratest-avs-rg-${var.postfix}\"\n  location = var.location\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY THE AVAILABILITY SET\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"azurerm_availability_set\" \"avs\" {\n  name                        = \"avs-${var.postfix}\"\n  location                    = azurerm_resource_group.avs.location\n  resource_group_name         = azurerm_resource_group.avs.name\n  platform_fault_domain_count = var.avs_fault_domain_count\n  managed                     = true\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY MINIMAL NETWORK RESOURCES FOR VM\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"azurerm_virtual_network\" \"avs\" {\n  name                = \"vnet-${var.postfix}\"\n  address_space       = [\"10.0.0.0/16\"]\n  location            = azurerm_resource_group.avs.location\n  resource_group_name = azurerm_resource_group.avs.name\n}\n\nresource \"azurerm_subnet\" \"avs\" {\n  name                 = \"subnet-${var.postfix}\"\n  resource_group_name  = azurerm_resource_group.avs.name\n  virtual_network_name = azurerm_virtual_network.avs.name\n  address_prefixes     = [\"10.0.17.0/24\"]\n}\n\nresource \"azurerm_network_interface\" \"avs\" {\n  name                = \"nic-${var.postfix}\"\n  location            = azurerm_resource_group.avs.location\n  resource_group_name = azurerm_resource_group.avs.name\n\n  ip_configuration {\n    name                          = \"config-${var.postfix}-01\"\n    subnet_id                     = azurerm_subnet.avs.id\n    private_ip_address_allocation = \"Dynamic\"\n  }\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY VIRTUAL MACHINE\n# This VM does not actually do anything and is the smallest size VM available with an Ubuntu image\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"azurerm_virtual_machine\" \"avs\" {\n  name                             = \"vm-${var.postfix}\"\n  location                         = azurerm_resource_group.avs.location\n  resource_group_name              = azurerm_resource_group.avs.name\n  network_interface_ids            = [azurerm_network_interface.avs.id]\n  availability_set_id              = azurerm_availability_set.avs.id\n  vm_size                          = \"Standard_B1ls\"\n  delete_os_disk_on_termination    = true\n  delete_data_disks_on_termination = true\n\n  storage_image_reference {\n    publisher = \"Canonical\"\n    offer     = \"UbuntuServer\"\n    sku       = \"18.04-LTS\"\n    version   = \"latest\"\n  }\n\n  storage_os_disk {\n    name              = \"osdisk-${var.postfix}\"\n    caching           = \"None\"\n    create_option     = \"FromImage\"\n    managed_disk_type = \"Standard_LRS\"\n  }\n\n  os_profile {\n    computer_name  = \"vm-${var.postfix}\"\n    admin_username = \"testadmin\"\n    admin_password = random_password.avs.result\n  }\n\n  os_profile_linux_config {\n    disable_password_authentication = false\n  }\n\n  depends_on = [random_password.avs]\n}\n\nresource \"random_password\" \"avs\" {\n  length           = 16\n  override_special = \"-_%@\"\n  min_upper        = \"1\"\n  min_lower        = \"1\"\n  min_numeric      = \"1\"\n  min_special      = \"1\"\n}\n"
  },
  {
    "path": "examples/azure/terraform-azure-availabilityset-example/outputs.tf",
    "content": "output \"resource_group_name\" {\n  value = azurerm_resource_group.avs.name\n}\n\noutput \"availability_set_name\" {\n  value = azurerm_availability_set.avs.name\n}\n\noutput \"availability_set_fdc\" {\n  value = azurerm_availability_set.avs.platform_fault_domain_count\n}\n\noutput \"vm_name\" {\n  value = azurerm_virtual_machine.avs.name\n}\n"
  },
  {
    "path": "examples/azure/terraform-azure-availabilityset-example/variables.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# ENVIRONMENT VARIABLES\n# Define these secrets as environment variables\n# ---------------------------------------------------------------------------------------------------------------------\n\n# ARM_CLIENT_ID\n# ARM_CLIENT_SECRET\n# ARM_SUBSCRIPTION_ID\n# ARM_TENANT_ID\n\n# ---------------------------------------------------------------------------------------------------------------------\n# REQUIRED PARAMETERS\n# You must provide a value for each of these parameters.\n# ---------------------------------------------------------------------------------------------------------------------\n\n# ---------------------------------------------------------------------------------------------------------------------\n# OPTIONAL PARAMETERS\n# These parameters have reasonable defaults.\n# ---------------------------------------------------------------------------------------------------------------------\n\nvariable \"avs_fault_domain_count\" {\n  description = \"Domain Fault Domain Count of the Availability Set\"\n  type        = number\n  default     = 3\n}\n\nvariable \"location\" {\n  description = \"The Azure location where to deploy your resources too\"\n  type        = string\n  default     = \"East US\"\n}\n\nvariable \"postfix\" {\n  description = \"A postfix string to centrally mitigate resource name collisions\"\n  type        = string\n  default     = \"resource\"\n}\n"
  },
  {
    "path": "examples/azure/terraform-azure-container-apps-example/README.md",
    "content": "# Terraform Azure Container Apps Example\n\nThis folder contains a simple Terraform module that deploys resources in [Azure](https://azure.microsoft.com/) to demonstrate\nhow you can use Terratest to write automated tests for your Azure Terraform code. This module deploys two [Azure Container Apps](https://learn.microsoft.com/en-us/azure/container-apps/overview) (one app and a job).\n\nCheck out [test/azure/terraform_azure_container_apps_example_test.go](./../../../test/azure/terraform_azure_container_apps_example_test.go) to see how you can write automated tests for this module and validate the configuration of the parameters and options.\n\n**WARNING**: This module and the automated tests for it deploy real resources into your Azure account which can cost you money.\n\n## Running this module manually\n\n1. Sign up for [Azure](https://azure.microsoft.com/).\n1. Configure your Azure credentials using one of the [supported methods for Azure CLI\n   tools](https://docs.microsoft.com/en-us/cli/azure/azure-cli-configuration?view=azure-cli-latest)\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`.\n1. Ensure [environment variables](../README.md#review-environment-variables) are available\n1. Run `terraform init`\n1. Run `terraform apply`\n1. When you're done, run `terraform destroy`.\n\n## Running automated tests against this module\n\n1. Sign up for [Azure](https://azure.microsoft.com/)\n1. Configure your Azure credentials using one of the [supported methods for Azure CLI\n   tools](https://docs.microsoft.com/en-us/cli/azure/azure-cli-configuration?view=azure-cli-latest)\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`\n1. Configure your Terratest [Go test environment](../README.md)\n1. `cd test/azure`\n1. `go build terraform_azure_container_apps_example_test.go`\n1. `go test -v -timeout 60m -tags azure -run TestTerraformAzureContainerAppExample`"
  },
  {
    "path": "examples/azure/terraform-azure-container-apps-example/main.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY AN AZURE CONTAINER APPS\n# This is an example of how to deploy an Azure Container App and Azure Container App Job with the minimum set of options.\n# ---------------------------------------------------------------------------------------------------------------------\n# See test/azure/terraform_azure_container_apps_test.go for how to write automated tests for this code.\n# ---------------------------------------------------------------------------------------------------------------------\n\nprovider \"azurerm\" {\n  features {}\n}\n\n\n# ---------------------------------------------------------------------------------------------------------------------\n# PIN TERRAFORM VERSION TO >= 0.12\n# The examples have been upgraded to 0.12 syntax\n# ---------------------------------------------------------------------------------------------------------------------\n\nterraform {\n  # This module is now only being tested with Terraform 0.13.x. However, to make upgrading easier, we are setting\n  # 0.12.26 as the minimum version, as that version added support for required_providers with source URLs, making it\n  # forwards compatible with 0.13.x code.\n  required_version = \">= 0.12.26\"\n  required_providers {\n    azurerm = {\n      version = \"~> 3.103\"\n      source  = \"hashicorp/azurerm\"\n    }\n  }\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY A RESOURCE GROUP\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"azurerm_resource_group\" \"aca\" {\n  name     = \"terratest-rg-${var.postfix}\"\n  location = \"East US\"\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY A AZURE APP ENVIRONMENT\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"azurerm_container_app_environment\" \"aca\" {\n  name                = \"terratest-aca-env-${var.postfix}\"\n  location            = azurerm_resource_group.aca.location\n  resource_group_name = azurerm_resource_group.aca.name\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY A AZURE CONTAINER APP\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"azurerm_container_app\" \"aca\" {\n  name                         = \"terratest-aca-${var.postfix}\"\n  resource_group_name          = azurerm_resource_group.aca.name\n  container_app_environment_id = azurerm_container_app_environment.aca.id\n  revision_mode                = \"Single\"\n  template {\n    container {\n      name   = \"terratest-aca-app-${var.postfix}\"\n      image  = \"mcr.microsoft.com/azuredocs/containerapps-helloworld:latest\"\n      cpu    = \"0.5\"\n      memory = \"1.0Gi\"\n    }\n  }\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY A AZURE CONTAINER APP JOB\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"azurerm_container_app_job\" \"aca\" {\n  name                         = \"terratest-aca-job-${var.postfix}\"\n  location                     = azurerm_resource_group.aca.location\n  resource_group_name          = azurerm_resource_group.aca.name\n  container_app_environment_id = azurerm_container_app_environment.aca.id\n  replica_timeout_in_seconds   = 10\n  template {\n    container {\n      name    = \"terratest-aca-job-${var.postfix}\"\n      image   = \"busybox:stable\"\n      command = [\"echo\", \"Hello, World!\"]\n      cpu     = \"0.5\"\n      memory  = \"1.0Gi\"\n    }\n  }\n  manual_trigger_config {\n    parallelism = 1\n  }\n}\n"
  },
  {
    "path": "examples/azure/terraform-azure-container-apps-example/outputs.tf",
    "content": "output \"resource_group_name\" {\n  value = azurerm_resource_group.aca.name\n}\n\noutput \"container_app_env_name\" {\n  value = azurerm_container_app_environment.aca.name\n}\n\noutput \"container_app_name\" {\n  value = azurerm_container_app.aca.name\n}\n\noutput \"container_app_job_name\" {\n  value = azurerm_container_app_job.aca.name\n}\n"
  },
  {
    "path": "examples/azure/terraform-azure-container-apps-example/variables.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# ENVIRONMENT VARIABLES\n# Define these secrets as environment variables\n# ---------------------------------------------------------------------------------------------------------------------\n\n# ARM_CLIENT_ID\n# ARM_CLIENT_SECRET\n# ARM_SUBSCRIPTION_ID\n# ARM_TENANT_ID\n\n# ---------------------------------------------------------------------------------------------------------------------\n# REQUIRED PARAMETERS\n# You must provide a value for each of these parameters.\n# ---------------------------------------------------------------------------------------------------------------------\n\n# ---------------------------------------------------------------------------------------------------------------------\n# OPTIONAL PARAMETERS\n# These parameters have reasonable defaults.\n# ---------------------------------------------------------------------------------------------------------------------\n\nvariable \"location\" {\n  description = \"The Azure location where to deploy your resources too\"\n  type        = string\n  default     = \"East US\"\n}\n\nvariable \"postfix\" {\n  description = \"A postfix string to centrally mitigate resource name collisions\"\n  type        = string\n  default     = \"resource\"\n}\n\n"
  },
  {
    "path": "examples/azure/terraform-azure-cosmosdb-example/README.md",
    "content": "# Terraform Azure CosmosDB Example\n\nThis folder contains a complete Terraform Cosmos DB module that deploys resources in [Azure](https://azure.microsoft.com/) to demonstrate how you can use Terratest to write automated tests for your Azure Terraform code. This module deploys the following resources:\n\n- A [Cosmos DB Account](https://azure.microsoft.com/services/cosmos-db/) configured with:\n  - A [SQL Database](https://docs.microsoft.com/en-gb/azure/cosmos-db/account-databases-containers-items#azure-cosmos-databases)\n  - Three [SQL Containers](https://docs.microsoft.com/en-gb/azure/cosmos-db/account-databases-containers-items#azure-cosmos-containers)\n\nCheck out [test/azure/terraform_azure_cosmosdb_example_test.go](./../../../test/azure/terraform_azure_cosmosdb_example_test.go) to see how you can write automated tests for this module and validate the configuration of the parameters and options. \n\n**WARNING**: This module and the automated tests for it deploy real resources into your Azure account which can cost you money. \n\n## Running this module manually\n\n1. Sign up for [Azure](https://azure.microsoft.com/).\n1. Configure your Azure credentials using one of the [supported methods for Azure CLI\n   tools](https://docs.microsoft.com/en-us/cli/azure/azure-cli-configuration?view=azure-cli-latest)\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`.\n1. Ensure [environment variables](../README.md#review-environment-variables) are available\n1. Run `terraform init`\n1. Run `terraform apply`\n1. When you're done, run `terraform destroy`.\n\n## Running automated tests against this module\n\n1. Sign up for [Azure](https://azure.microsoft.com/)\n1. Configure your Azure credentials using one of the [supported methods for Azure CLI\n   tools](https://docs.microsoft.com/en-us/cli/azure/azure-cli-configuration?view=azure-cli-latest)\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`\n1. Configure your Terratest [Go test environment](../README.md) \n1. `cd test/azure`\n1. `go build terraform_azure_cosmosdb_example_test.go`\n1. `go test -v -timeout 60m -tags azure -run TestTerraformAzureCosmosDBExample`\n"
  },
  {
    "path": "examples/azure/terraform-azure-cosmosdb-example/main.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY AN AZURE VIRTUAL MACHINE\n# This is an example of how to deploy an Azure Virtual Machine with the minimum network resources.\n# ---------------------------------------------------------------------------------------------------------------------\n# See test/azure/terraform_azure_example_test.go for how to write automated tests for this code.\n# ---------------------------------------------------------------------------------------------------------------------\n\nprovider \"azurerm\" {\n  features {}\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# PIN TERRAFORM VERSION TO >= 0.12\n# The examples have been upgraded to 0.12 syntax\n# ---------------------------------------------------------------------------------------------------------------------\n\nterraform {\n  # This module is now only being tested with Terraform 0.13.x. However, to make upgrading easier, we are setting\n  # 0.12.26 as the minimum version, as that version added support for required_providers with source URLs, making it\n  # forwards compatible with 0.13.x code.\n  required_version = \">= 0.12.26\"\n  required_providers {\n    azurerm = {\n      version = \"~> 2.29\"\n      source  = \"hashicorp/azurerm\"\n    }\n  }\n}\n\n\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY A RESOURCE GROUP\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"azurerm_resource_group\" \"rg\" {\n  name     = \"terratest-cosmos-rg-${var.postfix}\"\n  location = var.location\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY A COSMOSDB ACCOUNT\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"azurerm_cosmosdb_account\" \"test\" {\n  name                = \"terratest-${var.postfix}\"\n  location            = azurerm_resource_group.rg.location\n  resource_group_name = azurerm_resource_group.rg.name\n  offer_type          = \"Standard\"\n  kind                = \"GlobalDocumentDB\"\n\n  consistency_policy {\n    consistency_level       = \"Session\"\n    max_interval_in_seconds = 5\n    max_staleness_prefix    = 100\n  }\n\n  geo_location {\n    location          = azurerm_resource_group.rg.location\n    failover_priority = 0\n  }\n}\n\nresource \"azurerm_cosmosdb_sql_database\" \"testdb\" {\n  name                = \"testdb\"\n  throughput          = var.throughput\n  resource_group_name = azurerm_resource_group.rg.name\n  account_name        = azurerm_cosmosdb_account.test.name\n}\n\nresource \"azurerm_cosmosdb_sql_container\" \"container1\" {\n  name                = \"test-container-1\"\n  throughput          = var.throughput\n  partition_key_path  = \"/key1\"\n  resource_group_name = azurerm_cosmosdb_account.test.resource_group_name\n  account_name        = azurerm_cosmosdb_account.test.name\n  database_name       = azurerm_cosmosdb_sql_database.testdb.name\n}\n\nresource \"azurerm_cosmosdb_sql_container\" \"container2\" {\n  name                = \"test-container-2\"\n  partition_key_path  = \"/key2\"\n  resource_group_name = azurerm_cosmosdb_account.test.resource_group_name\n  account_name        = azurerm_cosmosdb_account.test.name\n  database_name       = azurerm_cosmosdb_sql_database.testdb.name\n}\n\nresource \"azurerm_cosmosdb_sql_container\" \"container3\" {\n  name                = \"test-container-3\"\n  partition_key_path  = \"/key3\"\n  resource_group_name = azurerm_cosmosdb_account.test.resource_group_name\n  account_name        = azurerm_cosmosdb_account.test.name\n  database_name       = azurerm_cosmosdb_sql_database.testdb.name\n}\n"
  },
  {
    "path": "examples/azure/terraform-azure-cosmosdb-example/outputs.tf",
    "content": "output \"resource_group_name\" {\n  value = azurerm_resource_group.rg.name\n}\n\noutput \"account_name\" {\n  value = azurerm_cosmosdb_account.test.name\n}\n\noutput \"endpoint\" {\n  value = azurerm_cosmosdb_account.test.endpoint\n}\n\noutput \"primary_key\" {\n  value     = azurerm_cosmosdb_account.test.primary_key\n  sensitive = true\n}\n"
  },
  {
    "path": "examples/azure/terraform-azure-cosmosdb-example/variables.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# REQUIRED PARAMETERS\n# You must provide a value for each of these parameters.\n# ---------------------------------------------------------------------------------------------------------------------\n\n# ---------------------------------------------------------------------------------------------------------------------\n# OPTIONAL PARAMETERS\n# These parameters have reasonable defaults.\n# ---------------------------------------------------------------------------------------------------------------------\n\nvariable \"postfix\" {\n  description = \"A postfix string to centrally mitigate resource name collisions\"\n  type        = string\n  default     = \"resource\"\n}\n\nvariable \"location\" {\n  description = \"The location to set for the CosmosDB instance.\"\n  default     = \"East US\"\n}\n\nvariable \"throughput\" {\n  description = \"The RU/s throughput for the database account.\"\n  default     = 400\n}\n"
  },
  {
    "path": "examples/azure/terraform-azure-datafactory-example/README.md",
    "content": "# Terraform Azure Data Factory Example\n\nThis folder contains a Terraform module that deploys resources in [Azure](https://azure.microsoft.com/) to demonstrate how you can use Terratest to write automated tests for your Azure Terraform code.\nThis module deploys a Data Factory.\n\n- A [Azure MySQL Database](https://azure.microsoft.com/en-us/products/data-factory).\n\nCheck out [test/azure/terraform_azure_datafactory_example_test.go](./../../../test/azure/terraform_azure_datafactory_example_test.go) to see how you can write automated tests for this module and validate the configuration of the parameters and options.\n\n**WARNING**: This module and the automated tests for it deploy real resources into your Azure account which can cost you money.\n\n## Running this module manually\n1. Sign up for [Azure](https://azure.microsoft.com/).\n1. Configure your Azure credentials using one of the [supported methods for Azure CLI\n   tools](https://docs.microsoft.com/en-us/cli/azure/azure-cli-configuration?view=azure-cli-latest)\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`.\n1. Ensure [environment variables](../README.md#review-environment-variables) are available\n1. Run `terraform init`\n1. Run `terraform apply`\n1. When you're done, run `terraform destroy`.\n\n\n## Running automated tests against this module\n1. Sign up for [Azure](https://azure.microsoft.com/)\n1. Configure your Azure credentials using one of the [supported methods for Azure CLI\n   tools](https://docs.microsoft.com/en-us/cli/azure/azure-cli-configuration?view=azure-cli-latest)\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`\n1. Configure your Terratest [Go test environment](../README.md)\n1. `cd test/azure`\n1. `go build terraform_azure_datafactory_example_test.go`\n2. `go test -v -timeout 60m -tags azure -run TestTerraformAzureDataFactoryExample`"
  },
  {
    "path": "examples/azure/terraform-azure-datafactory-example/main.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY AN AZURE DATA FACTORY\n# This is an example of how to deploy an AZURE Data Factory\n# See test/terraform_azure_example_test.go for how to write automated tests for this code.\n# ---------------------------------------------------------------------------------------------------------------------\n\n\n# ---------------------------------------------------------------------------------------------------------------------\n# CONFIGURE OUR AZURE CONNECTION\n# ---------------------------------------------------------------------------------------------------------------------\nterraform {\n  required_providers {\n    azurerm = {\n      version = \"~> 2.93.0\"\n      source  = \"hashicorp/azurerm\"\n    }\n  }\n}\nprovider \"azurerm\" {\n  features {}\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# CREATE RANDOM PASSWORD\n# ---------------------------------------------------------------------------------------------------------------------\n\n# Random password is used as an example to simplify the deployment and improve the security of the database.\n# This is not as a production recommendation as the password is stored in the Terraform state file.\nresource \"random_password\" \"password\" {\n  length           = 16\n  override_special = \"-_%@\"\n  min_upper        = \"1\"\n  min_lower        = \"1\"\n  min_numeric      = \"1\"\n  min_special      = \"1\"\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY A RESOURCE GROUP\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"azurerm_resource_group\" \"datafactory_rg\" {\n  name     = \"terratest-datafactory-${var.postfix}\"\n  location = var.location\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY A DATA FACTORY\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"azurerm_data_factory\" \"data_factory\" {\n  name                = \"datafactory${var.postfix}\"\n  location            = azurerm_resource_group.datafactory_rg.location\n  resource_group_name = azurerm_resource_group.datafactory_rg.name\n}"
  },
  {
    "path": "examples/azure/terraform-azure-datafactory-example/outputs.tf",
    "content": "output \"resource_group_name\" {\n  value = azurerm_resource_group.datafactory_rg.name\n}\n\noutput \"datafactory_name\" {\n  value = azurerm_data_factory.data_factory.name\n}\n"
  },
  {
    "path": "examples/azure/terraform-azure-datafactory-example/variables.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# ENVIRONMENT VARIABLES\n# Define these secrets as environment variables\n# ---------------------------------------------------------------------------------------------------------------------\n\n# ARM_CLIENT_ID\n# ARM_CLIENT_SECRET\n# ARM_SUBSCRIPTION_ID\n# ARM_TENANT_ID\n\n# ---------------------------------------------------------------------------------------------------------------------\n# REQUIRED PARAMETERS\n# You must provide a value for each of these parameters.\n# ---------------------------------------------------------------------------------------------------------------------\n\n# ---------------------------------------------------------------------------------------------------------------------\n# OPTIONAL PARAMETERS\n# These parameters have reasonable defaults.\n# ---------------------------------------------------------------------------------------------------------------------\n\nvariable \"location\" {\n  description = \"The supported azure location where the resource exists\"\n  type        = string\n  default     = \"West US2\"\n}\n\nvariable \"postfix\" {\n  description = \"A postfix string to centrally mitigate resource name collisions.\"\n  type        = string\n  default     = \"resource\"\n}\n"
  },
  {
    "path": "examples/azure/terraform-azure-disk-example/README.md",
    "content": "# Terraform Azure Disk Example\n\nThis folder contains a simple Terraform module that deploys resources in [Azure](https://azure.microsoft.com/) to demonstrate\nhow you can use Terratest to write automated tests for your Azure Terraform code. This module deploys an [Azure managed disk](https://azure.microsoft.com/services/storage/disks).\n\nCheck out [test/azure/terraform_azure_disk_example_test.go](/test/azure/terraform_azure_disk_example_test.go) to see how you can write automated tests for this module.\n\nNote that the resources deployed in this module don't actually do anything; it just runs the resources for demonstration purposes.\n\n**WARNING**: This module and the automated tests for it deploy real resources into your Azure account which can cost you money. The resources are all part of the [Azure Free Account](https://azure.microsoft.com/free/), so if you haven't used that up, it should be free, but you are completely responsible for all Azure charges.\n\n## Running this module manually\n\n1. Sign up for [Azure](https://azure.microsoft.com/)\n1. Configure your Azure credentials using one of the [supported methods for Azure CLI\n   tools](https://docs.microsoft.com/cli/azure/azure-cli-configuration?view=azure-cli-latest)\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`\n1. Ensure [environment variables](../README.md#review-environment-variables) are available\n1. Run `terraform init`\n1. Run `terraform apply`\n1. When you're done, run `terraform destroy`\n\n## Running automated tests against this module\n\n1. Sign up for [Azure](https://azure.microsoft.com/).\n2. Configure your Azure credentials using one of the [supported methods for Azure CLI tools](https://docs.microsoft.com/cli/azure/azure-cli-configuration?view=azure-cli-latest).\n\n3. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`.\n4. [Review environment variables](#review-environment-variables).\n5. Install [Golang](https://golang.org/) and make sure this code is checked out into your `GOPATH`.\n6. `cd test`\n7. Make sure [the azure-sdk-for-go versions match](#check-go-dependencies) in [/test/go.mod](/test/go.mod) and in [test/azure/terraform_azure_disk_example_test.go](/test/azure/terraform_azure_disk_example_test.go).\n8. `go test -v -run TestTerraformAzureDiskExample`\n"
  },
  {
    "path": "examples/azure/terraform-azure-disk-example/main.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY AN AZURE MANAGED DISK\n# This is an example of how to deploy a managed disk.\n# ---------------------------------------------------------------------------------------------------------------------\n# See test/azure/terraform_azure_disk_example_test.go for how to write automated tests for this code.\n# ---------------------------------------------------------------------------------------------------------------------\n\nprovider \"azurerm\" {\n  features {}\n}\n\nterraform {\n  # This module is now only being tested with Terraform 0.13.x. However, to make upgrading easier, we are setting\n  # 0.12.26 as the minimum version, as that version added support for required_providers with source URLs, making it\n  # forwards compatible with 0.13.x code.\n  required_version = \">= 0.12.26\"\n  required_providers {\n    azurerm = {\n      version = \"~> 2.29\"\n      source  = \"hashicorp/azurerm\"\n    }\n  }\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY A RESOURCE GROUP\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"azurerm_resource_group\" \"disk_rg\" {\n  name     = \"terratest-disk-rg-${var.postfix}\"\n  location = var.location\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY THE DISK\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"azurerm_managed_disk\" \"disk\" {\n  name                 = \"disk-${var.postfix}\"\n  location             = azurerm_resource_group.disk_rg.location\n  resource_group_name  = azurerm_resource_group.disk_rg.name\n  storage_account_type = var.disk_type\n  create_option        = \"Empty\"\n  disk_size_gb         = 10\n}\n"
  },
  {
    "path": "examples/azure/terraform-azure-disk-example/outputs.tf",
    "content": "output \"resource_group_name\" {\n  value = azurerm_resource_group.disk_rg.name\n}\n\noutput \"disk_name\" {\n  value = azurerm_managed_disk.disk.name\n}\n\noutput \"disk_type\" {\n  value = azurerm_managed_disk.disk.storage_account_type\n}\n"
  },
  {
    "path": "examples/azure/terraform-azure-disk-example/variables.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# ENVIRONMENT VARIABLES\n# Define these secrets as environment variables\n# ---------------------------------------------------------------------------------------------------------------------\n\n# ARM_CLIENT_ID\n# ARM_CLIENT_SECRET\n# ARM_SUBSCRIPTION_ID\n# ARM_TENANT_ID\n\n# ---------------------------------------------------------------------------------------------------------------------\n# REQUIRED PARAMETERS\n# You must provide a value for each of these parameters.\n# ---------------------------------------------------------------------------------------------------------------------\n\n# ---------------------------------------------------------------------------------------------------------------------\n# OPTIONAL PARAMETERS\n# These parameters have reasonable defaults.\n# ---------------------------------------------------------------------------------------------------------------------\n\nvariable \"postfix\" {\n  description = \"A postfix string to centrally mitigate resource name collisions\"\n  type        = string\n  default     = \"resource\"\n}\n\nvariable \"location\" {\n  description = \"The Azure region in which to deploy your resources to\"\n  type        = string\n  default     = \"East US\"\n}\n\nvariable \"disk_type\" {\n  description = \"The managed disk type\"\n  type        = string\n  default     = \"Standard_LRS\"\n}\n\n\n"
  },
  {
    "path": "examples/azure/terraform-azure-example/README.md",
    "content": "# Terraform Azure Example\n\nThis folder contains a simple Terraform module that deploys resources in [Azure](https://azure.microsoft.com/) to demonstrate\nhow you can use Terratest to write automated tests for your Azure Terraform code. This module deploys a [Virtual\nMachine](https://azure.microsoft.com/en-us/services/virtual-machines/) and gives that VM a `Name` tag with the value specified in the\n`vm_name` variable.\n\nCheck out [test/azure/terraform_azure_example_test.go](/test/azure/terraform_azure_example_test.go) to see how you can write\nautomated tests for this module.\n\nNote that the Virtual Machine in this module doesn't actually do anything; it just runs a Vanilla Ubuntu 16.04 image for\ndemonstration purposes. For slightly more complicated, real-world examples of Terraform modules, see\n[terraform-http-example](/examples/terraform-http-example) and [terraform-ssh-example](/examples/terraform-ssh-example).\n\n**WARNING**: This module and the automated tests for it deploy real resources into your Azure account which can cost you\nmoney. The resources are all part of the [Azure Free Account](https://azure.microsoft.com/en-us/free/), so if you haven't used that up,\nit should be free, but you are completely responsible for all Azure charges.\n\n## Running this module manually\n\n1. Sign up for [Azure](https://azure.microsoft.com/).\n1. Configure your Azure credentials using one of the [supported methods for Azure CLI\n   tools](https://docs.microsoft.com/en-us/cli/azure/azure-cli-configuration?view=azure-cli-latest).\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`.\n1. Run `terraform init`.\n1. Run `terraform apply`.\n1. When you're done, run `terraform destroy`.\n\n## Running automated tests against this module\n\n1. Sign up for [Azure](https://azure.microsoft.com/).\n1. Configure your Azure credentials using one of the [supported methods for Azure CLI\n   tools](https://docs.microsoft.com/en-us/cli/azure/azure-cli-configuration?view=azure-cli-latest).\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`.\n1. [Review environment variables](#review-environment-variables).\n1. Install [Golang](https://golang.org/) and make sure this code is checked out into your `GOPATH`.\n1. `cd test`\n1. Make sure [the azure-sdk-for-go versions match](#check-go-dependencies) in [/test/go.mod](/test/go.mod) and in [test/azure/terraform_azure_example_test.go](/test/azure/terraform_azure_example_test.go).\n1. `go build terraform_azure_example_test.go`\n1. `go test -v -run TestTerraformAzureExample`\n\n## Check Go Dependencies\n\nCheck that the `github.com/Azure/azure-sdk-for-go` version in your generated `go.mod` for this test matches the version in the terratest [go.mod](https://github.com/gruntwork-io/terratest/blob/main/go.mod) file.\n\n> This was tested with **go1.14.1**.\n\n### Check Azure-sdk-for-go version\n\nLet's make sure [go.mod](https://github.com/gruntwork-io/terratest/blob/main/go.mod) includes the appropriate [azure-sdk-for-go version](https://github.com/Azure/azure-sdk-for-go/releases/tag/v38.1.0):\n\n```go\nrequire (\n    ...\n    github.com/Azure/azure-sdk-for-go v38.1.0+incompatible\n    ...\n)\n```\n\nWe should check that [test/azure/terraform_azure_example_test.go](/test/azure/terraform_azure_example_test.go) includes the corresponding [azure-sdk-for-go package](https://github.com/Azure/azure-sdk-for-go/tree/master/services/compute/mgmt/2019-07-01/compute):\n\n```go\nimport (\n    \"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2019-07-01/compute\"\n    ...\n)\n```\n\nIf we make changes to either the **go.mod** or the **go test file**, we should make sure that the go build command works still.\n\n```powershell\ngo build terraform_azure_example_test.go\n```\n\n## Review Environment Variables\n\nAs part of configuring terraform for Azure, we'll want to check that we have set the appropriate [credentials](https://docs.microsoft.com/azure/terraform/terraform-install-configure?toc=https%3A%2F%2Fdocs.microsoft.com%2Fen-us%2Fazure%2Fterraform%2Ftoc.json&bc=https%3A%2F%2Fdocs.microsoft.com%2Fen-us%2Fazure%2Fbread%2Ftoc.json#set-up-terraform-access-to-azure) and also that we set the [environment variables](https://docs.microsoft.com/azure/terraform/terraform-install-configure?toc=https%3A%2F%2Fdocs.microsoft.com%2Fen-us%2Fazure%2Fterraform%2Ftoc.json&bc=https%3A%2F%2Fdocs.microsoft.com%2Fen-us%2Fazure%2Fbread%2Ftoc.json#configure-terraform-environment-variables) on the testing host.\n\nFor non-commercial cloud deployments, set the \"AZURE_ENVIRONMENT\" environment variable to the appropriate cloud environment (this is used by the [client_factory](../../modules/azure/client_factory.go) to set the correct azure endpoints):\n\n```bash\nexport ARM_CLIENT_ID=your_app_id\nexport ARM_CLIENT_SECRET=your_password\nexport ARM_SUBSCRIPTION_ID=your_subscription_id\nexport ARM_TENANT_ID=your_tenant_id\n\n# AZURE_ENVIRONMENT is the name of the Azure environment to use. Set to one of the following:\nexport AZURE_ENVIRONMENT=AzureUSGovernmentCloud\nexport AZURE_ENVIRONMENT=AzureChinaCloud\nexport AZURE_ENVIRONMENT=AzureGermanCloud\nexport AZURE_ENVIRONMENT=AzurePublicCloud\nexport AZURE_ENVIRONMENT=AzureStackCloud\n```\n\nNote, in a Windows environment, these should be set as **system environment variables**. We can use a PowerShell console with administrative rights to update these environment variables:\n\n```powershell\n[System.Environment]::SetEnvironmentVariable(\"ARM_CLIENT_ID\",$your_app_id,[System.EnvironmentVariableTarget]::Machine)\n[System.Environment]::SetEnvironmentVariable(\"ARM_CLIENT_SECRET\",$your_password,[System.EnvironmentVariableTarget]::Machine)\n[System.Environment]::SetEnvironmentVariable(\"ARM_SUBSCRIPTION_ID\",$your_subscription_id,[System.EnvironmentVariableTarget]::Machine)\n[System.Environment]::SetEnvironmentVariable(\"ARM_TENANT_ID\",$your_tenant_id,[System.EnvironmentVariableTarget]::Machine)\n[System.Environment]::SetEnvironmentVariable(\"AZURE_ENVIRONMENT\",$your_azure_env,[System.EnvironmentVariableTarget]::Machine)\n```\n"
  },
  {
    "path": "examples/azure/terraform-azure-example/main.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY AN AZURE VIRTUAL MACHINE\n# This is an example of how to deploy an Azure Virtual Machine with the minimum network resources.\n# ---------------------------------------------------------------------------------------------------------------------\n# See test/azure/terraform_azure_example_test.go for how to write automated tests for this code.\n# ---------------------------------------------------------------------------------------------------------------------\n\nprovider \"azurerm\" {\n  features {}\n}\n\n\n# ---------------------------------------------------------------------------------------------------------------------\n# PIN TERRAFORM VERSION TO >= 0.12\n# The examples have been upgraded to 0.12 syntax\n# ---------------------------------------------------------------------------------------------------------------------\n\nterraform {\n  # This module is now only being tested with Terraform 0.13.x. However, to make upgrading easier, we are setting\n  # 0.12.26 as the minimum version, as that version added support for required_providers with source URLs, making it\n  # forwards compatible with 0.13.x code.\n  required_version = \">= 0.12.26\"\n  required_providers {\n    azurerm = {\n      version = \"~> 2.50\"\n      source  = \"hashicorp/azurerm\"\n    }\n  }\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY A RESOURCE GROUP\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"azurerm_resource_group\" \"main\" {\n  name     = \"terratest-rg-${var.postfix}\"\n  location = \"East US\"\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY VIRTUAL NETWORK RESOURCES\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"azurerm_virtual_network\" \"main\" {\n  name                = \"vnet-${var.postfix}\"\n  address_space       = [\"10.0.0.0/16\"]\n  location            = azurerm_resource_group.main.location\n  resource_group_name = azurerm_resource_group.main.name\n}\n\nresource \"azurerm_subnet\" \"internal\" {\n  name                 = \"subnet-${var.postfix}\"\n  resource_group_name  = azurerm_resource_group.main.name\n  virtual_network_name = azurerm_virtual_network.main.name\n  address_prefixes     = [\"10.0.17.0/24\"]\n}\n\nresource \"azurerm_network_interface\" \"main\" {\n  name                = \"nic-${var.postfix}\"\n  location            = azurerm_resource_group.main.location\n  resource_group_name = azurerm_resource_group.main.name\n\n  ip_configuration {\n    name                          = \"terratestconfiguration1\"\n    subnet_id                     = azurerm_subnet.internal.id\n    private_ip_address_allocation = \"Dynamic\"\n  }\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY A VIRTUAL MACHINE RUNNING UBUNTU\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"azurerm_virtual_machine\" \"main\" {\n  name                             = \"vm-${var.postfix}\"\n  location                         = azurerm_resource_group.main.location\n  resource_group_name              = azurerm_resource_group.main.name\n  network_interface_ids            = [azurerm_network_interface.main.id]\n  vm_size                          = \"Standard_B1s\"\n  delete_os_disk_on_termination    = true\n  delete_data_disks_on_termination = true\n\n  storage_image_reference {\n    publisher = \"Canonical\"\n    offer     = \"UbuntuServer\"\n    sku       = \"16.04-LTS\"\n    version   = \"latest\"\n  }\n\n  storage_os_disk {\n    name              = \"terratestosdisk1\"\n    caching           = \"ReadWrite\"\n    create_option     = \"FromImage\"\n    managed_disk_type = \"Standard_LRS\"\n  }\n\n  os_profile {\n    computer_name  = \"vm-${var.postfix}\"\n    admin_username = var.username\n    admin_password = random_password.main.result\n  }\n\n  os_profile_linux_config {\n    disable_password_authentication = false\n  }\n\n  depends_on = [random_password.main]\n}\n\n# Random password is used as an example to simplify the deployment and improve the security of the remote VM.\n# This is not as a production recommendation as the password is stored in the Terraform state file.\nresource \"random_password\" \"main\" {\n  length           = 16\n  override_special = \"-_%@\"\n  min_upper        = \"1\"\n  min_lower        = \"1\"\n  min_numeric      = \"1\"\n  min_special      = \"1\"\n}\n"
  },
  {
    "path": "examples/azure/terraform-azure-example/outputs.tf",
    "content": "output \"resource_group_name\" {\n  value = azurerm_resource_group.main.name\n}\n\noutput \"vm_name\" {\n  value = azurerm_virtual_machine.main.name\n}\n"
  },
  {
    "path": "examples/azure/terraform-azure-example/variables.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# ENVIRONMENT VARIABLES\n# Define these secrets as environment variables\n# ---------------------------------------------------------------------------------------------------------------------\n\n# ARM_CLIENT_ID\n# ARM_CLIENT_SECRET\n# ARM_SUBSCRIPTION_ID\n# ARM_TENANT_ID\n\n# ---------------------------------------------------------------------------------------------------------------------\n# REQUIRED PARAMETERS\n# You must provide a value for each of these parameters.\n# ---------------------------------------------------------------------------------------------------------------------\n\n# ---------------------------------------------------------------------------------------------------------------------\n# OPTIONAL PARAMETERS\n# These parameters have reasonable defaults.\n# ---------------------------------------------------------------------------------------------------------------------\n\nvariable \"location\" {\n  description = \"The Azure location where to deploy your resources too\"\n  type        = string\n  default     = \"East US\"\n}\n\nvariable \"postfix\" {\n  description = \"A postfix string to centrally mitigate resource name collisions\"\n  type        = string\n  default     = \"resource\"\n}\n\nvariable \"username\" {\n  description = \"The username to be provisioned into your VM\"\n  type        = string\n  default     = \"testadmin\"\n}\n\n"
  },
  {
    "path": "examples/azure/terraform-azure-frontdoor-example/README.md",
    "content": "# Terraform Azure Front Door Example\n\nThis folder contains a simple Terraform module that deploys resources in [Azure](https://azure.microsoft.com/) to demonstrate\nhow you can use Terratest to write automated tests for your Azure Terraform code. This module deploys an [Azure Front Door](https://azure.microsoft.com/en-us/services/frontdoor/).\n\nCheck out [test/azure/terraform_azure_frontdoor_example_test.go](./../../../test/azure/terraform_azure_frontdoor_example_test.go) to see how you can write automated tests for this module and validate the configuration of the parameters and options. \n\n**WARNING**: This module and the automated tests for it deploy real resources into your Azure account which can cost you money. \n\n## Running this module manually\n\n1. Sign up for [Azure](https://azure.microsoft.com/).\n1. Configure your Azure credentials using one of the [supported methods for Azure CLI\n   tools](https://docs.microsoft.com/en-us/cli/azure/azure-cli-configuration?view=azure-cli-latest)\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`.\n1. Ensure [environment variables](../README.md#review-environment-variables) are available\n1. Run `terraform init`\n1. Run `terraform apply`\n1. When you're done, run `terraform destroy`.\n\n## Running automated tests against this module\n\n1. Sign up for [Azure](https://azure.microsoft.com/)\n1. Configure your Azure credentials using one of the [supported methods for Azure CLI\n   tools](https://docs.microsoft.com/en-us/cli/azure/azure-cli-configuration?view=azure-cli-latest)\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`\n1. Configure your Terratest [Go test environment](../README.md) \n1. `cd test/azure`\n1. `go build terraform_azure_frontdoor_example_test.go`\n1. `go test -v -timeout 60m -tags azure -run TestTerraformAzureFrontDoorExample`"
  },
  {
    "path": "examples/azure/terraform-azure-frontdoor-example/main.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY AN AZURE FRONT DOOR\n# This is an example of how to deploy an Azure Front Door with the minimum resources.\n# ---------------------------------------------------------------------------------------------------------------------\n# See test/azure/terraform_azure_frontdoor_example_test.go for how to write automated tests for this code.\n# ---------------------------------------------------------------------------------------------------------------------\n\nterraform {\n  required_version = \">=0.14.0\"\n}\n\nprovider \"azurerm\" {\n  features {}\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY A RESOURCE GROUP\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"azurerm_resource_group\" \"rg\" {\n  name     = \"terratest-frontdoor-rg-${var.postfix}\"\n  location = var.location\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY FRONT DOOR\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"azurerm_frontdoor\" \"frontdoor\" {\n  name                = \"terratest-afd-${var.postfix}\"\n  resource_group_name = azurerm_resource_group.rg.name\n\n  backend_pool_settings {\n    enforce_backend_pools_certificate_name_check = false\n  }\n\n  routing_rule {\n    name               = \"terratestRoutingRule1\"\n    accepted_protocols = [\"Http\", \"Https\"]\n    patterns_to_match  = [\"/*\"]\n    frontend_endpoints = [\"terratestEndpoint\"]\n    forwarding_configuration {\n      forwarding_protocol = \"MatchRequest\"\n      backend_pool_name   = \"terratestBackend\"\n    }\n  }\n\n  backend_pool_load_balancing {\n    name = \"terratestLoadBalanceSetting\"\n  }\n\n  backend_pool_health_probe {\n    name = \"terratestHealthProbeSetting\"\n  }\n\n  backend_pool {\n    name = \"terratestBackend\"\n    backend {\n      host_header = var.backend_host\n      address     = var.backend_host\n      http_port   = 80\n      https_port  = 443\n    }\n\n    load_balancing_name = \"terratestLoadBalanceSetting\"\n    health_probe_name   = \"terratestHealthProbeSetting\"\n  }\n\n  frontend_endpoint {\n    name      = \"terratestEndpoint\"\n    host_name = \"terratest-afd-${var.postfix}.azurefd.net\"\n  }\n}\n"
  },
  {
    "path": "examples/azure/terraform-azure-frontdoor-example/output.tf",
    "content": "output \"resource_group_name\" {\n  value = azurerm_resource_group.rg.name\n}\n\noutput \"front_door_name\" {\n  description = \"Specifies the name of the Front Door service.\"\n  value       = azurerm_frontdoor.frontdoor.name\n}\n\noutput \"front_door_url\" {\n  description = \"Specifies the host name of the frontend_endpoint. Must be a domain name.\"\n  value       = azurerm_frontdoor.frontdoor.frontend_endpoint[0].host_name\n}\n\noutput \"front_door_endpoint_name\" {\n  description = \"Specifies the friendly name of the frontend_endpoint\"\n  value       = azurerm_frontdoor.frontdoor.frontend_endpoint[0].name\n}"
  },
  {
    "path": "examples/azure/terraform-azure-frontdoor-example/variables.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# ENVIRONMENT VARIABLES\n# Define these secrets as environment variables\n# ---------------------------------------------------------------------------------------------------------------------\n\n# ARM_CLIENT_ID\n# ARM_CLIENT_SECRET\n# ARM_SUBSCRIPTION_ID\n# ARM_TENANT_ID\n\n# ---------------------------------------------------------------------------------------------------------------------\n# OPTIONAL PARAMETERS\n# These parameters have reasonable defaults.\n# ---------------------------------------------------------------------------------------------------------------------\n\nvariable \"location\" {\n  description = \"The Azure location where to deploy your resources too\"\n  type        = string\n  default     = \"East US\"\n}\n\nvariable \"postfix\" {\n  description = \"A postfix string to centrally mitigate resource name collisions\"\n  type        = string\n  default     = \"resource\"\n}\n\nvariable \"backend_host\" {\n  description = \"The IP address or FQDN of the backend\"\n  type        = string\n  default     = \"www.bing.com\"\n}"
  },
  {
    "path": "examples/azure/terraform-azure-functionapp-example/README.md",
    "content": "# Terraform Azure Function App Example\n\nThis folder contains a Terraform module that deploys a Function App in [Azure](https://azure.microsoft.com/) to demonstrate how you can use Terratest to write automated tests for your Azure Terraform code.\n\nThis module deploys [Azure Storage](https://azure.microsoft.com/en-us/services/storage/), [Azure Function App](https://azure.microsoft.com/en-us/services/functions/), [Azure Function App](https://azure.microsoft.com/en-us/services/functions/).\n\nCheck out [test/azure/terraform_azure_functionapp_example_test.go](/test/azure/terraform_azure_functionapp_example_test.go) to see how you can write automated tests for this module and validate the configuration of the parameters and options.\n\n**WARNING**: This module and the automated tests for it deploy real resources into your Azure account which can cost you\nmoney. The resources are all part of the [Azure Free Account](https://azure.microsoft.com/free/), so if you haven't used that up,\nit should be free, but you are completely responsible for all Azure charges.\n\n## Running this module manually\n\n1. Sign up for [Azure](https://azure.microsoft.com/)\n1. Configure your Azure credentials using one of the [supported methods for Azure CLI\n   tools](https://docs.microsoft.com/cli/azure/azure-cli-configuration?view=azure-cli-latest)\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`\n1. Ensure [environment variables](../README.md#review-environment-variables) are available\n1. Run `terraform init`\n1. Run `terraform apply`\n1. When you're done, run `terraform destroy`\n\n## Running automated tests against this module\n\n1. Sign up for [Azure](https://azure.microsoft.com/)\n1. Configure your Azure credentials using one of the [supported methods for Azure CLI\n   tools](https://docs.microsoft.com/cli/azure/azure-cli-configuration?view=azure-cli-latest)\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`\n1. Configure your Terratest [Go test environment](../README.md)\n1. `cd test/azure`\n1. `go build terraform_azure_functionapp_example_test.go`\n1. `go test -v -run TestTerraformAzureFunctionAppExample`\n"
  },
  {
    "path": "examples/azure/terraform-azure-functionapp-example/main.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# Deploy an Azure storage account, service plan, function app, and application insights\n# This is an example of how to deploy an Azure function app.\n# See test/terraform_azure_functionapp_example_test.go for how to write automated tests for this code.\n# ---------------------------------------------------------------------------------------------------------------------\n\n\n# ---------------------------------------------------------------------------------------------------------------------\n# CONFIGURE OUR AZURE CONNECTION\n# ---------------------------------------------------------------------------------------------------------------------\n\nterraform {\n  required_providers {\n    azurerm = {\n      version = \"~>2.29.0\"\n      source  = \"hashicorp/azurerm\"\n    }\n  }\n}\n\nprovider \"azurerm\" {\n  features {}\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY THE RESOURCE GROUP\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"azurerm_resource_group\" \"app_rg\" {\n  name     = \"terratest-functionapp-rg-${var.postfix}\"\n  location = var.location\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY THE AZURE STORAGE ACCOUNT\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"azurerm_storage_account\" \"storage\" {\n  name                     = \"storageaccount${var.postfix}\"\n  resource_group_name      = azurerm_resource_group.app_rg.name\n  location                 = azurerm_resource_group.app_rg.location\n  account_tier             = \"Standard\"\n  account_replication_type = \"LRS\"\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY THE APP SERVICE PLAN\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"azurerm_app_service_plan\" \"app_service_plan\" {\n  name                = \"appservice-plan-${var.postfix}\"\n  location            = azurerm_resource_group.app_rg.location\n  resource_group_name = azurerm_resource_group.app_rg.name\n  kind                = \"FunctionApp\"\n\n  sku {\n    tier = \"Standard\"\n    size = \"S1\"\n  }\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY THE APPLICATION INSIGHTS\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"azurerm_application_insights\" \"application_insights\" {\n  name                = \"appinsights-${var.postfix}\"\n  location            = azurerm_resource_group.app_rg.location\n  resource_group_name = azurerm_resource_group.app_rg.name\n  application_type    = \"web\"\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY THE AZURE FUNCTION APP\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"azurerm_function_app\" \"function_app\" {\n  name                       = \"functionapp-${var.postfix}\"\n  location                   = azurerm_resource_group.app_rg.location\n  resource_group_name        = azurerm_resource_group.app_rg.name\n  app_service_plan_id        = azurerm_app_service_plan.app_service_plan.id\n  storage_account_name       = azurerm_storage_account.storage.name\n  storage_account_access_key = azurerm_storage_account.storage.primary_access_key\n\n\n  app_settings = {\n    \"APPINSIGHTS_INSTRUMENTATIONKEY\"        = azurerm_application_insights.application_insights.instrumentation_key\n    \"APPLICATIONINSIGHTS_CONNECTION_STRING\" = \"InstrumentationKey=${azurerm_application_insights.application_insights.instrumentation_key}\"\n  }\n}\n"
  },
  {
    "path": "examples/azure/terraform-azure-functionapp-example/output.tf",
    "content": "output \"resource_group_name\" {\n  value = azurerm_resource_group.app_rg.name\n}\n\noutput \"function_app_id\" {\n  value = azurerm_function_app.function_app.id\n}\n\noutput \"default_hostname\" {\n  value = azurerm_function_app.function_app.default_hostname\n}\n\noutput \"function_app_kind\" {\n  value = azurerm_function_app.function_app.kind\n}\n\noutput \"function_app_name\" {\n  value = azurerm_function_app.function_app.name\n}\n"
  },
  {
    "path": "examples/azure/terraform-azure-functionapp-example/variables.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# ENVIRONMENT VARIABLES\n# Define these secrets as environment variables\n# ---------------------------------------------------------------------------------------------------------------------\n\n# ARM_CLIENT_ID\n# ARM_CLIENT_SECRET\n# ARM_SUBSCRIPTION_ID\n# ARM_TENANT_ID\n\n# ---------------------------------------------------------------------------------------------------------------------\n# REQUIRED PARAMETERS\n# You must provide a value for each of these parameters.\n# ---------------------------------------------------------------------------------------------------------------------\n\n# ---------------------------------------------------------------------------------------------------------------------\n# OPTIONAL PARAMETERS\n# These parameters have reasonable defaults.\n# ---------------------------------------------------------------------------------------------------------------------\n\nvariable \"location\" {\n  description = \"The supported azure location where the resource exists\"\n  type        = string\n  default     = \"West US2\"\n}\n\nvariable \"postfix\" {\n  description = \"A postfix string to centrally mitigate resource name collisions.\"\n  type        = string\n  default     = \"1276\"\n}\n"
  },
  {
    "path": "examples/azure/terraform-azure-keyvault-example/README.md",
    "content": "# Terraform Azure Keyvault Example\n\nThis folder contains a simple Terraform module that deploys resources in [Azure](https://azure.microsoft.com/) to demonstrate\nhow you can use Terratest to write automated tests for your Azure Terraform code. This module deploys a Key Vault with one secret, key, and certificate each.\n\n- A [Key Vault](https://azure.microsoft.com/services/key-vault/) that gives the module the following:\n  - [Secret](https://docs.microsoft.com/azure/key-vault/general/about-keys-secrets-certificates)  with the value specified in the `secret_name`  output variable.\n  - [Key](https://docs.microsoft.com/azure/key-vault/general/about-keys-secrets-certificates)  with the value specified in the `key_name`  output variable.\n  - [Certificate](https://docs.microsoft.com/azure/key-vault/general/about-keys-secrets-certificates)  with the value specified in the `certificate_name`  output variable.\n\nCheck out [test/azure/terraform_azure_keyvault_example_test.go](/test/azure/terraform_azure_keyvault_example_test.go) to see how you can write\nautomated tests for this module.\n\nNote that the Key Vault in this module don't actually do anything; it just runs the resources for\ndemonstration purposes.\n\n**WARNING**: This module and the automated tests for it deploy real resources into your Azure account which can cost you\nmoney. The resources are all part of the [Azure Free Account](https://azure.microsoft.com/free/), so if you haven't used that up,\nit should be free, but you are completely responsible for all Azure charges.\n\n## Running this module manually\n\n1. Sign up for [Azure](https://azure.microsoft.com/)\n1. Configure your Azure credentials using one of the [supported methods for Azure CLI\n   tools](https://docs.microsoft.com/cli/azure/azure-cli-configuration?view=azure-cli-latest)\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`\n1. Ensure [environment variables](../README.md#review-environment-variables) are available\n1. Run `terraform init`\n1. Run `terraform apply`\n1. When you're done, run `terraform destroy`\n\n## Running automated tests against this module\n\n1. Sign up for [Azure](https://azure.microsoft.com/)\n1. Configure your Azure credentials using one of the [supported methods for Azure CLI\n   tools](https://docs.microsoft.com/cli/azure/azure-cli-configuration?view=azure-cli-latest)\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`\n1. Configure your Terratest [Go test environment](../README.md)\n1. `cd test/azure`\n1. `go build terraform_azure_keyvault_example_test.go`\n1. `go test -v -run TestTerraformAzureKeyVaultExample`\n"
  },
  {
    "path": "examples/azure/terraform-azure-keyvault-example/main.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY AN AZURE KEY VAULT\n# This is an example of how to deploy a Key Vault \n# ---------------------------------------------------------------------------------------------------------------------\n# See test/azure/terraform_azure_keyvault_example_test.go for how to write automated tests for this code.\n# ---------------------------------------------------------------------------------------------------------------------\n\nprovider \"azurerm\" {\n  features {\n    key_vault {\n      purge_soft_delete_on_destroy = false\n    }\n  }\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# PIN TERRAFORM VERSION TO >= 0.12\n# The examples have been upgraded to 0.12 syntax\n# ---------------------------------------------------------------------------------------------------------------------\n\nterraform {\n  # This module is now only being tested with Terraform 0.13.x. However, to make upgrading easier, we are setting\n  # 0.12.26 as the minimum version, as that version added support for required_providers with source URLs, making it\n  # forwards compatible with 0.13.x code.\n  required_version = \">= 0.12.26\"\n  required_providers {\n    azurerm = {\n      version = \"~>3.0\"\n      source  = \"hashicorp/azurerm\"\n    }\n  }\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY A RESOURCE GROUP\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"azurerm_resource_group\" \"resource_group\" {\n  name     = \"terratest-kv-rg-${var.postfix}\"\n  location = var.location\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# CONFIGURE A CLIENT FOR KEY VAULT ACCESS\n# ---------------------------------------------------------------------------------------------------------------------\n\ndata \"azurerm_client_config\" \"current\" {}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# CONFIGURE AN ACCESS POLICY TO MANAGE THE SECRET, KEY, AND CERTIFICATE\n# ---------------------------------------------------------------------------------------------------------------------\n\ndata \"azurerm_key_vault_access_policy\" \"contributor\" {\n  name = \"Key, Secret, & Certificate Management\"\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY A KEY VAULT\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"azurerm_key_vault\" \"key_vault\" {\n  name                        = \"keyvault-${var.postfix}\"\n  location                    = azurerm_resource_group.resource_group.location\n  resource_group_name         = azurerm_resource_group.resource_group.name\n  enabled_for_disk_encryption = true\n  tenant_id                   = data.azurerm_client_config.current.tenant_id\n\n  soft_delete_retention_days = 7\n  purge_protection_enabled   = false\n\n  sku_name = \"standard\"\n\n  access_policy {\n    tenant_id = data.azurerm_client_config.current.tenant_id\n    object_id = data.azurerm_client_config.current.object_id\n\n    key_permissions = [\n      \"Create\",\n      \"Get\",\n      \"List\",\n      \"Delete\",\n      \"Purge\",\n      \"SetRotationPolicy\",\n      \"GetRotationPolicy\"\n    ]\n\n    secret_permissions = [\n      \"Set\",\n      \"Get\",\n      \"List\",\n      \"Delete\",\n      \"Purge\",\n    ]\n\n    certificate_permissions = [\n      \"Create\",\n      \"Delete\",\n      \"DeleteIssuers\",\n      \"Get\",\n      \"GetIssuers\",\n      \"Import\",\n      \"List\",\n      \"ListIssuers\",\n      \"ManageContacts\",\n      \"ManageIssuers\",\n      \"SetIssuers\",\n      \"Update\",\n      \"Purge\",\n    ]\n  }\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY A SECRET TO THE KEY VAULT\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"azurerm_key_vault_secret\" \"key_vault_secret\" {\n  name         = \"${var.secret_name}-${var.postfix}\"\n  value        = \"mysecret\"\n  key_vault_id = azurerm_key_vault.key_vault.id\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n#  DEPLOY A KEY TO THE KEY VAULT\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"azurerm_key_vault_key\" \"key_vault_key\" {\n  name         = \"${var.key_name}-${var.postfix}\"\n  key_vault_id = azurerm_key_vault.key_vault.id\n  key_type     = \"RSA\"\n  key_size     = 2048\n\n  key_opts = [\n    \"decrypt\",\n    \"encrypt\",\n    \"sign\",\n    \"unwrapKey\",\n    \"verify\",\n    \"wrapKey\",\n  ]\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n#  DEPLOY A CERTIFICATE TO THE KEY VAULT\n#  The example uses a sample pfx file with plain text password to make it easier to test. However, in production modules \n#  should use a more secure mechanisms for transferring these files.\n# ---------------------------------------------------------------------------------------------------------------------\nresource \"azurerm_key_vault_certificate\" \"key_vault_certificate\" {\n  name         = \"${var.certificate_name}-${var.postfix}\"\n  key_vault_id = azurerm_key_vault.key_vault.id\n\n  certificate {\n    contents = filebase64(\"example.pfx\")\n    password = \"password\"\n  }\n\n  certificate_policy {\n    issuer_parameters {\n      name = \"Self\"\n    }\n\n    key_properties {\n      exportable = true\n      key_size   = 2048\n      key_type   = \"RSA\"\n      reuse_key  = false\n    }\n\n    secret_properties {\n      content_type = \"application/x-pkcs12\"\n    }\n  }\n}\n"
  },
  {
    "path": "examples/azure/terraform-azure-keyvault-example/outputs.tf",
    "content": "output \"resource_group_name\" {\n  value = azurerm_resource_group.resource_group.name\n}\n\noutput \"key_vault_name\" {\n  value = azurerm_key_vault.key_vault.name\n}\n\noutput \"secret_name\" {\n  value = azurerm_key_vault_secret.key_vault_secret.name\n}\n\noutput \"key_name\" {\n  value = azurerm_key_vault_key.key_vault_key.name\n}\n\noutput \"certificate_name\" {\n  value = azurerm_key_vault_certificate.key_vault_certificate.name\n}"
  },
  {
    "path": "examples/azure/terraform-azure-keyvault-example/variables.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# ENVIRONMENT VARIABLES\n# Define these secrets as environment variables\n# ---------------------------------------------------------------------------------------------------------------------\n\n# ARM_CLIENT_ID\n# ARM_CLIENT_SECRET\n# ARM_SUBSCRIPTION_ID\n# ARM_TENANT_ID\n\n# ---------------------------------------------------------------------------------------------------------------------\n# REQUIRED PARAMETERS\n# You must provide a value for each of these parameters.\n# ---------------------------------------------------------------------------------------------------------------------\n\n# ---------------------------------------------------------------------------------------------------------------------\n# OPTIONAL PARAMETERS\n# These parameters have reasonable defaults.\n# ---------------------------------------------------------------------------------------------------------------------\n\nvariable \"location\" {\n  description = \"The location to set for the storage account.\"\n  type        = string\n  default     = \"East US\"\n}\n\nvariable \"secret_name\" {\n  description = \"The name to set for the key vault secret.\"\n  type        = string\n  default     = \"secret1\"\n}\n\nvariable \"key_name\" {\n  description = \"The name to set for the key vault key.\"\n  type        = string\n  default     = \"key1\"\n}\n\nvariable \"certificate_name\" {\n  description = \"The name to set for the key vault certificate.\"\n  type        = string\n  default     = \"certificate1\"\n}\n\nvariable \"postfix\" {\n  description = \"A postfix string to centrally mitigate resource name collisions\"\n  type        = string\n  default     = \"resource\"\n}\n\n\n"
  },
  {
    "path": "examples/azure/terraform-azure-loadbalancer-example/README.md",
    "content": "# Terraform Load Balancer Example\n\nThis folder contains a simple Terraform module that deploys resources in [Azure](https://azure.microsoft.com/) to demonstrate\nhow you can use Terratest to write automated tests for your Azure Terraform code. This module deploys two Load Balancers for Public and Private IP scenarios.\n\n- A Public [Load Balancer](https://docs.microsoft.com/azure/load-balancer/) that gives the module the following:\n\n  - `Load Balancer` with the name specified in the `lb_public_name` and configuration in the `lb_public_fe_config_name` output variables.\n\n- A Private [Load Balancer](https://docs.microsoft.com/azure/load-balancer/) that gives the module the following:\n\n  - `Load Balancer` with the name specified in the `lb_private_name` and static Frontend IP Configuration in the `lb_private_fe_config_static_name` output variables.\n\n- A Default [Load Balancer](https://docs.microsoft.com/azure/load-balancer/) that gives the module the following:\n\n  - `Load Balancer` with the name specified in the `lb_default_name` and no Frontend configuration.\n\n- Networking that provides the following for the Load Balancer module with the following:\n  - [Virtual Network](https://docs.microsoft.com/azure/virtual-network/) with the name specified in the `vnet_name` output variable.\n  - [Subnet](https://docs.microsoft.com/azure/virtual-network/virtual-network-manage-subnet) with the name specified in the `subnet_name` output variable.\n  - [Public IP Address](https://docs.microsoft.com/azure/virtual-network/virtual-network-public-ip-address) with the name specified in the `public_address_name` output variable.\n\nCheck out [test/azure/terraform_azure_loadbalancer_example_test.go](/test/azure/terraform_azure_loadbalancer_example_test.go) to see how you can write\nautomated tests for this module.\n\nNote that the Load Balancers and their associated resources in this module don't actually do anything; they are created before running the tests, for demonstration purposes.\n\n**WARNING**: This module and the automated tests for it deploy real resources into your Azure account which can cost you money. The resources are all part of the [Azure Free Account](https://azure.microsoft.com/free/), so if you haven't used that up, it should be free, but you are completely responsible for all Azure charges.\n\n## Running this module manually\n\n1. Sign up for [Azure](https://azure.microsoft.com/)\n1. Configure your Azure credentials using one of the [supported methods for Azure CLI\n   tools](https://docs.microsoft.com/cli/azure/azure-cli-configuration?view=azure-cli-latest)\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`\n1. Ensure [environment variables](../README.md#review-environment-variables) are available\n1. Run `terraform init`\n1. Run `terraform apply`\n1. When you're done, run `terraform destroy`\n\n## Running automated tests against this module\n\n1. Sign up for [Azure](https://azure.microsoft.com/)\n1. Configure your Azure credentials using one of the [supported methods for Azure CLI\n   tools](https://docs.microsoft.com/cli/azure/azure-cli-configuration?view=azure-cli-latest)\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`\n1. Configure your Terratest [Go test environment](../README.md)\n1. `cd test/azure`\n1. `go build terraform_azure_loadbalancer_example_test.go`\n1. `go test -v -run TestTerraformAzureLoadBalancerExample`\n"
  },
  {
    "path": "examples/azure/terraform-azure-loadbalancer-example/main.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY AN AZURE AVAILABILITY SET\n# This is an example of how to deploy an Azure Availability Set with a Virtual Machine in the availability set \n# and the minimum network resources for the VM.\n# ---------------------------------------------------------------------------------------------------------------------\n# See test/azure/terraform_azure_loadbalancer_example_test.go for how to write automated tests for this code.\n# ---------------------------------------------------------------------------------------------------------------------\nprovider \"azurerm\" {\n  features {}\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# PIN TERRAFORM VERSION TO >= 0.12\n# The examples have been upgraded to 0.12 syntax\n# ---------------------------------------------------------------------------------------------------------------------\n\nterraform {\n  # This module is now only being tested with Terraform 0.13.x. However, to make upgrading easier, we are setting\n  # 0.12.26 as the minimum version, as that version added support for required_providers with source URLs, making it\n  # forwards compatible with 0.13.x code.\n  required_version = \">= 0.12.26\"\n  required_providers {\n    azurerm = {\n      version = \"~>2.29\"\n      source  = \"hashicorp/azurerm\"\n    }\n  }\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY A RESOURCE GROUP\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"azurerm_resource_group\" \"lb_rg\" {\n  name     = \"terratest-lb-rg-${var.postfix}\"\n  location = var.location\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY VIRTUAL NETWORK\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"azurerm_virtual_network\" \"vnet\" {\n  name                = \"vnet-${var.postfix}\"\n  location            = azurerm_resource_group.lb_rg.location\n  resource_group_name = azurerm_resource_group.lb_rg.name\n  address_space       = [\"10.200.0.0/21\"]\n}\n\nresource \"azurerm_subnet\" \"subnet\" {\n  name                 = \"subnet-${var.postfix}\"\n  resource_group_name  = azurerm_resource_group.lb_rg.name\n  virtual_network_name = azurerm_virtual_network.vnet.name\n  address_prefixes     = [\"10.200.2.0/25\"]\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY LOAD BALANCER WITH PUBLIC IP \n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"azurerm_public_ip\" \"pip\" {\n  name                    = \"pip-${var.postfix}\"\n  location                = azurerm_resource_group.lb_rg.location\n  resource_group_name     = azurerm_resource_group.lb_rg.name\n  allocation_method       = \"Static\"\n  ip_version              = \"IPv4\"\n  sku                     = \"Basic\"\n  idle_timeout_in_minutes = \"4\"\n}\n\nresource \"azurerm_lb\" \"public\" {\n  name                = \"lb-public-${var.postfix}\"\n  location            = azurerm_resource_group.lb_rg.location\n  resource_group_name = azurerm_resource_group.lb_rg.name\n  sku                 = \"Basic\"\n\n  frontend_ip_configuration {\n    name                 = \"config-public\"\n    public_ip_address_id = azurerm_public_ip.pip.id\n  }\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY LOAD BALANCER WITH PRIVATE IPs \n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"azurerm_lb\" \"private\" {\n  name                = \"lb-private-${var.postfix}\"\n  location            = azurerm_resource_group.lb_rg.location\n  resource_group_name = azurerm_resource_group.lb_rg.name\n  sku                 = \"Basic\"\n\n  frontend_ip_configuration {\n    name                          = \"config-private-static\"\n    subnet_id                     = azurerm_subnet.subnet.id\n    private_ip_address            = var.lb_private_ip\n    private_ip_address_allocation = \"Static\"\n  }\n\n  frontend_ip_configuration {\n    name                          = \"config-private-dynamic\"\n    subnet_id                     = azurerm_subnet.subnet.id\n    private_ip_address_allocation = \"Dynamic\"\n  }\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY LOAD BALANCER WITH NO FRONTEND CONFIGURATION\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"azurerm_lb\" \"default\" {\n  name                = \"lb-no-frontend-${var.postfix}\"\n  location            = azurerm_resource_group.lb_rg.location\n  resource_group_name = azurerm_resource_group.lb_rg.name\n  sku                 = \"Basic\"\n}\n"
  },
  {
    "path": "examples/azure/terraform-azure-loadbalancer-example/outputs.tf",
    "content": "output \"lb_default_name\" {\n  value = azurerm_lb.default.name\n}\n\noutput \"lb_private_name\" {\n  value = azurerm_lb.private.name\n}\n\noutput \"lb_private_fe_config_static_name\" {\n  value = azurerm_lb.private.frontend_ip_configuration[0].name\n}\n\noutput \"lb_private_fe_config_dynamic_name\" {\n  value = azurerm_lb.private.frontend_ip_configuration[1].name\n}\n\noutput \"lb_private_ip_static\" {\n  value = azurerm_lb.private.frontend_ip_configuration[0].private_ip_address\n}\n\noutput \"lb_private_ip_dynamic\" {\n  value = azurerm_lb.private.frontend_ip_configuration[1].private_ip_address\n}\n\noutput \"lb_public_name\" {\n  value = azurerm_lb.public.name\n}\n\noutput \"lb_public_fe_config_name\" {\n  value = azurerm_lb.public.frontend_ip_configuration[0].name\n}\n\noutput \"public_address_name\" {\n  value = azurerm_public_ip.pip.name\n}\n\noutput \"resource_group_name\" {\n  value = azurerm_resource_group.lb_rg.name\n}\n"
  },
  {
    "path": "examples/azure/terraform-azure-loadbalancer-example/variables.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# ENVIRONMENT VARIABLES\n# Define these secrets as environment variables\n# ---------------------------------------------------------------------------------------------------------------------\n\n# ARM_CLIENT_ID\n# ARM_CLIENT_SECRET\n# ARM_SUBSCRIPTION_ID\n# ARM_TENANT_ID\n\n# ---------------------------------------------------------------------------------------------------------------------\n# REQUIRED PARAMETERS\n# You must provide a value for each of these parameters.\n# ---------------------------------------------------------------------------------------------------------------------\n\n# ---------------------------------------------------------------------------------------------------------------------\n# OPTIONAL PARAMETERS\n# These parameters have reasonable defaults.\n# ---------------------------------------------------------------------------------------------------------------------\n\n\nvariable \"lb_private_ip\" {\n  description = \"Private IP for the Private Load Balancer\"\n  type        = string\n  default     = \"10.200.2.10\"\n}\n\nvariable \"location\" {\n  description = \"The Azure location where to deploy your resources too\"\n  type        = string\n  default     = \"East US\"\n}\n\nvariable \"postfix\" {\n  description = \"A postfix string to centrally mitigate resource name collisions\"\n  type        = string\n  default     = \"resource\"\n}\n"
  },
  {
    "path": "examples/azure/terraform-azure-loganalytics-example/README.md",
    "content": "# Terraform Azure Log Analytics Example\n\nThis folder contains a Terraform module that deploys resources in [Azure](https://azure.microsoft.com/) to demonstrate\nhow you can use TerraTest to write automated tests for your Azure Terraform code. This module deploys a Log Analytics Workspace.\n\n- A [Log Analytics Workspace](https://docs.microsoft.com/azure/azure-monitor/platform/log-analytics-agent) that gives the module the following:\n  - [Name](https://docs.microsoft.com/azure/azure-monitor/learn/quick-create-workspace#:~:text=%20Create%20a%20Log%20Analytics%20workspace%20in%20the,and%20region%20as%20in%20the%20deleted...%20More%20)  with the value specified in the `loganalytics_workspace_name`  output variable.\n  - [Sku](https://docs.microsoft.com/azure/azure-monitor/learn/quick-create-workspace#:~:text=%20Create%20a%20Log%20Analytics%20workspace%20in%20the,and%20region%20as%20in%20the%20deleted...%20More%20)  with the value specified in the `loganalytics_workspace_sku`  output variable.\n  - [RetentionPeriodInDays](https://docs.microsoft.com/azure/azure-monitor/learn/quick-create-workspace#:~:text=%20Create%20a%20Log%20Analytics%20workspace%20in%20the,and%20region%20as%20in%20the%20deleted...%20More%20)  with the value specified in the `loganalytics_workspace_retention`  output variable.\n\nCheck out [test/azure/terraform_azure_loganalytics_example_test.go](/test/azure/terraform_azure_loganalytics_example_test.go) to see how you can write\nautomated tests for this module.\n\nNote that the Log Analytics Workspace in this module don't actually do anything; it just runs the resources for\ndemonstration purposes.\n\n**WARNING**: This module and the automated tests for it deploy real resources into your Azure account which can cost you\nmoney. The resources are all part of the [Azure Free Account](https://azure.microsoft.com/free/), so if you haven't used that up,\nit should be free, but you are completely responsible for all Azure charges.\n\n## Running this module manually\n\n1. Sign up for [Azure](https://azure.microsoft.com/)\n1. Configure your Azure credentials using one of the [supported methods for Azure CLI\n   tools](https://docs.microsoft.com/cli/azure/azure-cli-configuration?view=azure-cli-latest)\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`\n1. Ensure [environment variables](../README.md#review-environment-variables) are available\n1. Run `terraform init`\n1. Run `terraform apply`\n1. When you're done, run `terraform destroy`\n\n## Running automated tests against this module\n\n1. Sign up for [Azure](https://azure.microsoft.com/)\n1. Configure your Azure credentials using one of the [supported methods for Azure CLI\n   tools](https://docs.microsoft.com/cli/azure/azure-cli-configuration?view=azure-cli-latest)\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`\n1. Configure your TerraTest [Go test environment](../README.md)\n1. `cd test/azure`\n1. `go build terraform_azure_loganalytics_example_test.go`\n1. `go test -v -run TestTerraformAzureLogAnalyticsExample`\n"
  },
  {
    "path": "examples/azure/terraform-azure-loganalytics-example/main.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY LOG ANALYTICS\n# This is an example of how to deploy a Log Analytics workspace resource.\n# ---------------------------------------------------------------------------------------------------------------------\n# See test/azure/terraform_azure_loganalytics_example_test.go for how to write automated tests for this code.\n# ---------------------------------------------------------------------------------------------------------------------\n\nprovider \"azurerm\" {\n  features {}\n}\n\n# PIN TERRAFORM VERSION\n\nterraform {\n  # This module is now only being tested with Terraform 0.13.x. However, to make upgrading easier, we are setting\n  # 0.12.26 as the minimum version, as that version added support for required_providers with source URLs, making it\n  # forwards compatible with 0.13.x code.\n  required_version = \">= 0.12.26\"\n  required_providers {\n    azurerm = {\n      version = \"~> 2.20\"\n      source  = \"hashicorp/azurerm\"\n    }\n  }\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY A RESOURCE GROUP\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"azurerm_resource_group\" \"resource_group\" {\n  name     = \"terratest-log-rg-${var.postfix}\"\n  location = var.location\n}\n\nresource \"azurerm_log_analytics_workspace\" \"log_analytics_workspace\" {\n  name                = \"log-ws-${var.postfix}\"\n  location            = azurerm_resource_group.resource_group.location\n  resource_group_name = azurerm_resource_group.resource_group.name\n  sku                 = \"PerGB2018\"\n  retention_in_days   = 30\n}"
  },
  {
    "path": "examples/azure/terraform-azure-loganalytics-example/outputs.tf",
    "content": "output \"resource_group_name\" {\n  value = azurerm_resource_group.resource_group.name\n}\n\noutput \"loganalytics_workspace_name\" {\n  value = azurerm_log_analytics_workspace.log_analytics_workspace.name\n}\n\noutput \"loganalytics_workspace_sku\" {\n  value = azurerm_log_analytics_workspace.log_analytics_workspace.sku\n}\n\noutput \"loganalytics_workspace_retention\" {\n  value = azurerm_log_analytics_workspace.log_analytics_workspace.retention_in_days\n}\n\n"
  },
  {
    "path": "examples/azure/terraform-azure-loganalytics-example/variables.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# ENVIRONMENT VARIABLES\n# Define these secrets as environment variables\n# ---------------------------------------------------------------------------------------------------------------------\n\n# ARM_CLIENT_ID\n# ARM_CLIENT_SECRET\n# ARM_SUBSCRIPTION_ID\n# ARM_TENANT_ID\n\n# ---------------------------------------------------------------------------------------------------------------------\n# REQUIRED PARAMETERS\n# You must provide a value for each of these parameters.\n# ---------------------------------------------------------------------------------------------------------------------\n\n# ---------------------------------------------------------------------------------------------------------------------\n# OPTIONAL PARAMETERS\n# These parameters have reasonable defaults.\n# ---------------------------------------------------------------------------------------------------------------------\n\n\nvariable \"location\" {\n  description = \"The location to set for the storage account.\"\n  type        = string\n  default     = \"East US\"\n}\n\nvariable \"postfix\" {\n  description = \"A postfix string to centrally mitigate resource name collisions\"\n  type        = string\n  default     = \"resource\"\n}\n"
  },
  {
    "path": "examples/azure/terraform-azure-monitor-example/README.md",
    "content": "# Terraform Azure Monitor Example\n\nThis folder contains a simple Terraform module that deploys resources in [Azure](https://azure.microsoft.com/) to demonstrate\nhow you can use Terratest to write automated tests for your Azure Terraform code. This module deploys an Azure Storage Account and Azure Azure Key Vault with an Azure Monitor Diagnostic Setting.\n\n- A [Diagnostic Setting](https://docs.microsoft.com/azure/azure-monitor/platform/diagnostic-settings-template)\n\nCheck out [test/azure/terraform_azure_monitor_example_test.go](/test/azure/terraform_azure_monitor_example_test.go) to see how you can write\nautomated tests for this module.\n\nNote that the resources deployed in this module don't actually do anything; it just runs the resources for\ndemonstration purposes.\n\n**WARNING**: This module and the automated tests for it deploy real resources into your Azure account which can cost you\nmoney. The resources are all part of the [Azure Free Account](https://azure.microsoft.com/free/), so if you haven't used that up,\nit should be free, but you are completely responsible for all Azure charges.\n\n## Running this module manually\n\n1. Sign up for [Azure](https://azure.microsoft.com/)\n1. Configure your Azure credentials using one of the [supported methods for Azure CLI\n   tools](https://docs.microsoft.com/cli/azure/azure-cli-configuration?view=azure-cli-latest)\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`\n1. Ensure [environment variables](../README.md#review-environment-variables) are available\n1. Run `terraform init`\n1. Run `terraform apply`\n1. When you're done, run `terraform destroy`\n\n## Running automated tests against this module\n\n1. Sign up for [Azure](https://azure.microsoft.com/).\n1. Configure your Azure credentials using one of the [supported methods for Azure CLI\n   tools](https://docs.microsoft.com/cli/azure/azure-cli-configuration?view=azure-cli-latest).\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`.\n1. [Review environment variables](#review-environment-variables).\n1. Install [Golang](https://golang.org/) and make sure this code is checked out into your `GOPATH`.\n1. `cd test`\n1. Make sure [the azure-sdk-for-go versions match](#check-go-dependencies) in [/go.mod](/go.mod) and in [test/azure/terraform_azure_monitor_example_test.go](/test/azure/terraform_azure_monitor_example_test.go).\n1. `go build test/azure/terraform_azure_monitor_example_test.go`\n1. `go test -v -run TestTerraformAzureMonitorExample`\n"
  },
  {
    "path": "examples/azure/terraform-azure-monitor-example/main.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY AN AZURE MONITOR DIAGNOSTIC SETTING\n# This is an example of how to deploy an Azure Monitor Diagnostic Setting\n# for a key vault with a storage account.\n# ---------------------------------------------------------------------------------------------------------------------\n# See test/azure/terraform_azure_monitor_example_test.go for how to write automated tests for this code.\n# ---------------------------------------------------------------------------------------------------------------------\n\nprovider \"azurerm\" {\n  features {\n    key_vault {\n      purge_soft_delete_on_destroy = true\n    }\n  }\n}\n\nterraform {\n  # This module is now only being tested with Terraform 0.13.x. However, to make upgrading easier, we are setting\n  # 0.12.26 as the minimum version, as that version added support for required_providers with source URLs, making it\n  # forwards compatible with 0.13.x code.\n  required_version = \">= 0.12.26\"\n  required_providers {\n    azurerm = {\n      version = \"~> 2.29\"\n      source  = \"hashicorp/azurerm\"\n    }\n    azuread = {\n      version = \"=0.7.0\"\n      source  = \"hashicorp/azuread\"\n    }\n  }\n}\n\nresource \"random_string\" \"short\" {\n  length  = 3\n  lower   = true\n  upper   = false\n  number  = false\n  special = false\n}\n\nresource \"random_string\" \"long\" {\n  length  = 6\n  lower   = true\n  upper   = false\n  number  = false\n  special = false\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY A RESOURCE GROUP\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"azurerm_resource_group\" \"monitor\" {\n  name     = \"terratest-monitor-rg-${var.postfix}\"\n  location = var.location\n}\n\ndata \"azurerm_client_config\" \"current\" {}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY A STORAGE ACCOUNT\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"azurerm_storage_account\" \"monitor\" {\n  name                     = format(\"%s%s\", \"storage\", random_string.long.result)\n  resource_group_name      = azurerm_resource_group.monitor.name\n  location                 = azurerm_resource_group.monitor.location\n  account_tier             = \"Standard\"\n  account_replication_type = \"GRS\"\n\n  tags = {\n    environment = \"staging\"\n  }\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY A KEY VAULT\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"azurerm_key_vault\" \"monitor\" {\n  name                        = \"kv-${var.postfix}\"\n  location                    = azurerm_resource_group.monitor.location\n  resource_group_name         = azurerm_resource_group.monitor.name\n  enabled_for_disk_encryption = true\n  tenant_id                   = data.azurerm_client_config.current.tenant_id\n  soft_delete_enabled         = true\n  purge_protection_enabled    = false\n\n  sku_name = \"standard\"\n\n  access_policy {\n    tenant_id = data.azurerm_client_config.current.tenant_id\n    object_id = data.azurerm_client_config.current.object_id\n\n    key_permissions = [\n      \"create\",\n      \"get\",\n      \"list\",\n      \"delete\",\n    ]\n\n    secret_permissions = [\n      \"set\",\n      \"get\",\n      \"list\",\n      \"delete\",\n    ]\n\n    certificate_permissions = [\n      \"create\",\n      \"delete\",\n      \"deleteissuers\",\n      \"get\",\n      \"getissuers\",\n      \"import\",\n      \"list\",\n      \"listissuers\",\n      \"managecontacts\",\n      \"manageissuers\",\n      \"setissuers\",\n      \"update\",\n    ]\n  }\n\n  network_acls {\n    default_action = \"Deny\"\n    bypass         = \"AzureServices\"\n  }\n\n  tags = {\n    environment = \"Testing\"\n  }\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY A DIAGNOSTIC SETTING\n# https://www.terraform.io/docs/providers/azurerm/r/monitor_diagnostic_setting.html\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"azurerm_monitor_diagnostic_setting\" \"monitor\" {\n  name               = var.diagnosticSettingName\n  target_resource_id = azurerm_key_vault.monitor.id\n  storage_account_id = azurerm_storage_account.monitor.id\n\n  log {\n    category = \"AuditEvent\"\n    enabled  = false\n\n    retention_policy {\n      enabled = false\n    }\n  }\n\n  metric {\n    category = \"AllMetrics\"\n\n    retention_policy {\n      enabled = false\n    }\n  }\n}\n"
  },
  {
    "path": "examples/azure/terraform-azure-monitor-example/outputs.tf",
    "content": "output \"resource_group_name\" {\n  value = azurerm_resource_group.monitor.name\n}\n\noutput \"diagnostic_setting_name\" {\n  value = azurerm_monitor_diagnostic_setting.monitor.name\n}\n\noutput \"diagnostic_setting_id\" {\n  value = azurerm_monitor_diagnostic_setting.monitor.id\n}\n\noutput \"keyvault_id\" {\n  value = azurerm_key_vault.monitor.id\n}\n"
  },
  {
    "path": "examples/azure/terraform-azure-monitor-example/variables.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# ENVIRONMENT VARIABLES\n# Define these secrets as environment variables\n# ---------------------------------------------------------------------------------------------------------------------\n\n# ARM_CLIENT_ID\n# ARM_CLIENT_SECRET\n# ARM_SUBSCRIPTION_ID\n# ARM_TENANT_ID\n\n# ---------------------------------------------------------------------------------------------------------------------\n# REQUIRED PARAMETERS\n# You must provide a value for each of these parameters.\n# ---------------------------------------------------------------------------------------------------------------------\n\n# ---------------------------------------------------------------------------------------------------------------------\n# OPTIONAL PARAMETERS\n# These parameters have reasonable defaults.\n# ---------------------------------------------------------------------------------------------------------------------\n\nvariable \"postfix\" {\n  description = \"A postfix string to centrally mitigate resource name collisions\"\n  type        = string\n  default     = \"resource\"\n}\n\nvariable \"diagnosticSettingName\" {\n  description = \"The diagnostic setting name\"\n  type        = string\n  default     = \"diag-test\"\n}\n\nvariable \"location\" {\n  description = \"The Azure region in which to deploy your resources to\"\n  type        = string\n  default     = \"East US\"\n}\n"
  },
  {
    "path": "examples/azure/terraform-azure-mysqldb-example/README.md",
    "content": "# Terraform Azure MySQL DB Example\n\nThis folder contains a Terraform module that deploys resources in [Azure](https://azure.microsoft.com/) to demonstrate how you can use Terratest to write automated tests for your Azure Terraform code.\nThis module deploys a database for MySQL.\n\n- A [Azure MySQL Database](https://azure.microsoft.com/services/mysql/).\n\nCheck out [test/azure/terraform_azure_mysqldb_example_test.go](./../../../test/azure/terraform_azure_mysqldb_example_test.go) to see how you can write automated tests for this module and validate the configuration of the parameters and options. \n\n**WARNING**: This module and the automated tests for it deploy real resources into your Azure account which can cost you money. \n\n## Running this module manually\n1. Sign up for [Azure](https://azure.microsoft.com/).\n1. Configure your Azure credentials using one of the [supported methods for Azure CLI\n   tools](https://docs.microsoft.com/en-us/cli/azure/azure-cli-configuration?view=azure-cli-latest)\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`.\n1. Ensure [environment variables](../README.md#review-environment-variables) are available\n1. Run `terraform init`\n1. Run `terraform apply`\n1. When you're done, run `terraform destroy`.\n\n\n## Running automated tests against this module\n1. Sign up for [Azure](https://azure.microsoft.com/)\n1. Configure your Azure credentials using one of the [supported methods for Azure CLI\n   tools](https://docs.microsoft.com/en-us/cli/azure/azure-cli-configuration?view=azure-cli-latest)\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`\n1. Configure your Terratest [Go test environment](../README.md) \n1. `cd test/azure`\n1. `go build terraform_azure_mysqldb_example_test.go`\n1. `go test -v -timeout 60m -tags azure -run TestTerraformAzureMySQLDBExample`\n\n"
  },
  {
    "path": "examples/azure/terraform-azure-mysqldb-example/main.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY AN AZURE MySQL Database\n# This is an example of how to deploy an Azure Mysql database.\n# See test/terraform_azure_example_test.go for how to write automated tests for this code.\n# ---------------------------------------------------------------------------------------------------------------------\n\n\n# ---------------------------------------------------------------------------------------------------------------------\n# CONFIGURE OUR AZURE CONNECTION\n# ---------------------------------------------------------------------------------------------------------------------\nterraform {\n  # This module is now only being tested with Terraform 0.13.x. However, to make upgrading easier, we are setting\n  # 0.12.26 as the minimum version, as that version added support for required_providers with source URLs, making it\n  # forwards compatible with 0.13.x code.\n  required_providers {\n    azurerm = {\n      version = \"~>2.29.0\"\n      source  = \"hashicorp/azurerm\"\n    }\n  }\n}\n\nprovider \"azurerm\" {\n  features {}\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY A RESOURCE GROUP\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"azurerm_resource_group\" \"mysql_rg\" {\n  name     = \"terratest-mysql-${var.postfix}\"\n  location = var.location\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY AZURE MySQL SERVER\n# ---------------------------------------------------------------------------------------------------------------------\n\n# Random password is used as an example to simplify the deployment and improve the security of the database.\n# This is not as a production recommendation as the password is stored in the Terraform state file.\nresource \"random_password\" \"password\" {\n  length           = 16\n  override_special = \"_%@\"\n  min_upper        = \"1\"\n  min_lower        = \"1\"\n  min_numeric      = \"1\"\n  min_special      = \"1\"\n}\n\nresource \"azurerm_mysql_server\" \"mysqlserver\" {\n  name                = \"mysqlserver-${var.postfix}\"\n  location            = azurerm_resource_group.mysql_rg.location\n  resource_group_name = azurerm_resource_group.mysql_rg.name\n\n  administrator_login          = var.mysqlserver_admin_login\n  administrator_login_password = random_password.password.result\n\n  sku_name   = var.mysqlserver_sku_name\n  storage_mb = var.mysqlserver_storage_mb\n  version    = \"5.7\"\n\n  auto_grow_enabled                 = true\n  geo_redundant_backup_enabled      = false\n  infrastructure_encryption_enabled = true\n  backup_retention_days             = 7\n  public_network_access_enabled     = false\n  ssl_enforcement_enabled           = true\n  ssl_minimal_tls_version_enforced  = \"TLS1_2\"\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY AZURE MySQL DATABASE\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"azurerm_mysql_database\" \"mysqldb\" {\n  name                = \"mysqldb-${var.postfix}\"\n  resource_group_name = azurerm_resource_group.mysql_rg.name\n  server_name         = azurerm_mysql_server.mysqlserver.name\n  charset             = var.mysqldb_charset\n  collation           = var.mysqldb_collation\n}\n"
  },
  {
    "path": "examples/azure/terraform-azure-mysqldb-example/outputs.tf",
    "content": "output \"resource_group_name\" {\n  value = azurerm_resource_group.mysql_rg.name\n}\n\noutput \"mysql_server_name\" {\n  value = azurerm_mysql_server.mysqlserver.name\n}\n\noutput \"sql_server_full_domain_name\" {\n  value = azurerm_mysql_server.mysqlserver.fqdn\n}\n\noutput \"sql_server_admin_login\" {\n  value = azurerm_mysql_server.mysqlserver.administrator_login\n}\n\noutput \"sql_server_admin_login_pass\" {\n  value     = azurerm_mysql_server.mysqlserver.administrator_login_password\n  sensitive = true\n}\n\noutput \"mysql_database_name\" {\n  value = azurerm_mysql_database.mysqldb.name\n}\n"
  },
  {
    "path": "examples/azure/terraform-azure-mysqldb-example/variables.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# ENVIRONMENT VARIABLES\n# Define these secrets as environment variables\n# ---------------------------------------------------------------------------------------------------------------------\n\n# ARM_CLIENT_ID\n# ARM_CLIENT_SECRET\n# ARM_SUBSCRIPTION_ID\n# ARM_TENANT_ID\n\n# ---------------------------------------------------------------------------------------------------------------------\n# REQUIRED PARAMETERS\n# You must provide a value for each of these parameters.\n# ---------------------------------------------------------------------------------------------------------------------\n\n# ---------------------------------------------------------------------------------------------------------------------\n# OPTIONAL PARAMETERS\n# These parameters have reasonable defaults.\n# ---------------------------------------------------------------------------------------------------------------------\n\nvariable \"location\" {\n  description = \"The supported azure location where the resource exists\"\n  type        = string\n  default     = \"West US2\"\n}\n\nvariable \"mysqlserver_admin_login\" {\n  description = \"The administrator login name for the mysql server.\"\n  type        = string\n  default     = \"mysqladmin\"\n}\n\nvariable \"mysqlserver_sku_name\" {\n  description = \"The SKU name for the mysql server.\"\n  type        = string\n  default     = \"GP_Gen5_2\"\n}\n\nvariable \"mysqlserver_storage_mb\" {\n  description = \"The Max storage allowed for mysql server.\"\n  type        = string\n  default     = \"5120\"\n}\n\nvariable \"mysqldb_charset\" {\n  description = \"The charset for mysql data base.\"\n  type        = string\n  default     = \"utf8\"\n}\n\nvariable \"mysqldb_collation\" {\n  description = \"The collation for mysql data base.\"\n  type        = string\n  default     = \"utf8_unicode_ci\"\n}\n\nvariable \"postfix\" {\n  description = \"A postfix string to centrally mitigate resource name collisions.\"\n  type        = string\n  default     = \"resource\"\n}\n"
  },
  {
    "path": "examples/azure/terraform-azure-network-example/README.md",
    "content": "# Terraform Azure Network Example\n\nThis folder contains a simple Terraform module that deploys resources in [Azure](https://azure.microsoft.com/) to demonstrate\nhow you can use Terratest to write automated tests for your Azure Terraform code. This module deploys to a Virtual Network two Network Interface Cards, one with an internal only IP and another with an internal and external Public IP.\n\n- A [Virtual Network](https://azure.microsoft.com/en-us/services/virtual-network/) module that includes the following resources:\n  - [Virtual Network](https://docs.microsoft.com/en-us/azure/virtual-network/) with the name specified in the `virtual_network_name` variable.\n  - [Subnet](https://docs.microsoft.com/en-us/rest/api/virtualnetwork/subnets) with the name specified in the `subnet_name` variable.\n  - [Public Address](https://docs.microsoft.com/en-us/azure/virtual-network/public-ip-addresses) with the name specified in the `public_ip_name` variable.\n  - [Internal Network Interface](https://docs.microsoft.com/en-us/azure/virtual-network/virtual-network-network-interface) with the name specified in the `network_interface_internal` variable.\n  - [ExternalNetwork Interface](https://docs.microsoft.com/en-us/azure/virtual-network/virtual-network-network-interface) with the name specified in the `network_interface_external` variable.\n\nCheck out [test/azure/terraform_azure_network_example_test.go](/test/azure/terraform_azure_network_example_test.go) to see how you can write\nautomated tests for this module.\n\nNote that the Azure Virtual Network, Subnet, Network Interface and Public IP resources in this module don't actually do anything; it just runs the resources for\ndemonstration purposes.\n\n**WARNING**: This module and the automated tests for it deploy real resources into your Azure account which can cost you\nmoney. The resources are all part of the [Azure Free Account](https://azure.microsoft.com/en-us/free/), so if you haven't used that up,\nit should be free, but you are completely responsible for all Azure charges.\n\n## Running this module manually\n\n1. Sign up for [Azure](https://azure.microsoft.com/)\n1. Configure your Azure credentials using one of the [supported methods for Azure CLI\n   tools](https://docs.microsoft.com/en-us/cli/azure/azure-cli-configuration?view=azure-cli-latest)\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`\n1. Ensure [environment variables](../README.md#review-environment-variables) are available\n1. Run `terraform init`\n1. Run `terraform apply`\n1. When you're done, run `terraform destroy`\n\n## Running automated tests against this module\n\n1. Sign up for [Azure](https://azure.microsoft.com/)\n1. Configure your Azure credentials using one of the [supported methods for Azure CLI\n   tools](https://docs.microsoft.com/en-us/cli/azure/azure-cli-configuration?view=azure-cli-latest)\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`\n1. Configure your Terratest [Go test environment](../README.md)\n1. `cd test/azure`\n1. `go build terraform_azure_network_example_test.go`\n1. `go test -v -run TestTerraformAzureNetworkExample`\n"
  },
  {
    "path": "examples/azure/terraform-azure-network-example/main.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY AN AZURE NETWORK\n# This is an example of how to deploy frequent Azure Networking Resources. Note this network doesn't actually do\n# anything and is only created for the example to test their commonly needed and integrated properties.\n# ---------------------------------------------------------------------------------------------------------------------\n# See test/azure/terraform_azure_network_example_test.go for how to write automated tests for this code.\n# ---------------------------------------------------------------------------------------------------------------------\n\nprovider \"azurerm\" {\n  features {}\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# PIN TERRAFORM VERSION TO >= 0.12\n# The examples have been upgraded to 0.12 syntax\n# ---------------------------------------------------------------------------------------------------------------------\n\nterraform {\n  # This module is now only being tested with Terraform 0.13.x. However, to make upgrading easier, we are setting\n  # 0.12.26 as the minimum version, as that version added support for required_providers with source URLs, making it\n  # forwards compatible with 0.13.x code.\n  required_version = \">= 0.12.26\"\n  required_providers {\n    azurerm = {\n      version = \"~>2.20\"\n      source  = \"hashicorp/azurerm\"\n    }\n  }\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY A RESOURCE GROUP\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"azurerm_resource_group\" \"net\" {\n  name     = \"terratest-network-rg-${var.postfix}\"\n  location = var.location\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY VIRTUAL NETWORK\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"azurerm_virtual_network\" \"net\" {\n  name                = \"vnet-${var.postfix}\"\n  location            = azurerm_resource_group.net.location\n  resource_group_name = azurerm_resource_group.net.name\n  address_space       = [\"10.0.0.0/16\"]\n  dns_servers         = [var.dns_ip_01, var.dns_ip_02]\n}\n\nresource \"azurerm_subnet\" \"net\" {\n  name                 = \"subnet-${var.postfix}\"\n  resource_group_name  = azurerm_resource_group.net.name\n  virtual_network_name = azurerm_virtual_network.net.name\n  address_prefixes     = [var.subnet_prefix]\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY PRIVATE NETWORK INTERFACE\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"azurerm_network_interface\" \"net01\" {\n  name                = \"nic-private-${var.postfix}\"\n  location            = azurerm_resource_group.net.location\n  resource_group_name = azurerm_resource_group.net.name\n\n  ip_configuration {\n    name                          = \"terratestconfiguration1\"\n    subnet_id                     = azurerm_subnet.net.id\n    private_ip_address_allocation = \"Static\"\n    private_ip_address            = var.private_ip\n  }\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY PUBLIC ADDRESS AND NETWORK INTERFACE\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"azurerm_public_ip\" \"net\" {\n  name                    = \"pip-${var.postfix}\"\n  resource_group_name     = azurerm_resource_group.net.name\n  location                = azurerm_resource_group.net.location\n  allocation_method       = \"Static\"\n  ip_version              = \"IPv4\"\n  sku                     = \"Basic\"\n  idle_timeout_in_minutes = \"4\"\n  domain_name_label       = var.domain_name_label\n}\n\nresource \"azurerm_network_interface\" \"net02\" {\n  name                = \"nic-public-${var.postfix}\"\n  location            = azurerm_resource_group.net.location\n  resource_group_name = azurerm_resource_group.net.name\n\n  ip_configuration {\n    name                          = \"terratestconfiguration1\"\n    subnet_id                     = azurerm_subnet.net.id\n    private_ip_address_allocation = \"Dynamic\"\n    public_ip_address_id          = azurerm_public_ip.net.id\n  }\n}\n\n"
  },
  {
    "path": "examples/azure/terraform-azure-network-example/outputs.tf",
    "content": "output \"resource_group_name\" {\n  value = azurerm_resource_group.net.name\n}\n\noutput \"virtual_network_name\" {\n  value = azurerm_virtual_network.net.name\n}\n\noutput \"subnet_name\" {\n  value = azurerm_subnet.net.name\n}\n\noutput \"public_address_name\" {\n  value = azurerm_public_ip.net.name\n}\n\noutput \"network_interface_internal\" {\n  value = azurerm_network_interface.net01.name\n}\n\noutput \"network_interface_external\" {\n  value = azurerm_network_interface.net02.name\n}\n\n\n"
  },
  {
    "path": "examples/azure/terraform-azure-network-example/variables.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# ENVIRONMENT VARIABLES\n# Define these secrets as environment variables\n# ---------------------------------------------------------------------------------------------------------------------\n\n# ARM_CLIENT_ID\n# ARM_CLIENT_SECRET\n# ARM_SUBSCRIPTION_ID\n# ARM_TENANT_ID\n\n# ---------------------------------------------------------------------------------------------------------------------\n# REQUIRED PARAMETERS\n# You must provide a value for each of these parameters.\n# ---------------------------------------------------------------------------------------------------------------------\n\nvariable \"domain_name_label\" {\n  description = \"The Domain Name Label for the Public IP Address\"\n  type        = string\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# OPTIONAL PARAMETERS\n# These parameters have reasonable defaults.\n# ---------------------------------------------------------------------------------------------------------------------\n\nvariable \"dns_ip_01\" {\n  description = \"The first DNS Server IP for the Virtual Network\"\n  type        = string\n  default     = \"10.0.0.5\"\n}\n\nvariable \"dns_ip_02\" {\n  description = \"The second DNS Server IP for the Virtual Network\"\n  type        = string\n  default     = \"10.0.0.6\"\n}\n\nvariable \"location\" {\n  description = \"The Azure Region to deploy resources too\"\n  type        = string\n  default     = \"East US\"\n}\n\nvariable \"postfix\" {\n  description = \"The postfix that will be attached to all resources deployed\"\n  type        = string\n  default     = \"resource\"\n}\n\nvariable \"private_ip\" {\n  description = \"The Static Private IP for the Internal NIC\"\n  type        = string\n  default     = \"10.0.20.5\"\n}\n\nvariable \"subnet_prefix\" {\n  description = \"The subnet range of IPs for the Virtual Network\"\n  type        = string\n  default     = \"10.0.20.0/24\"\n}\n"
  },
  {
    "path": "examples/azure/terraform-azure-nsg-example/README.md",
    "content": "# Terraform Azure NSG Example\n\nThis folder contains a simple Terraform module that deploys resources in [Azure](https://azure.microsoft.com/) to demonstrate how you can use Terratest to write automated tests for your Azure Terraform code. This module deploys the following:\n\n* A [Virtual Machine](https://azure.microsoft.com/en-us/services/virtual-machines/) that gives the module the following:\n    * [Virtual Machine](https://docs.microsoft.com/en-us/azure/virtual-machines/) with the value specified in the `vm_name` variable along with a random value for the `postfix` variable (set from test code).\n    * A [Network Security Group](https://docs.microsoft.com/en-us/azure/virtual-network/network-security-groups-overview) created with a single custom rule to allow SSH (port 22) with the nsg name specified in the `nsg_name` variable along with a random value for the `postfix` variable (set from test code).\n\nCheck out [test/azure/terraform_azure_nsg_example_test.go](/test/azure/terraform_azure_nsg_example_test.go) to see how you can write\nautomated tests for this module.\n\nNote that the resources deployed in this module don't actually do anything; it just runs the resources for\ndemonstration purposes.\n\n**WARNING**: This module and the automated tests for it deploy real resources into your Azure account which can cost you\nmoney. The resources are all part of the [Azure Free Account](https://azure.microsoft.com/en-us/free/), so if you haven't used that up,\nit should be free, but you are completely responsible for all Azure charges.\n\n## Running this module manually\n\n1. Sign up for [Azure](https://azure.microsoft.com/)\n1. Configure your Azure credentials using one of the [supported methods for Azure CLI\n   tools](https://docs.microsoft.com/en-us/cli/azure/azure-cli-configuration?view=azure-cli-latest)\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`\n1. Ensure [environment variables](../README.md#review-environment-variables) are available\n1. Run `terraform init`\n1. Run `terraform apply`\n1. When you're done, run `terraform destroy`\n\n## Running automated tests against this module\n\n1. Sign up for [Azure](https://azure.microsoft.com/)\n1. Configure your Azure credentials using one of the [supported methods for Azure CLI\n   tools](https://docs.microsoft.com/en-us/cli/azure/azure-cli-configuration?view=azure-cli-latest)\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`\n1. Configure your Terratest [Go test environment](../README.md)\n1. `cd test/azure`\n1. `go build terraform_azure_nsg_example_test.go`\n1. `go test -v -run TestTerraformAzureNsgExample`\n"
  },
  {
    "path": "examples/azure/terraform-azure-nsg-example/main.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY AN AZURE VM ALONG WITH AN EXAMPLE NETWORK SECURITY GROUP (NSG)\n# This is an example of how to deploy an NSG along with the minimum networking resources\n# to support a basic virtual machine.\n# ---------------------------------------------------------------------------------------------------------------------\n# See test/azure/terraform_azure_nsg_example_test.go for how to write automated tests for this code.\n# ---------------------------------------------------------------------------------------------------------------------\n\nprovider \"azurerm\" {\n  features {}\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# PIN TERRAFORM VERSION TO >= 0.12\n# The examples have been upgraded to 0.12 syntax\n# ---------------------------------------------------------------------------------------------------------------------\n\nterraform {\n  required_version = \">= 0.12\"\n  required_providers {\n    azurerm = {\n      version = \"~> 2.50\"\n      source  = \"hashicorp/azurerm\"\n    }\n  }\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY A RESOURCE GROUP\n# See test/terraform_azure_nsg_example_test.go for how to write automated tests for this code.\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"azurerm_resource_group\" \"nsg_rg\" {\n  name     = \"${var.resource_group_name}-${var.postfix}\"\n  location = var.location\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY VIRTUAL NETWORK RESOURCES\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"azurerm_virtual_network\" \"vnet\" {\n  name                = \"${var.vnet_name}-${var.postfix}\"\n  address_space       = [\"10.0.0.0/16\"]\n  location            = azurerm_resource_group.nsg_rg.location\n  resource_group_name = azurerm_resource_group.nsg_rg.name\n}\n\nresource \"azurerm_subnet\" \"internal\" {\n  name                 = \"${var.subnet_name}-${var.postfix}\"\n  resource_group_name  = azurerm_resource_group.nsg_rg.name\n  virtual_network_name = azurerm_virtual_network.vnet.name\n  address_prefixes     = [\"10.0.17.0/24\"]\n}\n\nresource \"azurerm_network_interface\" \"main\" {\n  name                = \"${var.vm_nic_name}-${var.postfix}\"\n  location            = azurerm_resource_group.nsg_rg.location\n  resource_group_name = azurerm_resource_group.nsg_rg.name\n\n  ip_configuration {\n    name                          = \"${var.vm_nic_ip_config_name}-${var.postfix}\"\n    subnet_id                     = azurerm_subnet.internal.id\n    private_ip_address_allocation = \"Dynamic\"\n  }\n}\n\nresource \"azurerm_network_security_group\" \"nsg_example\" {\n  name                = \"${var.nsg_name}-${var.postfix}\"\n  location            = azurerm_resource_group.nsg_rg.location\n  resource_group_name = azurerm_resource_group.nsg_rg.name\n}\n\nresource \"azurerm_network_interface_security_group_association\" \"main\" {\n  network_interface_id      = azurerm_network_interface.main.id\n  network_security_group_id = azurerm_network_security_group.nsg_example.id\n}\n\nresource \"azurerm_network_security_rule\" \"allow_ssh\" {\n  name                        = \"${var.nsg_ssh_rule_name}-${var.postfix}\"\n  description                 = \"${var.nsg_ssh_rule_name}-${var.postfix}\"\n  priority                    = 100\n  direction                   = \"Inbound\"\n  access                      = \"Allow\"\n  protocol                    = \"Tcp\"\n  source_port_range           = \"*\"\n  destination_port_range      = 22\n  source_address_prefix       = \"*\"\n  destination_address_prefix  = \"*\"\n  resource_group_name         = azurerm_resource_group.nsg_rg.name\n  network_security_group_name = azurerm_network_security_group.nsg_example.name\n}\n\nresource \"azurerm_network_security_rule\" \"block_http\" {\n  name                        = \"${var.nsg_http_rule_name}-${var.postfix}\"\n  description                 = \"${var.nsg_http_rule_name}-${var.postfix}\"\n  priority                    = 200\n  direction                   = \"Inbound\"\n  access                      = \"Deny\"\n  protocol                    = \"Tcp\"\n  source_port_range           = \"*\"\n  destination_port_range      = 80\n  source_address_prefix       = \"*\"\n  destination_address_prefix  = \"*\"\n  resource_group_name         = azurerm_resource_group.nsg_rg.name\n  network_security_group_name = azurerm_network_security_group.nsg_example.name\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY A VIRTUAL MACHINE RUNNING UBUNTU\n# This VM does not actually do anything and is the smallest size VM available with an Ubuntu image\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"azurerm_virtual_machine\" \"vm_example\" {\n  name                             = \"${var.vm_name}-${var.postfix}\"\n  location                         = azurerm_resource_group.nsg_rg.location\n  resource_group_name              = azurerm_resource_group.nsg_rg.name\n  network_interface_ids            = [azurerm_network_interface.main.id]\n  vm_size                          = var.vm_size\n  delete_os_disk_on_termination    = true\n  delete_data_disks_on_termination = true\n\n  storage_image_reference {\n    publisher = \"Canonical\"\n    offer     = \"UbuntuServer\"\n    sku       = \"16.04-LTS\"\n    version   = \"latest\"\n  }\n\n  storage_os_disk {\n    name              = \"${var.os_disk_name}-${var.postfix}\"\n    caching           = \"ReadWrite\"\n    create_option     = \"FromImage\"\n    managed_disk_type = \"Standard_LRS\"\n  }\n\n  os_profile {\n    computer_name  = var.hostname\n    admin_username = var.username\n    admin_password = random_password.nsg.result\n  }\n\n  os_profile_linux_config {\n    disable_password_authentication = false\n  }\n\n  # Correctly setup the dependencies to make sure resources are correctly destroyed.\n  depends_on = [\n    azurerm_network_interface_security_group_association.main\n  ]\n}\n\nresource \"random_password\" \"nsg\" {\n  length           = 16\n  override_special = \"-_%@\"\n  min_upper        = \"1\"\n  min_lower        = \"1\"\n  min_numeric      = \"1\"\n  min_special      = \"1\"\n}\n\n"
  },
  {
    "path": "examples/azure/terraform-azure-nsg-example/outputs.tf",
    "content": "output \"vm_name\" {\n  value = azurerm_virtual_machine.vm_example.name\n}\n\noutput \"resource_group_name\" {\n  value = azurerm_resource_group.nsg_rg.name\n}\n\noutput \"nsg_name\" {\n  value = azurerm_network_security_group.nsg_example.name\n}\n\noutput \"ssh_rule_name\" {\n  value = azurerm_network_security_rule.allow_ssh.name\n}\n\noutput \"http_rule_name\" {\n  value = azurerm_network_security_rule.block_http.name\n}\n"
  },
  {
    "path": "examples/azure/terraform-azure-nsg-example/variables.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# ENVIRONMENT VARIABLES\n# Define these secrets as environment variables\n# ---------------------------------------------------------------------------------------------------------------------\n\n# ARM_CLIENT_ID\n# ARM_CLIENT_SECRET\n# ARM_SUBSCRIPTION_ID\n# ARM_TENANT_ID\n\n# ---------------------------------------------------------------------------------------------------------------------\n# REQUIRED PARAMETERS\n# You must provide a value for each of these parameters.\n# ---------------------------------------------------------------------------------------------------------------------\n\n# ---------------------------------------------------------------------------------------------------------------------\n# OPTIONAL PARAMETERS\n# These parameters have reasonable defaults.\n# ---------------------------------------------------------------------------------------------------------------------\n\nvariable \"postfix\" {\n  description = \"Random postfix string used for each test run; set from the test file at runtime.\"\n  type        = string\n  default     = \"qwefgt\"\n}\n\nvariable \"resource_group_name\" {\n  description = \"Name for the resource group holding resources for this example\"\n  type        = string\n  default     = \"terratest-nsg-rg\"\n}\n\nvariable \"location\" {\n  description = \"The Azure region in which to deploy this sample\"\n  type        = string\n  default     = \"East US\"\n}\n\nvariable \"vnet_name\" {\n  description = \"Name for the example virtual network\"\n  type        = string\n  default     = \"vnet01\"\n}\n\nvariable \"subnet_name\" {\n  description = \"Name for the example virtual network default subnet\"\n  type        = string\n  default     = \"subnet01\"\n}\n\nvariable \"vm_nic_name\" {\n  description = \"Name for the NIC attached to our example VM.\"\n  type        = string\n  default     = \"nic01\"\n}\n\nvariable \"vm_nic_ip_config_name\" {\n  description = \"Name for the NIC IP configuration attached to our example VM.\"\n  type        = string\n  default     = \"nic_ipconfig01\"\n}\n\nvariable \"nsg_name\" {\n  description = \"Name for the example NSG.\"\n  type        = string\n  default     = \"nsg01\"\n}\n\nvariable \"nsg_ssh_rule_name\" {\n  description = \"Name for the example SSH NSG rule used in this example.\"\n  type        = string\n  default     = \"nsgrule01\"\n}\n\nvariable \"nsg_http_rule_name\" {\n  description = \"Name for the example HTTP NSG rule used in this example.\"\n  type        = string\n  default     = \"nsgrule02\"\n}\n\nvariable \"vm_name\" {\n  description = \"The name of the VM used in this example\"\n  type        = string\n  default     = \"vm01\"\n}\n\nvariable \"vm_size\" {\n  description = \"The size of the VM to deploy\"\n  type        = string\n  default     = \"Standard_B1s\"\n}\n\nvariable \"hostname\" {\n  description = \"The hostname of the new VM to be configured\"\n  type        = string\n  default     = \"vm01\"\n}\n\nvariable \"os_disk_name\" {\n  description = \"The of the OS disk to use on our example VM.\"\n  type        = string\n  default     = \"osdisk01\"\n}\n\nvariable \"username\" {\n  description = \"The username to be provisioned into your VM\"\n  type        = string\n  default     = \"testadmin\"\n}\n\nvariable \"password\" {\n  description = \"The password to configure for SSH access\"\n  type        = string\n  default     = \"!@#PasswordSetInCode!@#\"\n}\n"
  },
  {
    "path": "examples/azure/terraform-azure-postgresql-example/README.md",
    "content": "# Terraform Azure PostgreSQL DB Example\n\nThis folder contains a Terraform module that deploys resources in [Azure](https://azure.microsoft.com/) to demonstrate how you can use Terratest to write automated tests for your Azure Terraform code.\nThis module deploys a database for PostgreSQL.\n\n- A [Azure PostgreSQL Database](https://azure.microsoft.com/services/postgresql/).\n\nCheck out [test/azure/terraform_azure_postgresqldb_example_test.go](./../../../test/azure/terraform_azure_postgresqldb_example_test.go) to see how you can write automated tests for this module and validate the configuration of the parameters and options. \n\n**WARNING**: This module and the automated tests for it deploy real resources into your Azure account which can cost you money. \n\n## Running this module manually\n1. Sign up for [Azure](https://azure.microsoft.com/).\n1. Configure your Azure credentials using one of the [supported methods for Azure CLI\n   tools](https://docs.microsoft.com/en-us/cli/azure/azure-cli-configuration?view=azure-cli-latest)\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`.\n1. Ensure [environment variables](../README.md#review-environment-variables) are available\n1. Run `terraform init`\n1. Run `terraform apply`\n1. When you're done, run `terraform destroy`.\n\n\n## Running automated tests against this module\n1. Sign up for [Azure](https://azure.microsoft.com/)\n1. Configure your Azure credentials using one of the [supported methods for Azure CLI\n   tools](https://docs.microsoft.com/en-us/cli/azure/azure-cli-configuration?view=azure-cli-latest)\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`\n1. Configure your Terratest [Go test environment](../README.md) \n1. `cd test/azure`\n1. `go build terraform_azure_postgresql_example_test.go`\n1. `go test -v -timeout 60m -tags azure -run TestPostgreSQLDatabase`\n"
  },
  {
    "path": "examples/azure/terraform-azure-postgresql-example/main.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY AN PostgreSQL Database\n# This is an example of how to deploy an Azure PostgreSQL database.\n# See test/terraform_azure_example_test.go for how to write automated tests for this code.\n# ---------------------------------------------------------------------------------------------------------------------\n\n\n# ---------------------------------------------------------------------------------------------------------------------\n# CONFIGURE OUR AZURE CONNECTION\n# ---------------------------------------------------------------------------------------------------------------------\nprovider \"azurerm\" {\n  features {}\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY A RESOURCE GROUP\n# ---------------------------------------------------------------------------------------------------------------------\nresource \"azurerm_resource_group\" \"rg\" {\n  name     = \"${var.resource_group_name}-${var.postfix}\"\n  location = var.location\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY AZURE PostgreSQL SERVER\n# ---------------------------------------------------------------------------------------------------------------------\nresource \"azurerm_postgresql_server\" \"postgresqlserver\" {\n  name                = \"postgresqlserver-${var.postfix}\"\n  location            = azurerm_resource_group.rg.location\n  resource_group_name = azurerm_resource_group.rg.name\n\n  sku_name = \"B_Gen5_2\"\n\n  storage_mb                   = 5120\n  backup_retention_days        = 7\n  geo_redundant_backup_enabled = false\n  auto_grow_enabled            = true\n\n  administrator_login          = \"pgsqladmin\"\n  administrator_login_password = random_password.password.result\n  version                      = \"11\"\n  ssl_enforcement_enabled      = true\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY AZURE PostgreSQL Database\n# ---------------------------------------------------------------------------------------------------------------------\nresource \"azurerm_postgresql_database\" \"postgresqldb\" {\n  name                = \"postgresqldb\"\n  resource_group_name = azurerm_resource_group.rg.name\n  server_name         = azurerm_postgresql_server.postgresqlserver.name\n  charset             = \"UTF8\"\n  collation           = \"English_United States.1252\"\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# Use a random password geneerator\n# ---------------------------------------------------------------------------------------------------------------------\nresource \"random_password\" \"password\" {\n  length  = 20\n  special = true\n  upper   = true\n  lower   = true\n  number  = true\n}\n"
  },
  {
    "path": "examples/azure/terraform-azure-postgresql-example/output.tf",
    "content": "output \"sku_name\" {\n  value = azurerm_postgresql_server.postgresqlserver.sku_name\n}\n\noutput \"servername\" {\n  value = azurerm_postgresql_server.postgresqlserver.name\n\n}\n\noutput \"rgname\" {\n  value = azurerm_resource_group.rg.name\n}"
  },
  {
    "path": "examples/azure/terraform-azure-postgresql-example/variables.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# ENVIRONMENT VARIABLES\n# Define these secrets as environment variables\n# ---------------------------------------------------------------------------------------------------------------------\n\n# ARM_CLIENT_ID\n# ARM_CLIENT_SECRET\n# ARM_SUBSCRIPTION_ID\n# ARM_TENANT_ID\n\n# ---------------------------------------------------------------------------------------------------------------------\n# REQUIRED PARAMETERS\n# You must provide a value for each of these parameters.\n# ---------------------------------------------------------------------------------------------------------------------\n\n# ---------------------------------------------------------------------------------------------------------------------\n# OPTIONAL PARAMETERS\n# These parameters have reasonable defaults.\n# ---------------------------------------------------------------------------------------------------------------------\n\nvariable \"resource_group_name\" {\n  description = \"Name for the resource group holding resources for this example\"\n  type        = string\n  default     = \"terratest-postgres-rg\"\n}\n\nvariable \"location\" {\n  description = \"The Azure region in which to deploy this sample\"\n  type        = string\n  default     = \"East US\"\n}\n\nvariable \"postfix\" {\n  description = \"A postfix string to centrally mitigate resource name collisions.\"\n  type        = string\n  default     = \"resource\"\n}\n"
  },
  {
    "path": "examples/azure/terraform-azure-recoveryservices-example/README.md",
    "content": "# Terraform Azure Recovery Services Example\n\nThis folder contains a simple Terraform module that deploys resources in [Azure](https://azure.microsoft.com/) to demonstrate\nhow you can use Terratest to write automated tests for your Azure Terraform code. This module deploys a Recovery Services Vault with one backup virtual machine policy.\n\n- A [Recovery Services](https://azure.microsoft.com/services/backup/) that gives the module the following:\n  - [Backup Vault](https://docs.microsoft.com/azure/backup/backup-azure-recovery-services-vault-overview)  with the value specified in the `recovery_service_vault_name` output variable.\n  - [Backup VM Policy](https://azure.microsoft.com/en-in/updates/azure-vm-backup-policy-management/)  with the value specified in the `backup_policy_vm_name`  output variable.\n\nCheck out [test/azure/terraform_azure_recoveryservices_example_test.go](/test/azure/terraform_azure_recoveryservices_example_test.go) to see how you can write\nautomated tests for this module.\n\nNote that the Recovery Services Vault and backup virtual machine policy in this module don't actually do anything; it just runs the resources for\ndemonstration purposes.\n\n**WARNING**: This module and the automated tests for it deploy real resources into your Azure account which can cost you\nmoney. The resources are all part of the [Azure Free Account](https://azure.microsoft.com/free/), so if you haven't used that up,\nit should be free, but you are completely responsible for all Azure charges.\n\n## Running this module manually\n\n1. Sign up for [Azure](https://azure.microsoft.com/)\n1. Configure your Azure credentials using one of the [supported methods for Azure CLI\n   tools](https://docs.microsoft.com/cli/azure/azure-cli-configuration?view=azure-cli-latest)\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`\n1. Ensure [environment variables](../README.md#review-environment-variables) are available\n1. Run `terraform init`\n1. Run `terraform apply`\n1. When you're done, run `terraform destroy`\n\n## Running automated tests against this module\n\n1. Sign up for [Azure](https://azure.microsoft.com/)\n1. Configure your Azure credentials using one of the [supported methods for Azure CLI\n   tools](https://docs.microsoft.com/en-us/cli/azure/azure-cli-configuration?view=azure-cli-latest)\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`\n1. Configure your Terratest [Go test environment](../README.md)\n1. `cd test/azure`\n1. `go build terraform_azure_recoveryservices_example_test.go`\n1. `go test -v -run TestTerraformAzureRecoveryServicesExample`\n"
  },
  {
    "path": "examples/azure/terraform-azure-recoveryservices-example/main.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY AN AZURE AVAILABILITY SET\n# This is an example of how to deploy an Azure Availability Set with a Virtual Machine in the availability set \n# and the minimum network resources for the VM.\n# ---------------------------------------------------------------------------------------------------------------------\n# See test/azure/terraform_azure_availabilityset_example_test.go for how to write automated tests for this code.\n# ---------------------------------------------------------------------------------------------------------------------\n\nprovider \"azurerm\" {\n  features {}\n}\n\nterraform {\n  # This module is now only being tested with Terraform 0.13.x. However, to make upgrading easier, we are setting\n  # 0.12.26 as the minimum version, as that version added support for required_providers with source URLs, making it\n  # forwards compatible with 0.13.x code.\n  required_version = \">= 0.12.26\"\n  required_providers {\n    azurerm = {\n      version = \"~> 2.20\"\n      source  = \"hashicorp/azurerm\"\n    }\n  }\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY A RESOURCE GROUP\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"azurerm_resource_group\" \"resource_group\" {\n  name     = \"terratest-ars-rg-${var.postfix}\"\n  location = var.location\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY A RECOVERY SERVICES VAULT\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"azurerm_recovery_services_vault\" \"vault\" {\n  name                = \"rsvault${var.postfix}\"\n  location            = azurerm_resource_group.resource_group.location\n  resource_group_name = azurerm_resource_group.resource_group.name\n  sku                 = \"Standard\"\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY A BACKUP POLICY\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"azurerm_backup_policy_vm\" \"vm_policy\" {\n  name                = \"vmpolicy-${var.postfix}\"\n  resource_group_name = azurerm_resource_group.resource_group.name\n  recovery_vault_name = azurerm_recovery_services_vault.vault.name\n\n  timezone = \"UTC\"\n\n  backup {\n    frequency = \"Daily\"\n    time      = \"23:00\"\n  }\n\n  retention_daily {\n    count = 10\n  }\n\n  retention_weekly {\n    count    = 42\n    weekdays = [\"Sunday\", \"Wednesday\", \"Friday\", \"Saturday\"]\n  }\n\n  retention_monthly {\n    count    = 7\n    weekdays = [\"Sunday\", \"Wednesday\"]\n    weeks    = [\"First\", \"Last\"]\n  }\n\n  retention_yearly {\n    count    = 77\n    weekdays = [\"Sunday\"]\n    weeks    = [\"Last\"]\n    months   = [\"January\"]\n  }\n}"
  },
  {
    "path": "examples/azure/terraform-azure-recoveryservices-example/outputs.tf",
    "content": "output \"resource_group_name\" {\n  value = azurerm_resource_group.resource_group.name\n}\n\noutput \"recovery_service_vault_name\" {\n  value = azurerm_recovery_services_vault.vault.name\n}\n\noutput \"backup_policy_vm_name\" {\n  value = azurerm_backup_policy_vm.vm_policy.name\n}\n"
  },
  {
    "path": "examples/azure/terraform-azure-recoveryservices-example/variables.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# ENVIRONMENT VARIABLES\n# Define these secrets as environment variables\n# ---------------------------------------------------------------------------------------------------------------------\n\n# ARM_CLIENT_ID\n# ARM_CLIENT_SECRET\n# ARM_SUBSCRIPTION_ID\n# ARM_TENANT_ID\n\n# ---------------------------------------------------------------------------------------------------------------------\n# REQUIRED PARAMETERS\n# You must provide a value for each of these parameters.\n# ---------------------------------------------------------------------------------------------------------------------\n\n# ---------------------------------------------------------------------------------------------------------------------\n# OPTIONAL PARAMETERS\n# These parameters have reasonable defaults.\n# ---------------------------------------------------------------------------------------------------------------------\n\nvariable \"location\" {\n  description = \"The location to set for the storage account.\"\n  type        = string\n  default     = \"East US\"\n}\n\nvariable \"postfix\" {\n  description = \"A postfix string to centrally mitigate resource name collisions\"\n  type        = string\n  default     = \"resource\"\n}\n\n\n"
  },
  {
    "path": "examples/azure/terraform-azure-resourcegroup-example/README.md",
    "content": "# Terraform Azure Resource Group Example\n\nThis folder contains a simple Terraform module that deploys resources in [Azure](https://azure.microsoft.com/) to demonstrate\nhow you can use TerraTest to write automated tests for your Azure Terraform code. This module deploys a Resource Group.\n\n- A [Resource Group](https://docs.microsoft.com/azure/azure-resource-manager/management/overview) with no other resources.\n\nCheck out [test/azure/terraform_azure_resourcegroup_example_test.go](/test/azure/terraform_azure_resourcegroup_example_test.go) to see how you can write\nautomated tests for this module.\n\nNote that the Resource Group this module creates does not actually do anything; it just runs the resources for\ndemonstration purposes.\n\n**WARNING**: This module and the automated tests for it deploy real resources into your Azure account which can cost you\nmoney. The resources are all part of the [Azure Free Account](https://azure.microsoft.com/free/), so if you haven't used that up,\nit should be free, but you are completely responsible for all Azure charges.\n\n## Running this module manually\n\n1. Sign up for [Azure](https://azure.microsoft.com/)\n1. Configure your Azure credentials using one of the [supported methods for Azure CLI\n   tools](https://docs.microsoft.com/cli/azure/azure-cli-configuration?view=azure-cli-latest)\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`\n1. Ensure [environment variables](../README.md#review-environment-variables) are available\n1. Run `terraform init`\n1. Run `terraform apply`\n1. When you're done, run `terraform destroy`\n\n## Running automated tests against this module\n\n1. Sign up for [Azure](https://azure.microsoft.com/)\n1. Configure your Azure credentials using one of the [supported methods for Azure CLI\n   tools](https://docs.microsoft.com/cli/azure/azure-cli-configuration?view=azure-cli-latest)\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`\n1. Configure your TerraTest [Go test environment](../README.md)\n1. `cd test/azure`\n1. `go build terraform_azure_resourcegroup_example_test.go`\n1. `go test -v -run TestTerraformAzureResourceGroupExample`\n"
  },
  {
    "path": "examples/azure/terraform-azure-resourcegroup-example/main.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY A RESOURCE GROUP\n# This is an example of how to deploy a Resource Group\n# ---------------------------------------------------------------------------------------------------------------------\n# See test/azure/terraform_azure_resourcegroup_example_test.go for how to write automated tests for this code.\n# ---------------------------------------------------------------------------------------------------------------------\n\nprovider \"azurerm\" {\n  features {}\n}\n\n# PIN TERRAFORM VERSION\n\nterraform {\n  # This module is now only being tested with Terraform 0.13.x. However, to make upgrading easier, we are setting\n  # 0.12.26 as the minimum version, as that version added support for required_providers with source URLs, making it\n  # forwards compatible with 0.13.x code.\n  required_version = \">= 0.12.26\"\n  required_providers {\n    azurerm = {\n      version = \"~> 2.20\"\n      source  = \"hashicorp/azurerm\"\n    }\n  }\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY A RESOURCE GROUP\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"azurerm_resource_group\" \"resource_group\" {\n  name     = \"terratest-rg-${var.postfix}\"\n  location = var.location\n}\n"
  },
  {
    "path": "examples/azure/terraform-azure-resourcegroup-example/outputs.tf",
    "content": "output \"resource_group_name\" {\n  value = azurerm_resource_group.resource_group.name\n}"
  },
  {
    "path": "examples/azure/terraform-azure-resourcegroup-example/variables.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# ENVIRONMENT VARIABLES\n# Define these secrets as environment variables\n# ---------------------------------------------------------------------------------------------------------------------\n\n# ARM_CLIENT_ID\n# ARM_CLIENT_SECRET\n# ARM_SUBSCRIPTION_ID\n# ARM_TENANT_ID\n\n# ---------------------------------------------------------------------------------------------------------------------\n# REQUIRED PARAMETERS\n# You must provide a value for each of these parameters.\n# ---------------------------------------------------------------------------------------------------------------------\n\n# ---------------------------------------------------------------------------------------------------------------------\n# OPTIONAL PARAMETERS\n# These parameters have reasonable defaults.\n# ---------------------------------------------------------------------------------------------------------------------\n\nvariable \"location\" {\n  description = \"The location to set for the resource group.\"\n  type        = string\n  default     = \"East US\"\n}\n\nvariable \"postfix\" {\n  description = \"A postfix string to centrally mitigate resource name collisions.\"\n  type        = string\n  default     = \"resource\"\n}\n\n\n\n"
  },
  {
    "path": "examples/azure/terraform-azure-servicebus-example/README.md",
    "content": "# Terraform Azure Service Bus Example\n\nThis folder contains a Terraform module that deploys resources in [Azure](https://azure.microsoft.com/) to demonstrate how you can use Terratest to write automated tests for your Azure Terraform code. This module deploys a Service Bus.\n\n- A [Service Bus](https://docs.microsoft.com/en-us/azure/service-bus-messaging/service-bus-messaging-overview) with the namespace specified in the `namespace_name` variable.\n\nCheck out [test/azure/terraform_azure_servicebus_example_test.go](./../../../test/azure/terraform_azure_servicebus_example_test.go) to see how you can write automated tests for this module and validate the configuration of the parameters and options. \n\n**WARNING**: This module and the automated tests for it deploy real resources into your Azure account which can cost you money. The resources are all part of the [Azure Free Account](https://azure.microsoft.com/en-us/free/), so if you haven't used that up,\nit should be free, but you are completely responsible for all Azure charges.\n\n## Running this module manually\n1. Sign up for [Azure](https://azure.microsoft.com/).\n1. Configure your Azure credentials using one of the [supported methods for Azure CLI\n   tools](https://docs.microsoft.com/en-us/cli/azure/azure-cli-configuration?view=azure-cli-latest)\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`.\n1. Ensure [environment variables](../README.md#review-environment-variables) are available\n1. Run `terraform init`\n1. Run `terraform apply`\n1. When you're done, run `terraform destroy`.\n\n\n## Running automated tests against this module\n1. Sign up for [Azure](https://azure.microsoft.com/)\n1. Configure your Azure credentials using one of the [supported methods for Azure CLI\n   tools](https://docs.microsoft.com/en-us/cli/azure/azure-cli-configuration?view=azure-cli-latest)\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`\n1. Configure your Terratest [Go test environment](../README.md) \n1. `cd test/azure`\n1. `go build terraform_azure_servicebus_example_test.go`\n1. `go test -v -run TestTerraformAzureServiceBusExample`\n"
  },
  {
    "path": "examples/azure/terraform-azure-servicebus-example/main.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY AN AZURE SERVICE BUS\n# This is an example of how to deploy an Azure service bus.\n# See test/terraform_azure_example_test.go for how to write automated tests for this code.\n# ---------------------------------------------------------------------------------------------------------------------\n\n\n# ---------------------------------------------------------------------------------------------------------------------\n# CONFIGURE OUR AZURE CONNECTION\n# ---------------------------------------------------------------------------------------------------------------------\n\nprovider \"azurerm\" {\n  features {}\n}\n\nterraform {\n  # This module is now only being tested with Terraform 0.13.x. However, to make upgrading easier, we are setting\n  # 0.12.26 as the minimum version, as that version added support for required_providers with source URLs, making it\n  # forwards compatible with 0.13.x code.\n  required_providers {\n    azurerm = {\n      version = \"~>2.29\"\n      source  = \"hashicorp/azurerm\"\n    }\n  }\n}\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY A RESOURCE GROUP\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"azurerm_resource_group\" \"servicebus_rg\" {\n  name     = \"terratest-sb-rg-${var.postfix}\"\n  location = var.location\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# Define locals variables\n# ---------------------------------------------------------------------------------------------------------------------\nlocals {\n  topic_authorization_rules = flatten([\n    for topic in var.topics : [\n      for rule in topic.authorization_rules :\n      merge(\n        rule, {\n          topic_name = topic.name\n      })\n    ]\n  ])\n\n  topic_subscriptions = flatten([\n    for topic in var.topics : [\n      for subscription in topic.subscriptions :\n      merge(\n        subscription, {\n          topic_name = topic.name\n      })\n    ]\n  ])\n\n  topic_subscription_rules = flatten([\n    for subscription in local.topic_subscriptions :\n    merge({\n      filter_type = \"\"\n      sql_filter  = \"\"\n      action      = \"\"\n      }, subscription, {\n      topic_name        = subscription.topic_name\n      subscription_name = subscription.name\n    })\n    if subscription.filter_type != null\n  ])\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY AZURE Service Bus Namespace\n# ---------------------------------------------------------------------------------------------------------------------\nresource \"azurerm_servicebus_namespace\" \"servicebus\" {\n  name                = \"terratest-namespace-${var.namespace_name}\"\n  location            = azurerm_resource_group.servicebus_rg.location\n  resource_group_name = azurerm_resource_group.servicebus_rg.name\n  sku                 = var.sku\n  tags                = var.tags\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY AZURE Service Bus Namespace Authorization Rule\n# ---------------------------------------------------------------------------------------------------------------------\nresource \"azurerm_servicebus_namespace_authorization_rule\" \"sbnamespaceauth\" {\n  count = length(var.namespace_authorization_rules)\n\n  name                = var.namespace_authorization_rules[count.index].policy_name\n  namespace_name      = azurerm_servicebus_namespace.servicebus.name\n  resource_group_name = azurerm_resource_group.servicebus_rg.name\n\n  listen = var.namespace_authorization_rules[count.index].claims.listen\n  send   = var.namespace_authorization_rules[count.index].claims.send\n  manage = var.namespace_authorization_rules[count.index].claims.manage\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY AZURE Service Bus Topic\n# ---------------------------------------------------------------------------------------------------------------------\nresource \"azurerm_servicebus_topic\" \"sptopic\" {\n  count = length(var.topics)\n\n  name                = var.topics[count.index].name\n  resource_group_name = azurerm_resource_group.servicebus_rg.name\n  namespace_name      = azurerm_servicebus_namespace.servicebus.name\n\n  requires_duplicate_detection = var.topics[count.index].requires_duplicate_detection\n  default_message_ttl          = var.topics[count.index].default_message_ttl\n  enable_partitioning          = var.topics[count.index].enable_partitioning\n  support_ordering             = var.topics[count.index].support_ordering\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY AZURE Service Bus Topic Authorization Rule\n# ---------------------------------------------------------------------------------------------------------------------\nresource \"azurerm_servicebus_topic_authorization_rule\" \"topicaauth\" {\n  count = length(local.topic_authorization_rules)\n\n  name                = local.topic_authorization_rules[count.index].policy_name\n  resource_group_name = azurerm_resource_group.servicebus_rg.name\n  namespace_name      = azurerm_servicebus_namespace.servicebus.name\n  topic_name          = local.topic_authorization_rules[count.index].topic_name\n\n  listen = local.topic_authorization_rules[count.index].claims.listen\n  send   = local.topic_authorization_rules[count.index].claims.send\n  manage = local.topic_authorization_rules[count.index].claims.manage\n\n  depends_on = [azurerm_servicebus_topic.sptopic]\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY AZURE Service Bus Subscription\n# ---------------------------------------------------------------------------------------------------------------------\nresource \"azurerm_servicebus_subscription\" \"subscription\" {\n  count = length(local.topic_subscriptions)\n\n  name                = local.topic_subscriptions[count.index].name\n  resource_group_name = azurerm_resource_group.servicebus_rg.name\n  namespace_name      = azurerm_servicebus_namespace.servicebus.name\n  topic_name          = local.topic_subscriptions[count.index].topic_name\n\n  max_delivery_count                   = local.topic_subscriptions[count.index].max_delivery_count\n  lock_duration                        = local.topic_subscriptions[count.index].lock_duration\n  forward_to                           = local.topic_subscriptions[count.index].forward_to\n  dead_lettering_on_message_expiration = local.topic_subscriptions[count.index].dead_lettering_on_message_expiration\n\n  depends_on = [azurerm_servicebus_topic.sptopic]\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY AZURE Service Bus Subscription Rules\n# ---------------------------------------------------------------------------------------------------------------------\nresource \"azurerm_servicebus_subscription_rule\" \"subrules\" {\n  count = length(local.topic_subscription_rules)\n\n  name                = local.topic_subscription_rules[count.index].name\n  resource_group_name = azurerm_resource_group.servicebus_rg.name\n  namespace_name      = azurerm_servicebus_namespace.servicebus.name\n  topic_name          = local.topic_subscription_rules[count.index].topic_name\n  subscription_name   = local.topic_subscription_rules[count.index].subscription_name\n  filter_type         = local.topic_subscription_rules[count.index].filter_type != \"\" ? \"SqlFilter\" : null\n  sql_filter          = local.topic_subscription_rules[count.index].sql_filter\n  action              = local.topic_subscription_rules[count.index].action\n\n  depends_on = [azurerm_servicebus_subscription.subscription]\n}\n"
  },
  {
    "path": "examples/azure/terraform-azure-servicebus-example/outputs.tf",
    "content": "output \"resource_group\" {\n  description = \"The resource group name of the Service Bus namespace.\"\n  value       = azurerm_resource_group.servicebus_rg.name\n}\n\noutput \"namespace_name\" {\n  description = \"The namespace name.\"\n  value       = azurerm_servicebus_namespace.servicebus.name\n}\n\noutput \"namespace_id\" {\n  description = \"The namespace ID.\"\n  value       = azurerm_servicebus_namespace.servicebus.id\n  sensitive   = true\n}\n\noutput \"namespace_authorization_rules\" {\n  description = \"List of namespace authorization rules.\"\n  value = {\n    for auth in azurerm_servicebus_namespace_authorization_rule.sbnamespaceauth :\n    auth.name => {\n      listen = auth.listen\n      send   = auth.send\n      manage = auth.manage\n    }\n  }\n  sensitive = true\n}\n\noutput \"service_bus_namespace_default_primary_key\" {\n  description = \"The primary access key for the authorization rule RootManageSharedAccessKey which is created automatically by Azure.\"\n  value       = azurerm_servicebus_namespace.servicebus.default_primary_key\n  sensitive   = true\n}\n\noutput \"service_bus_namespace_default_connection_string\" {\n  description = \"The primary connection string for the authorization rule RootManageSharedAccessKey which is created automatically by Azure.\"\n  value       = azurerm_servicebus_namespace.servicebus.default_primary_connection_string\n  sensitive   = true\n}\n\n\noutput \"topics\" {\n  description = \"All topics with the corresponding subscriptions\"\n  value = {\n    for topic in azurerm_servicebus_topic.sptopic :\n    topic.name => {\n      id   = topic.id\n      name = topic.name\n      authorization_rules = {\n        for auth in azurerm_servicebus_topic_authorization_rule.topicaauth :\n        auth.name => {\n          listen = auth.listen\n          send   = auth.send\n          manage = auth.manage\n        } if topic.name == auth.topic_name\n      }\n      subscriptions = {\n        for subscription in azurerm_servicebus_subscription.subscription :\n        subscription.name => {\n          name = subscription.name\n        } if topic.name == subscription.topic_name\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "examples/azure/terraform-azure-servicebus-example/variables.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# ENVIRONMENT VARIABLES\n# Define these secrets as environment variables\n# ---------------------------------------------------------------------------------------------------------------------\n\n# ARM_CLIENT_ID\n# ARM_CLIENT_SECRET\n# ARM_SUBSCRIPTION_ID\n# ARM_TENANT_ID\n\n# ---------------------------------------------------------------------------------------------------------------------\n# REQUIRED PARAMETERS\n# You must provide a value for each of these parameters.\n# ---------------------------------------------------------------------------------------------------------------------\n\n# ---------------------------------------------------------------------------------------------------------------------\n# OPTIONAL PARAMETERS\n# These parameters have reasonable defaults.\n# ---------------------------------------------------------------------------------------------------------------------\n\nvariable \"location\" {\n  description = \"The supported azure location where the resource exists\"\n  type        = string\n  default     = \"West US2\"\n}\n\nvariable \"postfix\" {\n  description = \"string mitigate resource name collisions.\"\n  type        = string\n  default     = \"servicebus\"\n}\n\nvariable \"namespace_name\" {\n  description = \"The name of the namespace.\"\n  type        = string\n  default     = \"testservicebus101\"\n}\n\nvariable \"sku\" {\n  description = \"The SKU of the namespace. The options are: `Basic`, `Standard`, `Premium`.\"\n  type        = string\n  default     = \"Standard\"\n}\n\nvariable \"tags\" {\n  description = \" A mapping of tags to assign to the resource.\"\n  type        = map(string)\n  default     = {}\n}\n\nvariable \"namespace_authorization_rules\" {\n  description = \"List of namespace authorization rules.\"\n  type = list(object({\n    policy_name = string\n    claims      = object({ listen = bool, manage = bool, send = bool })\n  }))\n  default = []\n}\n\nvariable \"topics\" {\n  description = \"topics list\"\n  type = list(object({\n    name                         = string\n    default_message_ttl          = string //ISO 8601 format\n    enable_partitioning          = bool\n    requires_duplicate_detection = bool\n    support_ordering             = bool\n    authorization_rules = list(object({\n      policy_name = string\n      claims      = object({ listen = bool, manage = bool, send = bool })\n\n    }))\n    subscriptions = list(object({\n      name                                 = string\n      max_delivery_count                   = number\n      lock_duration                        = string //ISO 8601 format\n      forward_to                           = string //set with the topic name that will be used for forwarding. Otherwise, set to \"\"\n      dead_lettering_on_message_expiration = bool\n      filter_type                          = string // SqlFilter is the only supported type now.\n      sql_filter                           = string //Required when filter_type is set to SqlFilter\n      action                               = string\n    }))\n  }))\n  default = []\n}\n"
  },
  {
    "path": "examples/azure/terraform-azure-sqldb-example/README.md",
    "content": "# Terraform Azure SQL DB Example\n\nThis folder contains a Terraform module that deploys resources in [Azure](https://azure.microsoft.com/) to demonstrate how you can use Terratest to write automated tests for your Azure Terraform code. This module deploys a SQL server, and a SQL database.\n\n- A [SQL server](https://azure.microsoft.com/services/sql-database/campaign/) with the name specified in the `sql_server_name` variable.\n- A [SQL Database](https://azure.microsoft.com/services/sql-database/) with the name specified in the `sql_database_name` variable.\n\nCheck out [test/azure/terraform_azure_sqldb_example_test.go](./../../../test/azure/terraform_azure_sqldb_example_test.go) to see how you can write automated tests for this module and validate the configuration of the parameters and options. \n\n**WARNING**: This module and the automated tests for it deploy real resources into your Azure account which can cost you money. The resources are all part of the [Azure Free Account](https://azure.microsoft.com/en-us/free/), so if you haven't used that up,\nit should be free, but you are completely responsible for all Azure charges.\n\n## Running this module manually\n1. Sign up for [Azure](https://azure.microsoft.com/).\n1. Configure your Azure credentials using one of the [supported methods for Azure CLI\n   tools](https://docs.microsoft.com/en-us/cli/azure/azure-cli-configuration?view=azure-cli-latest)\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`.\n1. Ensure [environment variables](../README.md#review-environment-variables) are available\n1. Run `terraform init`\n1. Run `terraform apply`\n1. When you're done, run `terraform destroy`.\n\n\n## Running automated tests against this module\n1. Sign up for [Azure](https://azure.microsoft.com/)\n1. Configure your Azure credentials using one of the [supported methods for Azure CLI\n   tools](https://docs.microsoft.com/en-us/cli/azure/azure-cli-configuration?view=azure-cli-latest)\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`\n1. Configure your Terratest [Go test environment](../README.md) \n1. `cd test/azure`\n1. `go build terraform_azure_sqldb_example_test.go`\n1. `go test -v -run TestTerraformAzureSQLDBExample`\n"
  },
  {
    "path": "examples/azure/terraform-azure-sqldb-example/main.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY AN AZURE SQL Database\n# This is an example of how to deploy an Azure sql database.\n# See test/terraform_azure_example_test.go for how to write automated tests for this code.\n# ---------------------------------------------------------------------------------------------------------------------\n\n\n# ---------------------------------------------------------------------------------------------------------------------\n# CONFIGURE OUR AZURE CONNECTION\n# ---------------------------------------------------------------------------------------------------------------------\n\nprovider \"azurerm\" {\n  features {}\n}\n\nterraform {\n  required_providers {\n    azurerm = {\n      version = \"~>2.29\"\n      source  = \"hashicorp/azurerm\"\n    }\n  }\n}\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY A RESOURCE GROUP\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"azurerm_resource_group\" \"sql_rg\" {\n  name     = \"terratest-rg-${var.postfix}\"\n  location = var.location\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY AZURE SQL SERVER\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"random_password\" \"password\" {\n  length           = 16\n  override_special = \"_%@\"\n  min_upper        = \"1\"\n  min_lower        = \"1\"\n  min_numeric      = \"1\"\n  min_special      = \"1\"\n}\n\nresource \"azurerm_sql_server\" \"sqlserver\" {\n  name                         = \"mssqlserver-${var.postfix}\"\n  resource_group_name          = azurerm_resource_group.sql_rg.name\n  location                     = azurerm_resource_group.sql_rg.location\n  version                      = \"12.0\"\n  administrator_login          = var.sqlserver_admin_login\n  administrator_login_password = random_password.password.result\n\n  tags = {\n    environment = var.tags\n  }\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY AZURE SQL DATA BASE\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"azurerm_sql_database\" \"sqldb\" {\n  name                = \"sqldb-${var.postfix}\"\n  resource_group_name = azurerm_resource_group.sql_rg.name\n  location            = azurerm_resource_group.sql_rg.location\n  server_name         = azurerm_sql_server.sqlserver.name\n  tags = {\n    environment = var.tags\n  }\n}\n"
  },
  {
    "path": "examples/azure/terraform-azure-sqldb-example/outputs.tf",
    "content": "output \"resource_group_name\" {\n  value = azurerm_resource_group.sql_rg.name\n}\n\noutput \"sql_database_id\" {\n  value = azurerm_sql_database.sqldb.id\n}\n\noutput \"sql_database_name\" {\n  value = azurerm_sql_database.sqldb.name\n}\n\noutput \"sql_server_id\" {\n  value = azurerm_sql_server.sqlserver.id\n}\n\noutput \"sql_server_name\" {\n  value = azurerm_sql_server.sqlserver.name\n}\n\noutput \"sql_server_full_domain_name\" {\n  value = azurerm_sql_server.sqlserver.fully_qualified_domain_name\n}\n\noutput \"sql_server_admin_login\" {\n  value = azurerm_sql_server.sqlserver.administrator_login\n}\n\noutput \"sql_server_admin_login_pass\" {\n  value     = azurerm_sql_server.sqlserver.administrator_login_password\n  sensitive = true\n}\n"
  },
  {
    "path": "examples/azure/terraform-azure-sqldb-example/variables.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# ENVIRONMENT VARIABLES\n# Define these secrets as environment variables\n# ---------------------------------------------------------------------------------------------------------------------\n\n# ARM_CLIENT_ID\n# ARM_CLIENT_SECRET\n# ARM_SUBSCRIPTION_ID\n# ARM_TENANT_ID\n\n# ---------------------------------------------------------------------------------------------------------------------\n# REQUIRED PARAMETERS\n# You must provide a value for each of these parameters.\n# ---------------------------------------------------------------------------------------------------------------------\n\n# ---------------------------------------------------------------------------------------------------------------------\n# OPTIONAL PARAMETERS\n# These parameters have reasonable defaults.\n# ---------------------------------------------------------------------------------------------------------------------\n\nvariable \"location\" {\n  description = \"The supported azure location where the resource exists\"\n  type        = string\n  default     = \"West US2\"\n}\n\nvariable \"sqlserver_admin_login\" {\n  description = \"The administrator login name for the sql server.\"\n  type        = string\n  default     = \"AdminUser2314\"\n}\n\nvariable \"tags\" {\n  description = \"A mapping of tags to assign to the resource.\"\n  type        = string\n  default     = \"Development\"\n}\n\nvariable \"postfix\" {\n  description = \"A postfix string to centrally mitigate resource name collisions\"\n  type        = string\n  default     = \"resource\"\n}\n"
  },
  {
    "path": "examples/azure/terraform-azure-sqlmanagedinstance-example/README.md",
    "content": "# Terraform Azure SQL DB Example\n\nThis folder contains a Terraform module that deploys resources in [Azure](https://azure.microsoft.com/) to demonstrate how you can use Terratest to write automated tests for your Azure Terraform code. This module deploys a SQL Managed Instance, and a SQL Managed Instance database.\n\n- A [SQL Managed Instance](https://azure.microsoft.com/en-us/products/azure-sql/managed-instance/).\n- A SQL Managed Database.\n\nCheck out [test/azure/terraform_azure_sqlmanagedinstance_example_test.go](./../../../test/azure/terraform_azure_sqlmanagedinstance_example_test.go) to see how you can write automated tests for this module and validate the configuration of the parameters and options. \n\n**WARNING**: This module and the automated tests for it deploy real resources into your Azure account which can cost you money. The resources are all part of the [Azure Free Account](https://azure.microsoft.com/en-us/free/), so if you haven't used that up,\nit should be free, but you are completely responsible for all Azure charges.\n\n## Running this module manually\n1. Sign up for [Azure](https://azure.microsoft.com/).\n1. Configure your Azure credentials using one of the [supported methods for Azure CLI\n   tools](https://docs.microsoft.com/en-us/cli/azure/azure-cli-configuration?view=azure-cli-latest)\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`.\n1. Ensure [environment variables](../README.md#review-environment-variables) are available\n1. Run `terraform init`\n1. Run `terraform apply`\n1. When you're done, run `terraform destroy`.\n\n\n## Running automated tests against this module\n\n**WARNING**: The deploymnet for this module usually takes more than 4-6 hours as stated in the [microsoft docs](https://learn.microsoft.com/en-us/azure/azure-sql/managed-instance/management-operations-overview?view=azuresql#duration), so please make sure to set the timeout accordingly in the below go test command.\n\n1. Sign up for [Azure](https://azure.microsoft.com/)\n2. Configure your Azure credentials using one of the [supported methods for Azure CLI\n   tools](https://docs.microsoft.com/en-us/cli/azure/azure-cli-configuration?view=azure-cli-latest)\n3. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`\n4. Configure your Terratest [Go test environment](../README.md) \n5. `cd test/azure`\n6. `go build terraform_azure_sqlmanagedinstance_example_test.go`\n7. `go test -v -run TestTerraformAzureSQLManagedInstanceExample -timeout <in hours>`\n"
  },
  {
    "path": "examples/azure/terraform-azure-sqlmanagedinstance-example/main.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY AN AZURE SQL Managed Instance\n# This is an example of how to deploy an AZURE SQL Managed Instance\n# See test/terraform_azure_example_test.go for how to write automated tests for this code.\n# ---------------------------------------------------------------------------------------------------------------------\n\n\n# ---------------------------------------------------------------------------------------------------------------------\n# CONFIGURE OUR AZURE CONNECTION\n# ---------------------------------------------------------------------------------------------------------------------\n\nprovider \"azurerm\" {\n  version = \"~>3.13.0\"\n  features {}\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# CREATE RANDOM PASSWORD\n# ---------------------------------------------------------------------------------------------------------------------\n\n# Random password is used as an example to simplify the deployment and improve the security of the database.\n# This is not as a production recommendation as the password is stored in the Terraform state file.\nresource \"random_password\" \"password\" {\n  length           = 16\n  override_special = \"-_%@\"\n  min_upper        = \"1\"\n  min_lower        = \"1\"\n  min_numeric      = \"1\"\n  min_special      = \"1\"\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY A RESOURCE GROUP\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"azurerm_resource_group\" \"sqlmi_rg\" {\n  name     = \"terratest-sqlmi-${var.postfix}\"\n  location = var.location\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY NETWORK RESOURCES\n# This network includes a public address for integration test demonstration purposes\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"azurerm_network_security_group\" \"sqlmi_nt_sec_grp\" {\n  name                = \"securitygroup-${var.postfix}\"\n  location            = azurerm_resource_group.sqlmi_rg.location\n  resource_group_name = azurerm_resource_group.sqlmi_rg.name\n}\n\nresource \"azurerm_network_security_rule\" \"allow_misubnet_inbound\" {\n  name                        = \"allow_subnet_${var.postfix}\"\n  priority                    = 200\n  direction                   = \"Inbound\"\n  access                      = \"Allow\"\n  protocol                    = \"*\"\n  source_port_range           = \"*\"\n  destination_port_range      = \"*\"\n  source_address_prefix       = \"10.0.0.0/24\"\n  destination_address_prefix  = \"*\"\n  resource_group_name         = azurerm_resource_group.sqlmi_rg.name\n  network_security_group_name = azurerm_network_security_group.sqlmi_nt_sec_grp.name\n}\n\nresource \"azurerm_virtual_network\" \"sqlmi_vm\" {\n  name                = \"vnet-${var.postfix}\"\n  resource_group_name = azurerm_resource_group.sqlmi_rg.name\n  address_space       = [\"10.0.0.0/16\"]\n  location            = azurerm_resource_group.sqlmi_rg.location\n}\n\nresource \"azurerm_subnet\" \"sqlmi_sub\" {\n  name                 = \"subnet-${var.postfix}\"\n  resource_group_name  = azurerm_resource_group.sqlmi_rg.name\n  virtual_network_name = azurerm_virtual_network.sqlmi_vm.name\n  address_prefixes     = [\"10.0.0.0/24\"]\n\n  delegation {\n    name = \"managedinstancedelegation\"\n\n    service_delegation {\n      name    = \"Microsoft.Sql/managedInstances\"\n      actions = [\"Microsoft.Network/virtualNetworks/subnets/join/action\", \"Microsoft.Network/virtualNetworks/subnets/prepareNetworkPolicies/action\", \"Microsoft.Network/virtualNetworks/subnets/unprepareNetworkPolicies/action\"]\n    }\n  }\n}\n\nresource \"azurerm_subnet_network_security_group_association\" \"sqlmi_sb_assoc\" {\n  subnet_id                 = azurerm_subnet.sqlmi_sub.id\n  network_security_group_id = azurerm_network_security_group.sqlmi_nt_sec_grp.id\n}\n\nresource \"azurerm_route_table\" \"sqlmi_rt\" {\n  name                          = \"routetable-${var.postfix}\"\n  location                      = azurerm_resource_group.sqlmi_rg.location\n  resource_group_name           = azurerm_resource_group.sqlmi_rg.name\n  disable_bgp_route_propagation = false\n  depends_on = [\n    azurerm_subnet.sqlmi_sub,\n  ]\n}\n\nresource \"azurerm_subnet_route_table_association\" \"sqlmi_sb_rt_assoc\" {\n  subnet_id      = azurerm_subnet.sqlmi_sub.id\n  route_table_id = azurerm_route_table.sqlmi_rt.id\n}\n\n# DEPLOY managed sql instance  ## This depends on vnet ##\nresource \"azurerm_mssql_managed_instance\" \"sqlmi_mi\" {\n  name                = \"sqlmi${var.postfix}\"\n  resource_group_name = azurerm_resource_group.sqlmi_rg.name\n  location            = azurerm_resource_group.sqlmi_rg.location\n\n  license_type       = var.sqlmi_license_type\n  sku_name           = var.sku_name\n  storage_size_in_gb = var.storage_size\n  subnet_id          = azurerm_subnet.sqlmi_sub.id\n  vcores             = var.cores\n\n  administrator_login          = var.admin_login\n  administrator_login_password = \"thisIsDog11\"\n}\n\nresource \"azurerm_mssql_managed_database\" \"sqlmi_db\" {\n  name                = var.sqlmi_db_name\n  managed_instance_id = azurerm_mssql_managed_instance.sqlmi_mi.id\n}"
  },
  {
    "path": "examples/azure/terraform-azure-sqlmanagedinstance-example/outputs.tf",
    "content": "output \"resource_group_name\" {\n  value = azurerm_resource_group.sqlmi_rg.name\n}\n\noutput \"network_security_group_name\" {\n  value = azurerm_network_security_group.sqlmi_nt_sec_grp.name\n}\n\noutput \"virtual_network_name\" {\n  value = azurerm_virtual_network.sqlmi_vm.name\n}\n\noutput \"subnet_name\" {\n  value = azurerm_subnet.sqlmi_sub.name\n}\n\noutput \"managed_instance_name\" {\n  value = azurerm_mssql_managed_instance.sqlmi_mi.name\n}\n\noutput \"managed_instance_db_name\" {\n  value = azurerm_mssql_managed_database.sqlmi_db.name\n}"
  },
  {
    "path": "examples/azure/terraform-azure-sqlmanagedinstance-example/variables.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# ENVIRONMENT VARIABLES\n# Define these secrets as environment variables\n# ---------------------------------------------------------------------------------------------------------------------\n\n# ARM_CLIENT_ID\n# ARM_CLIENT_SECRET\n# ARM_SUBSCRIPTION_ID\n# ARM_TENANT_ID\n\n# ---------------------------------------------------------------------------------------------------------------------\n# REQUIRED PARAMETERS\n# You must provide a value for each of these parameters.\n# ---------------------------------------------------------------------------------------------------------------------\n\n# ---------------------------------------------------------------------------------------------------------------------\n# OPTIONAL PARAMETERS\n# These parameters have reasonable defaults.\n# ---------------------------------------------------------------------------------------------------------------------\n\nvariable \"location\" {\n  description = \"The supported azure location where the resource exists\"\n  type        = string\n  default     = \"West US2\"\n}\n\nvariable \"sqlmi_license_type\" {\n  description = \"The license type for the sql managed instance\"\n  type        = string\n  default     = \"BasePrice\"\n}\n\nvariable \"sku_name\" {\n  description = \"The sku name for the sql managed instance\"\n  type        = string\n  default     = \"GP_Gen5\"\n}\n\nvariable \"storage_size\" {\n  description = \"The storage for the sql managed instance\"\n  type        = string\n  default     = 32\n}\n\nvariable \"cores\" {\n  description = \"The vcores for the sql managed instance\"\n  type        = string\n  default     = 4\n}\n\nvariable \"admin_login\" {\n  description = \"The login for the sql managed instance\"\n  type        = string\n  default     = \"sqlmiadmin\"\n}\n\n\nvariable \"sqlmi_db_name\" {\n  description = \"The Database for the sql managed instance\"\n  type        = string\n  default     = \"testdb\"\n}\n\nvariable \"postfix\" {\n  description = \"A postfix string to centrally mitigate resource name collisions.\"\n  type        = string\n  default     = \"resource\"\n}"
  },
  {
    "path": "examples/azure/terraform-azure-storage-example/README.md",
    "content": "# Terraform Azure Storage Example\n\nThis folder contains a simple Terraform module that deploys resources in [Azure](https://azure.microsoft.com/) to demonstrate\nhow you can use TerraTest to write automated tests for your Azure Terraform code. This module deploys a\nStorage Account.\n\n- An [Azure Storage Account](https://azure.microsoft.com/services/storage/) that gives the module the following:\n  - [Stock Account Name](https://azure.microsoft.com/services/storage/)  with the value specified in the `storage_account_name`  output variable.\n  - [Storage Account Tier](https://azure.microsoft.com/services/storage/)  with the value specified in the `\"storage_account_account_tier`  output variable.\n  - [Storage Account Kind](https://azure.microsoft.com/services/storage/)  with the value specified in the `\"storage_account_account_kind`  output variable.\n  - [Storage Container](https://azure.microsoft.com/services/storage/)  with the value specified in the `\"storage_container_name`  output variable.\n\nCheck out [test/azure/terraform_azure_storage_example_test.go](/test/azure/terraform_azure_storage_example_test.go) to see how you can write\nautomated tests for this module.\n\nNote that the Storage Account in this module don't actually do anything; it just runs the resources for\ndemonstration purposes.\n\n**WARNING**: This module and the automated tests for it deploy real resources into your Azure account which can cost you\nmoney. The resources are all part of the [Azure Free Account](https://azure.microsoft.com/free/), so if you haven't used that up,\nit should be free, but you are completely responsible for all Azure charges.\n\n## Running this module manually\n\n1. Sign up for [Azure](https://azure.microsoft.com/)\n1. Configure your Azure credentials using one of the [supported methods for Azure CLI\n   tools](https://docs.microsoft.com/cli/azure/azure-cli-configuration?view=azure-cli-latest)\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`\n1. Ensure [environment variables](../README.md#review-environment-variables) are available\n1. Run `terraform init`\n1. Run `terraform apply`\n1. When you're done, run `terraform destroy`\n\n## Running automated tests against this module\n\n1. Sign up for [Azure](https://azure.microsoft.com/)\n1. Configure your Azure credentials using one of the [supported methods for Azure CLI\n   tools](https://docs.microsoft.com/cli/azure/azure-cli-configuration?view=azure-cli-latest)\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`\n1. Configure your TerraTest [Go test environment](../README.md)\n1. `cd test/azure`\n1. `go build terraform_azure_storage_example_test.go`\n1. `go test -v -run TestTerraformAzureStorageExample`\n"
  },
  {
    "path": "examples/azure/terraform-azure-storage-example/main.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY A STORAGE ACCOUNT SET\n# This is an example of how to deploy a Storage Account.\n# ---------------------------------------------------------------------------------------------------------------------\n# See test/azure/terraform_azure_storage_example_test.go for how to write automated tests for this code.\n# ---------------------------------------------------------------------------------------------------------------------\n\nprovider \"azurerm\" {\n  features {}\n}\n\n# PIN TERRAFORM VERSION\n\nterraform {\n  # This module is now only being tested with Terraform 0.13.x. However, to make upgrading easier, we are setting\n  # 0.12.26 as the minimum version, as that version added support for required_providers with source URLs, making it\n  # forwards compatible with 0.13.x code.\n  required_version = \">= 0.12.26\"\n  required_providers {\n    azurerm = {\n      version = \"~> 2.20\"\n      source  = \"hashicorp/azurerm\"\n    }\n  }\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY A RESOURCE GROUP\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"azurerm_resource_group\" \"resource_group\" {\n  name     = \"terratest-storage-rg-${var.postfix}\"\n  location = var.location\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY A STORAGE ACCOUNT\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"azurerm_storage_account\" \"storage_account\" {\n  name                     = \"storage${var.postfix}\"\n  resource_group_name      = azurerm_resource_group.resource_group.name\n  location                 = azurerm_resource_group.resource_group.location\n  account_kind             = var.storage_account_kind\n  account_tier             = var.storage_account_tier\n  account_replication_type = var.storage_replication_type\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# ADD A CONTAINER TO THE STORAGE ACCOUNT\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"azurerm_storage_container\" \"container\" {\n  name                  = \"container1\"\n  storage_account_name  = azurerm_storage_account.storage_account.name\n  container_access_type = var.container_access_type\n}\n\n"
  },
  {
    "path": "examples/azure/terraform-azure-storage-example/outputs.tf",
    "content": "output \"resource_group_name\" {\n  value = azurerm_resource_group.resource_group.name\n}\n\noutput \"storage_account_name\" {\n  value = azurerm_storage_account.storage_account.name\n}\n\noutput \"storage_account_account_tier\" {\n  value = azurerm_storage_account.storage_account.account_tier\n}\n\noutput \"storage_account_account_kind\" {\n  value = azurerm_storage_account.storage_account.account_kind\n}\n\noutput \"storage_container_name\" {\n  value = azurerm_storage_container.container.name\n}\n"
  },
  {
    "path": "examples/azure/terraform-azure-storage-example/variables.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# ENVIRONMENT VARIABLES\n# Define these secrets as environment variables\n# ---------------------------------------------------------------------------------------------------------------------\n\n# ARM_CLIENT_ID\n# ARM_CLIENT_SECRET\n# ARM_SUBSCRIPTION_ID\n# ARM_TENANT_ID\n\n# ---------------------------------------------------------------------------------------------------------------------\n# REQUIRED PARAMETERS\n# You must provide a value for each of these parameters.\n# ---------------------------------------------------------------------------------------------------------------------\n\n# ---------------------------------------------------------------------------------------------------------------------\n# OPTIONAL PARAMETERS\n# These parameters have reasonable defaults.\n# ---------------------------------------------------------------------------------------------------------------------\n\nvariable \"location\" {\n  description = \"The location to set for the storage account.\"\n  type        = string\n  default     = \"East US\"\n}\n\nvariable \"storage_account_kind\" {\n  description = \"The kind of storage account to set\"\n  type        = string\n  default     = \"StorageV2\"\n}\n\nvariable \"storage_account_tier\" {\n  description = \"The tier of storage account to set\"\n  type        = string\n  default     = \"Standard\"\n}\n\nvariable \"storage_replication_type\" {\n  description = \"The replication type of storage account to set\"\n  type        = string\n  default     = \"GRS\"\n}\n\nvariable \"container_access_type\" {\n  description = \"The replication type of storage account to set\"\n  type        = string\n  default     = \"private\"\n}\n\nvariable \"postfix\" {\n  description = \"A postfix string to centrally mitigate resource name collisions\"\n  type        = string\n  default     = \"resource\"\n}\n\n"
  },
  {
    "path": "examples/azure/terraform-azure-synapse-example/README.md",
    "content": "# Terraform Azure Synapse Example\n\nThis folder contains a Terraform module that deploys resources in [Azure](https://azure.microsoft.com/) to demonstrate how you can use Terratest to write automated tests for your Azure Terraform code.\nThis module deploys below resource:\n\n- A [Azure Synapse Analytics](https://azure.microsoft.com/en-us/products/synapse-analytics/).\n\nCheck out [test/azure/terraform_azure_synapse_example_test.go](./../../../test/azure/terraform_azure_synapse_example_test.go) to see how you can write automated tests for this module and validate the configuration of the parameters and options.\n\n**WARNING**: This module and the automated tests for it deploy real resources into your Azure account which can cost you money.\n\n## Running this module manually\n\n1. Sign up for [Azure](https://azure.microsoft.com/).\n1. Configure your Azure credentials using one of the [supported methods for Azure CLI\n   tools](https://docs.microsoft.com/en-us/cli/azure/azure-cli-configuration?view=azure-cli-latest)\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`.\n1. Ensure [environment variables](../README.md#review-environment-variables) are available\n1. Run `terraform init`\n1. Run `terraform apply`\n1. When you're done, run `terraform destroy`.\n\n## Running automated tests against this module\n\n1. Sign up for [Azure](https://azure.microsoft.com/)\n1. Configure your Azure credentials using one of the [supported methods for Azure CLI\n   tools](https://docs.microsoft.com/en-us/cli/azure/azure-cli-configuration?view=azure-cli-latest)\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`\n1. Configure your Terratest [Go test environment](../README.md)\n1. `cd test/azure`\n1. `go build terraform_azure_synapse_example_test.go`\n1. `go test -v -timeout 60m -tags azure -run TestTerraformAzureSynapseExample`\n"
  },
  {
    "path": "examples/azure/terraform-azure-synapse-example/main.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY AN AZURE Synapse Analytics\n# This is an example of how to deploy an AZURE Synapse Analytics\n# See test/terraform_azure_example_test.go for how to write automated tests for this code.\n# ---------------------------------------------------------------------------------------------------------------------\n\n\n# ---------------------------------------------------------------------------------------------------------------------\n# CONFIGURE OUR AZURE CONNECTION\n# ---------------------------------------------------------------------------------------------------------------------\n\nprovider \"azurerm\" {\n  features {}\n}\n\nterraform {\n  required_providers {\n    azurerm = {\n      version = \"~>2.93.0\"\n      source  = \"hashicorp/azurerm\"\n    }\n  }\n}\n# ---------------------------------------------------------------------------------------------------------------------\n# CREATE RANDOM PASSWORD\n# ---------------------------------------------------------------------------------------------------------------------\n\n# Random password is used as an example to simplify the deployment and improve the security of the database.\n# This is not as a production recommendation as the password is stored in the Terraform state file.\nresource \"random_password\" \"password\" {\n  length           = 16\n  override_special = \"-_%@\"\n  min_upper        = \"1\"\n  min_lower        = \"1\"\n  min_numeric      = \"1\"\n  min_special      = \"1\"\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY A RESOURCE GROUP\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"azurerm_resource_group\" \"synapse_rg\" {\n  name     = \"terratest-synapse-${var.postfix}\"\n  location = var.location\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY A STORAGE ACCOUNT\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"azurerm_storage_account\" \"storage_account\" {\n  name                     = \"storage${var.postfix}\"\n  resource_group_name      = azurerm_resource_group.synapse_rg.name\n  location                 = azurerm_resource_group.synapse_rg.location\n  account_kind             = var.storage_account_kind\n  account_tier             = var.storage_account_tier\n  account_replication_type = var.storage_account_replication_type\n}\n\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY A DATA LAKE GEN2\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"azurerm_storage_data_lake_gen2_filesystem\" \"dl_gen2\" {\n  name               = \"dlgen2-${var.postfix}\"\n  storage_account_id = azurerm_storage_account.storage_account.id\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY A SYNAPSE WORKSPACE\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"azurerm_synapse_workspace\" \"synapse_workspace\" {\n  name                                 = \"mysynapse${var.postfix}\"\n  resource_group_name                  = azurerm_resource_group.synapse_rg.name\n  location                             = azurerm_resource_group.synapse_rg.location\n  storage_data_lake_gen2_filesystem_id = azurerm_storage_data_lake_gen2_filesystem.dl_gen2.id\n  sql_administrator_login              = var.synapse_sql_user\n  sql_administrator_login_password     = random_password.password.result\n  managed_virtual_network_enabled      = true\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY A SYNAPSE SQL POOL\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"azurerm_synapse_sql_pool\" \"synapse_pool\" {\n  name                 = \"sqlpool${var.postfix}\"\n  synapse_workspace_id = azurerm_synapse_workspace.synapse_workspace.id\n  sku_name             = var.synapse_sqlpool_sku_name\n  create_mode          = var.synapse_sqlpool_create_mode\n}"
  },
  {
    "path": "examples/azure/terraform-azure-synapse-example/outputs.tf",
    "content": "output \"resource_group_name\" {\n  value = azurerm_resource_group.synapse_rg.name\n}\n\noutput \"synapse_storage_name\" {\n  value = azurerm_storage_account.storage_account.name\n}\n\noutput \"synapse_dlgen2_name\" {\n  value = azurerm_storage_data_lake_gen2_filesystem.dl_gen2.name\n}\n\noutput \"synapse_workspace_name\" {\n  value = azurerm_synapse_workspace.synapse_workspace.name\n}\n\noutput \"synapse_sqlpool_name\" {\n  value = azurerm_synapse_sql_pool.synapse_pool.name\n}\n"
  },
  {
    "path": "examples/azure/terraform-azure-synapse-example/variables.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# ENVIRONMENT VARIABLES\n# Define these secrets as environment variables\n# ---------------------------------------------------------------------------------------------------------------------\n\n# ARM_CLIENT_ID\n# ARM_CLIENT_SECRET\n# ARM_SUBSCRIPTION_ID\n# ARM_TENANT_ID\n\n# ---------------------------------------------------------------------------------------------------------------------\n# REQUIRED PARAMETERS\n# You must provide a value for each of these parameters.\n# ---------------------------------------------------------------------------------------------------------------------\n\n# ---------------------------------------------------------------------------------------------------------------------\n# OPTIONAL PARAMETERS\n# These parameters have reasonable defaults.\n# ---------------------------------------------------------------------------------------------------------------------\n\nvariable \"location\" {\n  description = \"The supported azure location where the resource exists\"\n  type        = string\n  default     = \"West US2\"\n}\n\nvariable \"storage_account_kind\" {\n  description = \"The kind of storage account to set\"\n  type        = string\n  default     = \"StorageV2\"\n}\n\nvariable \"storage_account_tier\" {\n  description = \"The tier of storage account to set\"\n  type        = string\n  default     = \"Standard\"\n}\n\nvariable \"storage_account_replication_type\" {\n  description = \"The replication type of storage account to set\"\n  type        = string\n  default     = \"GRS\"\n}\n\nvariable \"synapse_sql_user\" {\n  description = \"The sql pool user password for synapse\"\n  type        = string\n  default     = \"sqladminuser\"\n}\n\nvariable \"synapse_sqlpool_sku_name\" {\n  description = \"The sku name for the synapse sql pool\"\n  type        = string\n  default     = \"DW100c\"\n}\n\nvariable \"synapse_sqlpool_create_mode\" {\n  description = \"The create mode for the synapse sql pool\"\n  type        = string\n  default     = \"Default\"\n}\n\nvariable \"postfix\" {\n  description = \"A postfix string to centrally mitigate resource name collisions.\"\n  type        = string\n  default     = \"resource\"\n}"
  },
  {
    "path": "examples/azure/terraform-azure-vm-example/README.md",
    "content": "# Terraform Azure Virtual Machine Example\n\nThis folder contains a complete Terraform VM module that deploys resources in [Azure](https://azure.microsoft.com/) to demonstrate\nhow you can use Terratest to write automated tests for your Azure Virtual Machine Terraform code. This module deploys these resources:\n\n- A [Virtual Machine](https://azure.microsoft.com/services/virtual-machines/) and gives that VM the following resources:\n  - [Virtual Machine](https://docs.microsoft.com/azure/virtual-machines/) with the name specified in the `vm_name` variable.\n  - [Managed Disk](https://docs.microsoft.com/azure/virtual-machines/managed-disks-overview) with the name specified in the `managed_disk_name` variable.\n  - [Availability Set](https://docs.microsoft.com/azure/virtual-machines/availability) with the name specified in the `availability_set_name` variable.\n- A [Virtual Network](https://azure.microsoft.com/services/virtual-network/) module that contains the following resources:\n  - [Virtual Network](https://docs.microsoft.com/azure/virtual-network/) with the name specified in the `virtual_network_name` variable.\n  - [Subnet](https://docs.microsoft.com/rest/api/virtualnetwork/subnets) with the name specified in the `subnet_name` variable.\n  - [Public Address](https://docs.microsoft.com/azure/virtual-network/public-ip-addresses) with the name specified in the `public_ip_name` variable.\n  - [Network Interface](https://docs.microsoft.com/azure/virtual-network/virtual-network-network-interface) with the name specified in the `network_interface_name` variable.\n\nCheck out [test/azure/terraform_azure_vm_test.go](/test/azure/terraform_azure_vm_example_test.go) to see how you can write\nautomated tests for this module.\n\nNote that the Virtual Machine module creates a Microsoft Windows Server Image with a managed disk, availability set and network configuration for demonstration purposes.\n\n**WARNING**: This module and the automated tests for it deploy real resources into your Azure account which can cost you\nmoney. The resources are all part of the [Azure Free Account](https://azure.microsoft.com/free/), so if you haven't used that up,\nit should be free, but you are completely responsible for all Azure charges.\n\n## Running this module manually\n\n1. Sign up for [Azure](https://azure.microsoft.com/)\n1. Configure your Azure credentials using one of the [supported methods for Azure CL\n   tools](https://docs.microsoft.com/cli/azure/azure-cli-configuration?view=azure-cli-latest)\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`\n1. Ensure [environment variables](../README.md#review-environment-variables) are available\n1. Run `terraform init`\n1. Run `terraform apply`\n1. When you're done, run `terraform destroy`\n\n## Running automated tests against this module\n\n1. Sign up for [Azure](https://azure.microsoft.com/)\n1. Configure your Azure credentials using one of the [supported methods for Azure CLI\n   tools](https://docs.microsoft.com/cli/azure/azure-cli-configuration?view=azure-cli-latest)\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`\n1. Configure your Terratest [Go test environment](../README.md)\n1. `cd test/azure`\n1. `go build terraform_azure_vm_test.go`\n1. `go test -run -v -timeout 20m TestTerraformAzureVmExample`\n"
  },
  {
    "path": "examples/azure/terraform-azure-vm-example/main.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY AN ADVANCED AZURE VIRTUAL MACHINE\n# This is an advanced example of how to deploy an Azure Virtual Machine in an availability set, managed disk \n# and networking with a public IP.\n# ---------------------------------------------------------------------------------------------------------------------\n# See test/azure/terraform_azure_vm_example_test.go for how to write automated tests for this code.\n# ---------------------------------------------------------------------------------------------------------------------\n\nprovider \"azurerm\" {\n  features {}\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# PIN TERRAFORM VERSION\n# ---------------------------------------------------------------------------------------------------------------------\n\nterraform {\n  # This module is now only being tested with Terraform 0.13.x. However, to make upgrading easier, we are setting\n  # 0.12.26 as the minimum version, as that version added support for required_providers with source URLs, making it\n  # forwards compatible with 0.13.x code.\n  required_version = \">= 0.12.26\"\n  required_providers {\n    azurerm = {\n      version = \"~> 2.50\"\n      source  = \"hashicorp/azurerm\"\n    }\n  }\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY A RESOURCE GROUP\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"azurerm_resource_group\" \"vm_rg\" {\n  name     = \"terratest-vm-rg-${var.postfix}\"\n  location = var.location\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY NETWORK RESOURCES\n# This network includes a public address for integration test demonstration purposes\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"azurerm_virtual_network\" \"vnet\" {\n  name                = \"vnet-${var.postfix}\"\n  address_space       = [\"10.0.0.0/16\"]\n  location            = azurerm_resource_group.vm_rg.location\n  resource_group_name = azurerm_resource_group.vm_rg.name\n}\n\nresource \"azurerm_subnet\" \"subnet\" {\n  name                 = \"subnet-${var.postfix}\"\n  resource_group_name  = azurerm_resource_group.vm_rg.name\n  virtual_network_name = azurerm_virtual_network.vnet.name\n  address_prefixes     = [var.subnet_prefix]\n}\n\nresource \"azurerm_public_ip\" \"pip\" {\n  name                    = \"pip-${var.postfix}\"\n  resource_group_name     = azurerm_resource_group.vm_rg.name\n  location                = azurerm_resource_group.vm_rg.location\n  allocation_method       = \"Static\"\n  ip_version              = \"IPv4\"\n  sku                     = \"Standard\"\n  idle_timeout_in_minutes = \"4\"\n}\n\n# Public and Private IPs assigned to one NIC for test demonstration purposes\nresource \"azurerm_network_interface\" \"nic\" {\n  name                = \"nic-${var.postfix}\"\n  location            = azurerm_resource_group.vm_rg.location\n  resource_group_name = azurerm_resource_group.vm_rg.name\n\n  ip_configuration {\n    name                          = \"terratestconfiguration1\"\n    subnet_id                     = azurerm_subnet.subnet.id\n    private_ip_address_allocation = \"Static\"\n    private_ip_address            = var.private_ip\n    public_ip_address_id          = azurerm_public_ip.pip.id\n  }\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY AN AVAILABILITY SET\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"azurerm_availability_set\" \"avs\" {\n  name                        = \"avs-${var.postfix}\"\n  location                    = azurerm_resource_group.vm_rg.location\n  resource_group_name         = azurerm_resource_group.vm_rg.name\n  platform_fault_domain_count = 2\n  managed                     = true\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY VIRTUAL MACHINE\n# This VM does not actually do anything and is the smallest size VM available with a Windows image\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"azurerm_virtual_machine\" \"vm_example\" {\n  name                             = \"vm-${var.postfix}\"\n  location                         = azurerm_resource_group.vm_rg.location\n  resource_group_name              = azurerm_resource_group.vm_rg.name\n  network_interface_ids            = [azurerm_network_interface.nic.id]\n  availability_set_id              = azurerm_availability_set.avs.id\n  vm_size                          = var.vm_size\n  license_type                     = var.vm_license_type\n  delete_os_disk_on_termination    = true\n  delete_data_disks_on_termination = true\n\n  storage_image_reference {\n    publisher = var.vm_image_publisher\n    offer     = var.vm_image_offer\n    sku       = var.vm_image_sku\n    version   = var.vm_image_version\n  }\n\n  storage_os_disk {\n    name              = \"osdisk-${var.postfix}\"\n    caching           = \"ReadWrite\"\n    create_option     = \"FromImage\"\n    managed_disk_type = var.disk_type\n  }\n\n  os_profile {\n    computer_name  = \"vm-${var.postfix}\"\n    admin_username = var.user_name\n    admin_password = random_password.rand.result\n  }\n\n  os_profile_windows_config {\n    provision_vm_agent = true\n  }\n\n  tags = {\n    \"Version\"     = \"0.0.1\"\n    \"Environment\" = \"dev\"\n  }\n\n  depends_on = [random_password.rand]\n}\n\n# Random password is used as an example to simplify the deployment and improve the security of the remote VM.\n# This is not as a production recommendation as the password is stored in the Terraform state file.\nresource \"random_password\" \"rand\" {\n  length           = 16\n  override_special = \"-_%@\"\n  min_upper        = \"1\"\n  min_lower        = \"1\"\n  min_numeric      = \"1\"\n  min_special      = \"1\"\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# ATTACH A MANAGED DISK TO THE VIRTUAL MACHINE\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"azurerm_managed_disk\" \"disk\" {\n  name                 = \"disk-${var.postfix}\"\n  location             = azurerm_resource_group.vm_rg.location\n  resource_group_name  = azurerm_resource_group.vm_rg.name\n  storage_account_type = var.disk_type\n  create_option        = \"Empty\"\n  disk_size_gb         = 10\n}\n\nresource \"azurerm_virtual_machine_data_disk_attachment\" \"vm_disk\" {\n  managed_disk_id    = azurerm_managed_disk.disk.id\n  virtual_machine_id = azurerm_virtual_machine.vm_example.id\n  caching            = \"ReadWrite\"\n  lun                = 10\n}\n"
  },
  {
    "path": "examples/azure/terraform-azure-vm-example/outputs.tf",
    "content": "\noutput \"availability_set_name\" {\n  value = azurerm_availability_set.avs.name\n}\n\noutput \"managed_disk_name\" {\n  value = azurerm_managed_disk.disk.name\n}\n\noutput \"managed_disk_type\" {\n  value = azurerm_managed_disk.disk.storage_account_type\n}\n\noutput \"network_interface_name\" {\n  value = azurerm_network_interface.nic.name\n}\n\noutput \"os_disk_name\" {\n  value = azurerm_virtual_machine.vm_example.storage_os_disk[0].name\n}\n\noutput \"private_ip\" {\n  value = azurerm_network_interface.nic.ip_configuration[0].private_ip_address\n}\n\noutput \"public_ip_name\" {\n  value = azurerm_public_ip.pip.name\n}\n\noutput \"resource_group_name\" {\n  value = azurerm_resource_group.vm_rg.name\n}\n\noutput \"subnet_name\" {\n  value = azurerm_subnet.subnet.name\n}\n\noutput \"virtual_network_name\" {\n  value = azurerm_virtual_network.vnet.name\n}\n\noutput \"vm_admin_username\" {\n  value = nonsensitive(azurerm_virtual_machine.vm_example.os_profile[*].admin_username)\n}\n\noutput \"vm_image_sku\" {\n  value = azurerm_virtual_machine.vm_example.storage_image_reference[*].sku\n}\n\noutput \"vm_image_version\" {\n  value = azurerm_virtual_machine.vm_example.storage_image_reference[*].version\n}\n\noutput \"vm_name\" {\n  value = azurerm_virtual_machine.vm_example.name\n}\n\noutput \"vm_size\" {\n  value = azurerm_virtual_machine.vm_example.vm_size\n}\n\noutput \"vm_tags\" {\n  value = azurerm_virtual_machine.vm_example.tags\n}\n"
  },
  {
    "path": "examples/azure/terraform-azure-vm-example/variables.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# ENVIRONMENT VARIABLES\n# Define these secrets as environment variables\n# ---------------------------------------------------------------------------------------------------------------------\n\n# ARM_CLIENT_ID\n# ARM_CLIENT_SECRET\n# ARM_SUBSCRIPTION_ID\n# ARM_TENANT_ID\n\n# ---------------------------------------------------------------------------------------------------------------------\n# REQUIRED PARAMETERS\n# You must provide a value for each of these parameters.\n# ---------------------------------------------------------------------------------------------------------------------\n\n# ---------------------------------------------------------------------------------------------------------------------\n# OPTIONAL PARAMETERS\n# These parameters have reasonable defaults.\n# ---------------------------------------------------------------------------------------------------------------------\n\nvariable \"disk_type\" {\n  description = \"The type of the Virtual Machine disks\"\n  type        = string\n  default     = \"Standard_LRS\"\n}\n\nvariable \"location\" {\n  description = \"The Azure location where to deploy your resources too\"\n  type        = string\n  default     = \"East US\"\n}\n\nvariable \"postfix\" {\n  description = \"A postfix string to centrally mitigate resource name collisions\"\n  type        = string\n  default     = \"resource\"\n}\n\nvariable \"private_ip\" {\n  description = \"The Static Private IP for the Internal NIC\"\n  type        = string\n  default     = \"10.0.17.4\"\n}\n\nvariable \"subnet_prefix\" {\n  description = \"The subnet range of IPs for the Virtual Network\"\n  type        = string\n  default     = \"10.0.17.0/24\"\n}\n\nvariable \"user_name\" {\n  description = \"The username to be provisioned into the vm\"\n  type        = string\n  default     = \"testadmin\"\n}\n\n# Small Windows Server Image, available with Azure Free Account\nvariable \"vm_image_publisher\" {\n  description = \"The storage image reference Publisher from which the VM is created\"\n  type        = string\n  default     = \"MicrosoftWindowsServer\"\n}\n\nvariable \"vm_image_offer\" {\n  description = \"The storage image reference Offer from which the VM is created\"\n  type        = string\n  default     = \"WindowsServer\"\n}\n\nvariable \"vm_image_sku\" {\n  description = \"The storage image reference SKU from which the VM is created\"\n  type        = string\n  default     = \"2019-Datacenter-Core-smalldisk\"\n}\n\nvariable \"vm_image_version\" {\n  description = \"The storage image reference Version from which the VM is created\"\n  type        = string\n  default     = \"latest\"\n}\n\nvariable \"vm_license_type\" {\n  description = \"The License Type from which the VM is created\"\n  type        = string\n  default     = \"Windows_Server\"\n}\n\nvariable \"vm_size\" {\n  description = \"The Azure VM Size of the VM\"\n  type        = string\n  default     = \"Standard_B1s\"\n}\n"
  },
  {
    "path": "examples/docker-compose-stdout-example/Dockerfile",
    "content": "# website::tag::1:: Build a simple Docker image that contains a text file with the contents \"Hello, World!\"\nFROM ubuntu:20.04\nCOPY ./bash_script.sh /usr/local/bin/bash_script.sh\n"
  },
  {
    "path": "examples/docker-compose-stdout-example/bash_script.sh",
    "content": "#!/bin/bash\nset -e\n\necho \"stdout: message\"\n>&2 echo -e \"stderr: error\"\n"
  },
  {
    "path": "examples/docker-compose-stdout-example/docker-compose.yml",
    "content": "version: '2.0'\nservices:\n  bash_script:\n    build:\n      context: .\n    entrypoint: bash_script.sh\n"
  },
  {
    "path": "examples/docker-hello-world-example/Dockerfile",
    "content": "# website::tag::1:: Build a simple Docker image that contains a text file with the contents \"Hello, World!\"\nFROM ubuntu:18.04\nRUN echo 'Hello, World!' > /test.txt\n"
  },
  {
    "path": "examples/docker-hello-world-example/README.md",
    "content": "# Docker \"Hello, World\" Example\n\nThis folder contains a `Dockerfile` to build a very simple Docker image—one which contains a text file with the\ntext \"Hello, World\"!—to demonstrate how you can use Terratest to write automated tests for your Docker images. \n\nCheck out [test/docker_hello_world_example_test.go](/test/docker_hello_world_example_test.go) to see how you can write\nautomated tests for this simple Docker image.\n\n\n\n\n## Building the Docker container\n\n1. Install [Docker](https://www.docker.com/) and make sure it's on your `PATH`.\n1. Run `docker build -t gruntwork/docker-hello-world-example .`.\n1. Run `docker run -it --rm gruntwork/docker-hello-world-example cat /test.txt`.\n1. You should see the text \"Hello, World!\"\n\n\n\n\n## Running automated tests against the Docker container\n\n1. Install [Docker](https://www.docker.com/) and make sure it's on your `PATH`.\n1. Install [Golang](https://golang.org/) and make sure this code is checked out into your `GOPATH`.\n1. `cd test`\n1. `go test -v -run TestDockerHelloWorldExample`\n"
  },
  {
    "path": "examples/helm-basic-example/Chart.yaml",
    "content": "apiVersion: v1\nname: helm-basic-example\ndescription: A minimal Helm chart to demonstrate how to use terratest to test helm charts\nversion: 0.0.1\n"
  },
  {
    "path": "examples/helm-basic-example/README.md",
    "content": "# Helm Basic Example\n\nThis folder contains a minimal helm chart to demonstrate how you can use Terratest to test your helm charts.\n\nThere are two kinds of tests you can perform on a helm chart:\n\n- Helm Template tests are tests designed to test the logic of the templates. These tests should run `helm template` with\n  various input values and parse the yaml to validate any logic embedded in the templates (e.g by reading them in using\n  client-go). Since templates are not statically typed, the goal of these tests is to promote fast cycle time\n- Helm Integration tests are tests that are designed to deploy the infrastructure and validate that it actually\n  works as expected. If you consider the templates to be syntactic tests, these are semantic tests that validate the\n  behavior of the deployed resources.\n\nThe helm chart deploys a single replica `Deployment` resource given the container image spec and a `Service` that\nexposes it. This chart requires the `containerImageRepo` and `containerImageTag` input values.\n\nSee the corresponding terratest code for an example of how to test this chart:\n\n- [helm_basic_example_template_test.go](/test/helm_basic_example_template_test.go): the template tests for this chart.\n- [helm_basic_example_integration_test.go](/test/helm_basic_example_integration_test.go): the integration test for this\n  chart. This test will deploy the Helm Chart and verify the `Service` endpoint.\n\n## Running automated tests against this Helm Chart\n\n1. Install and setup [helm](https://docs.helm.sh/using_helm/#installing-helm)\n1. Install [Golang](https://golang.org/) and make sure this code is checked out into your `GOPATH`.\n1. `cd test`\n1. `dep ensure`\n1. `go test -v -tags helm -run TestHelmBasicExampleTemplate` for the template test\n1. `go test -v -tags helm -run TestHelmBasicExampleDeployment` for the integration test\n\n**NOTE**: we have build tags to differentiate kubernetes tests from non-kubernetes tests, and further differentiate helm\ntests. This is done because minikube is heavy and can interfere with docker related tests in terratest. Similarly, helm\ncan overload the minikube system and thus interfere with the other kubernetes tests. Specifically, many of the tests\nstart to fail with `connection refused` errors from `minikube`. To avoid overloading the system, we run the kubernetes\ntests and helm tests separately from the others. This may not be necessary if you have a sufficiently powerful machine.\nWe recommend at least 4 cores and 16GB of RAM if you want to run all the tests together.\n"
  },
  {
    "path": "examples/helm-basic-example/templates/_helpers.tpl",
    "content": "{{/* vim: set filetype=mustache: */}}\n{{/*\nExpand the name of the chart.\n*/}}\n{{- define \"helm-basic-example.name\" -}}\n{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix \"-\" -}}\n{{- end -}}\n\n{{/*\nCreate a default fully qualified app name.\nWe truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).\nIf release name contains chart name it will be used as a full name.\n*/}}\n{{- define \"helm-basic-example.fullname\" -}}\n{{- if .Values.fullnameOverride -}}\n{{- .Values.fullnameOverride | trunc 63 | trimSuffix \"-\" -}}\n{{- else -}}\n{{- $name := default .Chart.Name .Values.nameOverride -}}\n{{- if contains $name .Release.Name -}}\n{{- .Release.Name | trunc 63 | trimSuffix \"-\" -}}\n{{- else -}}\n{{- printf \"%s-%s\" .Release.Name $name | trunc 63 | trimSuffix \"-\" -}}\n{{- end -}}\n{{- end -}}\n{{- end -}}\n\n{{/*\nCreate chart name and version as used by the chart label.\n*/}}\n{{- define \"helm-basic-example.chart\" -}}\n{{- printf \"%s-%s\" .Chart.Name .Chart.Version | replace \"+\" \"_\" | trunc 63 | trimSuffix \"-\" -}}\n{{- end -}}\n"
  },
  {
    "path": "examples/helm-basic-example/templates/deployment.yaml",
    "content": "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: {{ include \"helm-basic-example.fullname\" . }}\n  namespace: {{ .Release.Namespace }}\n  labels:\n    # These labels are required by helm. You can read more about required labels in the chart best pracices guide:\n    # https://docs.helm.sh/chart_best_practices/#standard-labels\n    helm.sh/chart: {{ include \"helm-basic-example.chart\" . }}\n    app.kubernetes.io/name: {{ include \"helm-basic-example.name\" . }}\n    app.kubernetes.io/instance: {{ .Release.Name }}\n    app.kubernetes.io/managed-by: {{ .Release.Service }}\nspec:\n  replicas: 1\n  selector:\n    matchLabels:\n      app.kubernetes.io/name: {{ include \"helm-basic-example.name\" . }}\n      app.kubernetes.io/instance: {{ .Release.Name }}\n  template:\n    metadata:\n      labels:\n        app.kubernetes.io/name: {{ include \"helm-basic-example.name\" . }}\n        app.kubernetes.io/instance: {{ .Release.Name }}\n    spec:\n      containers:\n        - name: app\n          {{- $repo := required \"containerImageRepo is required\" .Values.containerImageRepo }}\n          {{- $tag := required \"containerImageTag is required\" .Values.containerImageTag }}\n          image: \"{{ $repo }}:{{ $tag }}\"\n          ports:\n          - containerPort: 80\n"
  },
  {
    "path": "examples/helm-basic-example/templates/service.yaml",
    "content": "apiVersion: v1\nkind: Service\nmetadata:\n  name: {{ include \"helm-basic-example.fullname\" . }}\n  labels:\n    # These labels are required by helm. You can read more about required labels in the chart best practices guide:\n    # https://docs.helm.sh/chart_best_practices/#standard-labels\n    helm.sh/chart: {{ include \"helm-basic-example.chart\" . }}\n    app.kubernetes.io/name: {{ include \"helm-basic-example.name\" . }}\n    app.kubernetes.io/instance: {{ .Release.Name }}\n    app.kubernetes.io/managed-by: {{ .Release.Service }}\nspec:\n  selector:\n    app.kubernetes.io/name: {{ include \"helm-basic-example.name\" . }}\n    app.kubernetes.io/instance: {{ .Release.Name }}\n  type: NodePort\n  ports:\n  - protocol: TCP\n    targetPort: 80\n    port: 80\n"
  },
  {
    "path": "examples/helm-basic-example/values.yaml",
    "content": "# This chart purposefully does not provide any values, to demonstrate how to test required values.\n# Note that the following two values must be specified if you wish to deploy this chart:\n\n# containerImageRepo is a string that describes the image repository to pull the container image from.\n# containerImageRepo: nginx\n\n# containerImageTag is a string that describes the image tag to use when pulling the container image.\n# containerImageTag: v1.15.4\n"
  },
  {
    "path": "examples/helm-dependency-example/.gitignore",
    "content": "charts/\nrequirements.lock"
  },
  {
    "path": "examples/helm-dependency-example/Chart.yaml",
    "content": "apiVersion: v1\nname: helm-dependency-example\ndescription: A minimal Helm chart to demonstrate how to use terratest to test helm charts with dependency\nversion: 0.0.1\ndependencies:\n  - name: helm-basic-example\n    alias: basic\n    repository: file://../helm-basic-example\n    condition: basic.enabled\n    version: 0.0.1\n"
  },
  {
    "path": "examples/helm-dependency-example/README.md",
    "content": "# Helm Dependency Example\n\nThis folder contains a minimal helm chart to demonstrate how you can use Terratest to test your helm charts with dependencies.\n\nThere are two kinds of tests you can perform on a helm chart:\n\n- Helm Template tests are tests designed to test the logic of the templates. These tests should run `helm template` with\n  various input values and parse the yaml to validate any logic embedded in the templates (e.g by reading them in using\n  client-go). Since templates are not statically typed, the goal of these tests is to promote fast cycle time\n\nThe helm chart deploys a single replica `Deployment` resource given the container image spec and a `Service` that\nexposes it. This chart requires the `containerImageRepo` and `containerImageTag` input values.\n\nSee the corresponding terratest code for an example of how to test this chart:\n\n- [helm_basic_example_template_test.go](/test/helm_basic_example_template_test.go): the template tests for this chart.\n\n## Running automated tests against this Helm Chart\n\n1. Install and setup [helm](https://docs.helm.sh/using_helm/#installing-helm)\n1. Install [Golang](https://golang.org/) and make sure this code is checked out into your `GOPATH`.\n1. `cd test`\n1. `dep ensure`\n1. `go test -v -tags helm -run TestHelmDependencyExampleTemplate` for the template test\n\n**NOTE**: we have build tags to differentiate kubernetes tests from non-kubernetes tests, and further differentiate helm\ntests. This is done because minikube is heavy and can interfere with docker related tests in terratest. Similarly, helm\ncan overload the minikube system and thus interfere with the other kubernetes tests. Specifically, many of the tests\nstart to fail with `connection refused` errors from `minikube`. To avoid overloading the system, we run the kubernetes\ntests and helm tests separately from the others. This may not be necessary if you have a sufficiently powerful machine.\nWe recommend at least 4 cores and 16GB of RAM if you want to run all the tests together.\n"
  },
  {
    "path": "examples/helm-dependency-example/templates/_helpers.tpl",
    "content": "{{/* vim: set filetype=mustache: */}}\n{{/*\nExpand the name of the chart.\n*/}}\n{{- define \"helm-dependency-example.name\" -}}\n{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix \"-\" -}}\n{{- end -}}\n\n{{/*\nCreate a default fully qualified app name.\nWe truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).\nIf release name contains chart name it will be used as a full name.\n*/}}\n{{- define \"helm-dependency-example.fullname\" -}}\n{{- if .Values.fullnameOverride -}}\n{{- .Values.fullnameOverride | trunc 63 | trimSuffix \"-\" -}}\n{{- else -}}\n{{- $name := default .Chart.Name .Values.nameOverride -}}\n{{- if contains $name .Release.Name -}}\n{{- .Release.Name | trunc 63 | trimSuffix \"-\" -}}\n{{- else -}}\n{{- printf \"%s-%s\" .Release.Name $name | trunc 63 | trimSuffix \"-\" -}}\n{{- end -}}\n{{- end -}}\n{{- end -}}\n\n{{/*\nCreate chart name and version as used by the chart label.\n*/}}\n{{- define \"helm-dependency-example.chart\" -}}\n{{- printf \"%s-%s\" .Chart.Name .Chart.Version | replace \"+\" \"_\" | trunc 63 | trimSuffix \"-\" -}}\n{{- end -}}\n"
  },
  {
    "path": "examples/helm-dependency-example/templates/deployment.yaml",
    "content": "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: {{ include \"helm-dependency-example.fullname\" . }}\n  namespace: {{ .Release.Namespace }}\n  labels:\n    # These labels are required by helm. You can read more about required labels in the chart best pracices guide:\n    # https://docs.helm.sh/chart_best_practices/#standard-labels\n    helm.sh/chart: {{ include \"helm-dependency-example.chart\" . }}\n    app.kubernetes.io/name: {{ include \"helm-dependency-example.name\" . }}\n    app.kubernetes.io/instance: {{ .Release.Name }}\n    app.kubernetes.io/managed-by: {{ .Release.Service }}\nspec:\n  replicas: 1\n  selector:\n    matchLabels:\n      app.kubernetes.io/name: {{ include \"helm-dependency-example.name\" . }}\n      app.kubernetes.io/instance: {{ .Release.Name }}\n  template:\n    metadata:\n      labels:\n        app.kubernetes.io/name: {{ include \"helm-dependency-example.name\" . }}\n        app.kubernetes.io/instance: {{ .Release.Name }}\n    spec:\n      containers:\n        - name: app\n          {{- $repo := required \"containerImageRepo is required\" .Values.containerImageRepo }}\n          {{- $tag := required \"containerImageTag is required\" .Values.containerImageTag }}\n          image: \"{{ $repo }}:{{ $tag }}\"\n          ports:\n          - containerPort: 80\n"
  },
  {
    "path": "examples/helm-dependency-example/templates/service.yaml",
    "content": "apiVersion: v1\nkind: Service\nmetadata:\n  name: {{ include \"helm-dependency-example.fullname\" . }}\n  labels:\n    # These labels are required by helm. You can read more about required labels in the chart best practices guide:\n    # https://docs.helm.sh/chart_best_practices/#standard-labels\n    helm.sh/chart: {{ include \"helm-dependency-example.chart\" . }}\n    app.kubernetes.io/name: {{ include \"helm-dependency-example.name\" . }}\n    app.kubernetes.io/instance: {{ .Release.Name }}\n    app.kubernetes.io/managed-by: {{ .Release.Service }}\nspec:\n  selector:\n    app.kubernetes.io/name: {{ include \"helm-dependency-example.name\" . }}\n    app.kubernetes.io/instance: {{ .Release.Name }}\n  type: NodePort\n  ports:\n  - protocol: TCP\n    targetPort: 80\n    port: 80\n"
  },
  {
    "path": "examples/helm-dependency-example/values.yaml",
    "content": "# This chart purposefully does not provide any values, to demonstrate how to test required values.\n# Note that the following two values must be specified if you wish to deploy this chart:\n\n# containerImageRepo is a string that describes the image repository to pull the container image from.\n# containerImageRepo: nginx\n\n# containerImageTag is a string that describes the image tag to use when pulling the container image.\n# containerImageTag: v1.15.4\n"
  },
  {
    "path": "examples/kubernetes-basic-example/README.md",
    "content": "# Kubernetes Basic Example\n\nThis folder contains a minimal Kubernetes resource config file to demonstrate how you can use Terratest to write\nautomated tests for Kubernetes.\n\nThis resource file deploys an nginx container as a single pod deployment with a node port service attached to it.\n\nSee the corresponding terratest code for an example of how to test this resource config:\n- [kubernetes_basic_example_test.go](../../test/kubernetes_basic_example_test.go) for the most basic verification\n- [kubernetes_basic_example_service_check_test.go](../../test/kubernetes_basic_example_service_check_test.go) for a more\n  advanced version of checking the service.\n\n\n## Deploying the Kubernetes resource\n\n1. Setup a Kubernetes cluster. We recommend using a local version:\n    - [minikube](https://github.com/kubernetes/minikube)\n    - [Kubernetes on Docker For Mac](https://docs.docker.com/docker-for-mac/kubernetes/)\n    - [Kubernetes on Docker For Windows](https://docs.docker.com/docker-for-windows/kubernetes/)\n\n1. Install and setup [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/) to talk to the deployed\n   Kubernetes cluster.\n1. Run `kubectl apply -f nginx-deployment.yml`\n\n\n## Running automated tests against this Kubernetes deployment\n\n1. Setup a Kubernetes cluster. We recommend using a local version:\n    - [minikube](https://github.com/kubernetes/minikube)\n    - [Kubernetes on Docker For Mac](https://docs.docker.com/docker-for-mac/kubernetes/)\n    - [Kubernetes on Docker For Windows](https://docs.docker.com/docker-for-windows/kubernetes/)\n\n1. Install and setup [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/) to talk to the deployed\n   Kubernetes cluster.\n1. Install and setup [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/).\n1. Install [Golang](https://golang.org/) and make sure this code is checked out into your `GOPATH`.\n1. `cd test`\n1. `dep ensure`\n1. `go test -v -tags kubernetes -run TestKubernetesBasicExample`\n1. You can also run `TestKubernetesBasicExampleServiceCheck`\n"
  },
  {
    "path": "examples/kubernetes-basic-example/nginx-deployment.yml",
    "content": "---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: nginx-deployment\nspec:\n  selector:\n    matchLabels:\n      app: nginx\n  replicas: 1\n  template:\n    metadata:\n      labels:\n        app: nginx\n    spec:\n      containers:\n      - name: nginx\n        image: nginx:1.15.7\n        ports:\n        - containerPort: 80\n---\nkind: Service\napiVersion: v1\nmetadata:\n  name: nginx-service\nspec:\n  selector:\n    app: nginx\n  ports:\n  - protocol: TCP\n    targetPort: 80\n    port: 80\n  type: NodePort\n"
  },
  {
    "path": "examples/kubernetes-basic-example/podinfo-daemonset.yml",
    "content": "---\napiVersion: apps/v1\nkind: DaemonSet\nmetadata:\n  name: podinfo-deamonset\nspec:\n  selector:\n    matchLabels:\n      app: podinfo\n  template:\n    metadata:\n      labels:\n        app: podinfo\n    spec:\n      containers:\n      - name: podinfo\n        image: ghcr.io/stefanprodan/podinfo:6.3.0\n        ports:\n        - containerPort: 9898\n"
  },
  {
    "path": "examples/kubernetes-hello-world-example/README.md",
    "content": "# Kubernetes \"Hello, World\" Example\n\nThis folder contains the most minimal Kubernetes resource config—which deploys a simple webapp that responds with\n\"Hello, World!\"—to demonstrate how you can use Terratest to write automated tests for Kubernetes.\n\nCheck out [test/kubernetes_hello_world_example_test.go](/test/kubernetes_hello_world_example_test.go) to see how you can \nwrite automated tests for this simple resource config.\n\n\n\n\n## Deploying the Kubernetes resource\n\n1. Setup a Kubernetes cluster. We recommend using a local version:\n    - [Kubernetes on Docker For Mac](https://docs.docker.com/docker-for-mac/kubernetes/)\n    - [Kubernetes on Docker For Windows](https://docs.docker.com/docker-for-windows/kubernetes/)\n    - [minikube](https://github.com/kubernetes/minikube)\n1. Install and setup [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/) to talk to the deployed\n   Kubernetes cluster.\n1. Run `kubectl apply -f hello-world-deployment.yml`\n\n\n\n\n## Running automated tests against this Kubernetes deployment\n\n1. Setup a Kubernetes cluster. We recommend using a local version:\n    - [Kubernetes on Docker For Mac](https://docs.docker.com/docker-for-mac/kubernetes/)\n    - [Kubernetes on Docker For Windows](https://docs.docker.com/docker-for-windows/kubernetes/)\n    - [minikube](https://github.com/kubernetes/minikube)\n1. Install and setup [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/) to talk to the deployed\n   Kubernetes cluster.\n1. Install [Golang](https://golang.org/) and make sure this code is checked out into your `GOPATH`.\n1. `cd test`\n1. `go test -v -tags kubernetes -run TestKubernetesHelloWorldExample`\n"
  },
  {
    "path": "examples/kubernetes-hello-world-example/hello-world-deployment.yml",
    "content": "---\n# website::tag::1:: Deploy the hashicorp/http-echo Docker Container: https://hub.docker.com/r/hashicorp/http-echo\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: hello-world-deployment\nspec:\n  selector:\n    matchLabels:\n      app: hello-world\n  replicas: 1\n  template:\n    metadata:\n      labels:\n        app: hello-world\n    spec:\n      containers:\n        # website::tag::2:: Runs an HTTP server that responds with \"Hello, World!\" on port 5000\n        - name: hello-world\n          image: hashicorp/http-echo\n          args:\n            - \"-text=Hello, World!\"\n            - \"-listen=:5000\"\n          ports:\n            - containerPort: 5000\n---\n# website::tag::3:: Expose the webapp on port 5000 via a Kubernetes LoadBalancer.\nkind: Service\napiVersion: v1\nmetadata:\n  name: hello-world-service\nspec:\n  selector:\n    app: hello-world\n  ports:\n    - protocol: TCP\n      targetPort: 5000\n      port: 5000\n  type: LoadBalancer\n"
  },
  {
    "path": "examples/kubernetes-kustomize-example/README.md",
    "content": "# Kubernetes Kustomize Example\n\nThis folder contains a minimal Kubernetes resource config file to demonstrate how you can use Terratest to write\nautomated tests for Kubernetes.\n\nThis resource file deploys an nginx container as a single pod deployment with a node port service attached to it.\n\nSee the corresponding terratest code for an example of how to test this resource config:\n- [kubernetes_kustomize_example_test.go](../../test/kubernetes_kustomize_example_test.go)\n\n\n## Deploying the Kubernetes resource\n\n1. Setup a Kubernetes cluster. We recommend using a local version:\n    - [minikube](https://github.com/kubernetes/minikube)\n    - [Kubernetes on Docker For Mac](https://docs.docker.com/docker-for-mac/kubernetes/)\n    - [Kubernetes on Docker For Windows](https://docs.docker.com/docker-for-windows/kubernetes/)\n\n1. Install and setup [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/) to talk to the deployed\n   Kubernetes cluster.\n1. Run `kubectl apply -k .`\n\n\n## Running automated tests against this Kubernetes deployment\n\n1. Setup a Kubernetes cluster. We recommend using a local version:\n    - [minikube](https://github.com/kubernetes/minikube)\n    - [Kubernetes on Docker For Mac](https://docs.docker.com/docker-for-mac/kubernetes/)\n    - [Kubernetes on Docker For Windows](https://docs.docker.com/docker-for-windows/kubernetes/)\n\n1. Install and setup [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/) to talk to the deployed\n   Kubernetes cluster.\n1. Install [Golang](https://golang.org/) and make sure this code is checked out into your `GOPATH`.\n1. `cd test`\n1. `dep ensure`\n1. `go test -v -tags kubernetes -run TestKubernetesKustomizeExample`\n"
  },
  {
    "path": "examples/kubernetes-kustomize-example/deployment.yaml",
    "content": "---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: nginx-deployment\nspec:\n  selector:\n    matchLabels:\n      app: nginx\n  replicas: 1\n  template:\n    metadata:\n      labels:\n        app: nginx\n    spec:\n      containers:\n      - name: nginx\n        image: nginx:1.15.7\n        ports:\n        - containerPort: 80\n"
  },
  {
    "path": "examples/kubernetes-kustomize-example/kustomization.yaml",
    "content": "resources:\n- ./deployment.yaml\n- ./service.yaml\n"
  },
  {
    "path": "examples/kubernetes-kustomize-example/service.yaml",
    "content": "---\nkind: Service\napiVersion: v1\nmetadata:\n  name: nginx-service\nspec:\n  selector:\n    app: nginx\n  ports:\n    - protocol: TCP\n      targetPort: 80\n      port: 1080\n  type: NodePort\n"
  },
  {
    "path": "examples/kubernetes-rbac-example/README.md",
    "content": "# Kubernetes RBAC Example\n\nThis folder contains a Kubernetes resource config file that creates a new Namespace and a ServiceAccount that has admin\nlevel permissions in the Namespace, but nowhere else. This example is used to demonstrate how you can test RBAC\npermissions using terratest.\n\nSee the corresponding terratest code ([kubernetes_rbac_example_test.go](../../test/kubernetes_rbac_example_test.go)) for\nan example of how to test this resource config:\n\n\n## Deploying the Kubernetes resource\n\n1. Setup a Kubernetes cluster. We recommend using a local version:\n    - [minikube](https://github.com/kubernetes/minikube)\n    - [Kubernetes on Docker For Mac](https://docs.docker.com/docker-for-mac/kubernetes/)\n    - [Kubernetes on Docker For Windows](https://docs.docker.com/docker-for-windows/kubernetes/)\n\n1. Install and setup [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/) to talk to the deployed\n   Kubernetes cluster.\n1. Run `kubectl apply -f namespace-service-account.yml`\n\n\n## Running automated tests against this Kubernetes deployment\n\n1. Setup a Kubernetes cluster. We recommend using a local version:\n    - [minikube](https://github.com/kubernetes/minikube)\n    - [Kubernetes on Docker For Mac](https://docs.docker.com/docker-for-mac/kubernetes/)\n    - [Kubernetes on Docker For Windows](https://docs.docker.com/docker-for-windows/kubernetes/)\n\n1. Install and setup [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/) to talk to the deployed\n   Kubernetes cluster.\n1. Install and setup [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/).\n1. Install [Golang](https://golang.org/) and make sure this code is checked out into your `GOPATH`.\n1. `cd test`\n1. `dep ensure`\n1. `go test -v -tags kubernetes -run TestKubernetesRBACExample`\n"
  },
  {
    "path": "examples/kubernetes-rbac-example/namespace-service-account.yml",
    "content": "---\napiVersion: v1\nkind: Namespace\nmetadata:\n  name: terratest-rbac-example-namespace\n---\napiVersion: v1\nkind: ServiceAccount\nmetadata:\n  name: terratest-rbac-example-service-account\n  namespace: terratest-rbac-example-namespace\n---\napiVersion: rbac.authorization.k8s.io/v1\nkind: Role\nmetadata:\n  name: terratest-rbac-example-role\n  namespace: terratest-rbac-example-namespace\nrules:\n  - apiGroups: [\"*\"]\n    resources: [\"*\"]\n    verbs: [\"*\"]\n---\napiVersion: rbac.authorization.k8s.io/v1\nkind: RoleBinding\nmetadata:\n  name: terratest-rbac-example-service-account-binding\n  namespace: terratest-rbac-example-namespace\nsubjects:\n  - kind: ServiceAccount\n    name: terratest-rbac-example-service-account\n    namespace: terratest-rbac-example-namespace\nroleRef:\n  kind: Role\n  name: terratest-rbac-example-role\n  apiGroup: rbac.authorization.k8s.io\n"
  },
  {
    "path": "examples/packer-basic-example/README.md",
    "content": "# Packer Basic Example\n\nThis folder contains a very simple Packer template to demonstrate how you can use Terratest to write automated tests\nfor your Packer templates. The template just creates an up-to-date Ubuntu AMI by running `apt-get update` and\n`apt-get upgrade`.\n\nCheck out [test/packer_basic_example_test.go](/test/packer_basic_example_test.go) to see how you can write\nautomated tests for this simple template.\n\nNote that this template doesn't do anything useful; it's just here to demonstrate the simplest usage pattern for\nTerratest. For slightly more complicated, real-world examples of Packer templates and the corresponding tests, see\n[packer-docker-example](/examples/packer-docker-example) and\n[terraform-packer-example](/examples/terraform-packer-example).\n\n\n\n## Installation steps\n1. Sign up for [AWS](https://aws.amazon.com/).\n1. Configure your AWS credentials using one of the [supported methods for AWS CLI\n   tools](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html), such as setting the\n   `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` environment variables.\n1. Install [Packer](https://www.packer.io/) and make sure it's on your `PATH`.\n\n## Building the Packer template manually (Packer >= 1.7.0)\n1. Run `packer init build.pkr.hcl`.  # Use build-gcp.pkr.hcl if using GCP\n1. Run `packer build build.pkr.hcl`. # Use build-gcp.pkr.hcl if using GCP\n\n## Building the Packer template manually (Packer < 1.7.0)\n1. Run `packer build build.json`.\n\n\n\n## Running automated tests against this Packer template\n\n1. Sign up for [AWS](https://aws.amazon.com/).\n1. Configure your AWS credentials using one of the [supported methods for AWS CLI\n   tools](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html), such as setting the\n   `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` environment variables.\n1. Install [Packer](https://www.packer.io/) and make sure it's on your `PATH`.\n1. Install [Golang](https://golang.org/) and make sure this code is checked out into your `GOPATH`.\n1. `cd test`\n1. `dep ensure`\n1. `go test -v -run TestPackerBasicExample`\n\n\n\n\n## Running automated tests against this Packer template for the GCP builder\n\n1. Sign up for [GCP](https://cloud.google.com/).\n1. Configure your GCP credentials using one of the\n   [Authentication](https://www.packer.io/docs/builders/googlecompute.html#authentication) methods.\n1. Install [Packer](https://www.packer.io/) and make sure it's on your `PATH`.\n1. Install [Golang](https://golang.org/) and make sure this code is checked out into your `GOPATH`.\n1. `cd test`\n1. `dep ensure`\n1. `go test -v -run TestPackerGCPBasicExample`\n\n\n\n\n## Running automated tests against this Packer template for the OCI builder\n\n1. Sign up for [OCI](https://cloud.oracle.com/cloud-infrastructure).\n1. Configure your OCI credentials via [CLI Configuration\n   Information](https://docs.cloud.oracle.com/iaas/Content/API/Concepts/sdkconfig.htm).\n1. Create [VCN](https://docs.cloud.oracle.com/iaas/Content/GSG/Tasks/creatingnetwork.htm) and subnet\n   resources in your tenancy (a.k.a. a root compartment).\n1. (Optional) Create `TF_VAR_pass_phrase` environment property with the pass phrase for decrypting of the OCI [API signing\n      key](https://docs.cloud.oracle.com/iaas/Content/API/Concepts/apisigningkey.htm) (can be omitted\n      if the key is not protected).\n1. Install [Packer](https://www.packer.io/) and make sure it's on your `PATH`.\n1. Install [Golang](https://golang.org/) and make sure this code is checked out into your `GOPATH`.\n1. `cd test`\n1. `dep ensure`\n1. `go test -v -run TestPackerOciExample`\n"
  },
  {
    "path": "examples/packer-basic-example/build-gcp.pkr.hcl",
    "content": "packer {\n  required_plugins {\n    googlecompute = {\n      version = \">=v1.0.0\"\n      source  = \"github.com/hashicorp/googlecompute\"\n    }\n  }\n}\n\nvariable \"gcp_project_id\" {\n  type    = string\n  default = \"\"\n}\n\nvariable \"gcp_zone\" {\n  type    = string\n  default = \"us-central1-a\"\n}\n\nsource \"googlecompute\" \"ubuntu-bionic\" {\n  image_family        = \"terratest\"\n  image_name          = \"terratest-packer-example-${formatdate(\"YYYYMMDD-hhmm\", timestamp())}\"\n  project_id          = var.gcp_project_id\n  source_image_family = \"ubuntu-2204-lts\"\n  ssh_username        = \"ubuntu\"\n  zone                = var.gcp_zone\n}\n\n\nbuild {\n  sources = [\n    \"source.googlecompute.ubuntu-bionic\"\n  ]\n\n  provisioner \"shell\" {\n    inline       = [\"sudo DEBIAN_FRONTEND=noninteractive apt-get update\", \"sudo DEBIAN_FRONTEND=noninteractive apt-get upgrade -y\"]\n    pause_before = \"30s\"\n  }\n\n}\n"
  },
  {
    "path": "examples/packer-basic-example/build.pkr.hcl",
    "content": "packer {\n  required_plugins {\n    amazon = {\n      version = \">=v1.0.0\"\n      source  = \"github.com/hashicorp/amazon\"\n    }\n    oracle = {\n      version = \">=v1.0.0\"\n      source  = \"github.com/hashicorp/oracle\"\n    }\n  }\n}\n\nvariable \"ami_base_name\" {\n  type    = string\n  default = \"\"\n}\n\nvariable \"aws_region\" {\n  type    = string\n  default = \"us-east-1\"\n}\n\nvariable \"instance_type\" {\n  type    = string\n  default = \"t2.micro\"\n}\n\nvariable \"oci_availability_domain\" {\n  type    = string\n  default = \"\"\n}\n\nvariable \"oci_base_image_ocid\" {\n  type    = string\n  default = \"\"\n}\n\nvariable \"oci_compartment_ocid\" {\n  type    = string\n  default = \"\"\n}\n\nvariable \"oci_pass_phrase\" {\n  type    = string\n  default = \"\"\n}\n\nvariable \"oci_subnet_ocid\" {\n  type    = string\n  default = \"\"\n}\n\ndata \"amazon-ami\" \"ubuntu-jammy\" {\n  filters = {\n    architecture                       = \"x86_64\"\n    \"block-device-mapping.volume-type\" = \"gp2\"\n    name                               = \"*ubuntu-jammy-22.04-amd64-server-*\"\n    root-device-type                   = \"ebs\"\n    virtualization-type                = \"hvm\"\n  }\n  most_recent = true\n  owners      = [\"099720109477\"]\n  region      = var.aws_region\n}\n\nsource \"amazon-ebs\" \"ubuntu-example\" {\n  ami_description = \"An example of how to create a custom AMI on top of Ubuntu\"\n  ami_name        = \"${var.ami_base_name}-terratest-packer-example\"\n  encrypt_boot    = false\n  instance_type   = var.instance_type\n  region          = var.aws_region\n  source_ami      = data.amazon-ami.ubuntu-jammy.id\n  ssh_username    = \"ubuntu\"\n}\n\nsource \"oracle-oci\" \"oracle-example\" {\n  availability_domain = var.oci_availability_domain\n  base_image_ocid     = var.oci_base_image_ocid\n  compartment_ocid    = var.oci_compartment_ocid\n  image_name          = \"terratest-packer-example-${formatdate(\"YYYYMMDD-hhmm\", timestamp())}\"\n  pass_phrase         = var.oci_pass_phrase\n  shape               = \"VM.Standard2.1\"\n  ssh_username        = \"ubuntu\"\n  subnet_ocid         = var.oci_subnet_ocid\n}\n\nbuild {\n  sources = [\n    \"source.amazon-ebs.ubuntu-example\",\n    \"source.oracle-oci.oracle-example\"\n  ]\n\n  provisioner \"shell\" {\n    inline       = [\"sudo DEBIAN_FRONTEND=noninteractive apt-get update\", \"sudo DEBIAN_FRONTEND=noninteractive apt-get upgrade -y\"]\n    pause_before = \"30s\"\n  }\n}\n"
  },
  {
    "path": "examples/packer-docker-example/README.md",
    "content": "# Packer Docker Example\n\nThis folder contains a Packer template to demonstrate how you can use Terratest to write automated tests for your\nPacker templates. The template creates an Ubuntu AMI with a simple web app (built on top of Ruby / Sinatra) installed.\nThis template _also_ creates a Docker image with the same web app installed, and contains a `docker-compose.yml` file\nfor running that Docker image. These allow you to test your Packer template completely locally, without having to\ndeploy to AWS.\n\nCheck out [test/packer_docker_example_test.go](/test/packer_docker_example_test.go) to see how you can write\nautomated tests for this simple template.\n\nThe Docker-based tests in this folder are in some sense \"unit tests\" for the Packer template. To see an example of\n\"integration tests\" that deploy the AMI to AWS, check out the\n[terraform-packer-example](/examples/terraform-packer-example).\n\n\n## Installation steps\n1. Install [Packer](https://www.packer.io/) and make sure it's on your `PATH`.\n1. Install [Docker](https://www.docker.com/) and make sure it's on your `PATH`.\n\n\n## Building a Docker image for local testing (Packer >= 1.7.0)\n1. Run `packer init build.pkr.hcl`.\n1. Run `packer build build.pkr.hcl`.\n\n\n## Building a Docker image for local testing (Packer < 1.7.0)\n1. Run `packer build build.json`.\n\n\n## Run the container\n1. Run `docker compose up`.\n1. You should now be able to access the sample web app at http://localhost:8080\n\n\n\n\n## Building an AMI for testing in AWS\n\n1. Sign up for [AWS](https://aws.amazon.com/).\n1. Configure your AWS credentials using one of the [supported methods for AWS CLI\n   tools](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html), such as setting the\n   `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` environment variables.\n1. Install [Packer](https://www.packer.io/) and make sure it's on your `PATH`.\n1. Run `packer build -only=ubuntu-ami build.json`.\n\n\n\n\n## Running automated tests locally against this Packer template\n\n1. Install [Packer](https://www.packer.io/) and make sure it's on your `PATH`.\n1. Install [Docker](https://www.docker.com/) and make sure it's on your `PATH`.\n1. Install [Golang](https://golang.org/) and make sure this code is checked out into your `GOPATH`.\n1. `cd test`\n1. `dep ensure`\n1. `go test -v -run TestPackerDockerExampleLocal`\n\n\n\n\n## Running automated tests in AWS against this Packer template\n\nSee [terraform-packer-example](/examples/terraform-packer-example).\n"
  },
  {
    "path": "examples/packer-docker-example/app.rb",
    "content": "# A simple web app built on top of Ruby and Sinatra.\n\nrequire 'sinatra'\nrequire 'json'\n\nif ARGV.length != 2\n  raise 'Expected exactly two arguments: SERVER_PORT SERVER_TEXT'\nend\n\nserver_port = ARGV[0]\nserver_text = ARGV[1]\n\nset :port, server_port\nset :bind, '0.0.0.0'\nset :server, 'puma'\n\nget '/' do\n  server_text\nend\n"
  },
  {
    "path": "examples/packer-docker-example/build.json",
    "content": "{\n  \"variables\": {\n    \"aws_region\": \"us-east-1\",\n    \"ami_name_base\": \"terratest-packer-docker-example\",\n    \"instance_type\": \"t2.micro\"\n  },\n  \"builders\": [{\n    \"name\": \"ubuntu-ami\",\n    \"ami_name\": \"{{user `ami_name_base`}}-{{isotime | clean_resource_name}}\",\n    \"ami_description\": \"An example of how to create a custom AMI with a simple web app on top of Ubuntu\",\n    \"instance_type\": \"{{user `instance_type`}}\",\n    \"region\": \"{{user `aws_region`}}\",\n    \"type\": \"amazon-ebs\",\n    \"source_ami_filter\": {\n      \"filters\": {\n        \"virtualization-type\": \"hvm\",\n        \"architecture\": \"x86_64\",\n        \"name\": \"*ubuntu-jammy-22.04-amd64-server-*\",\n        \"block-device-mapping.volume-type\": \"gp2\",\n        \"root-device-type\": \"ebs\"\n      },\n      \"owners\": [\"099720109477\"],\n      \"most_recent\": true\n    },\n    \"ssh_username\": \"ubuntu\",\n    \"encrypt_boot\": false\n  },{\n    \"name\": \"ubuntu-docker\",\n    \"type\": \"docker\",\n    \"image\": \"gruntwork/ubuntu-test:22.04\",\n    \"commit\": true,\n    \"changes\": [\"ENTRYPOINT [\\\"\\\"]\"]\n  }],\n  \"provisioners\": [{\n    \"type\": \"shell\",\n    \"inline\": [\n      \"echo 'Sleeping for a few seconds to give Ubuntu time to boot up'\",\n      \"sleep 30\"\n    ],\n    \"only\": [\"ubuntu-ami\"]\n  },{\n    \"type\": \"file\",\n    \"source\": \"{{template_dir}}\",\n    \"destination\": \"/tmp/packer-docker-example\"\n  },{\n    \"type\": \"shell\",\n    \"inline\": [\"/tmp/packer-docker-example/configure-sinatra-app.sh\"]\n  }],\n  \"post-processors\": [{\n    \"type\": \"docker-tag\",\n    \"repository\": \"gruntwork/packer-docker-example\",\n    \"tag\": \"latest\",\n    \"only\": [\"ubuntu-docker\"]\n  }]\n}\n"
  },
  {
    "path": "examples/packer-docker-example/build.pkr.hcl",
    "content": "packer {\n  required_plugins {\n    amazon = {\n      version = \">=v1.0.0\"\n      source  = \"github.com/hashicorp/amazon\"\n    }\n    docker = {\n      version = \">=v1.0.1\"\n      source  = \"github.com/hashicorp/docker\"\n    }\n  }\n}\n\nvariable \"ami_name_base\" {\n  type    = string\n  default = \"terratest-packer-docker-example\"\n}\n\nvariable \"aws_region\" {\n  type    = string\n  default = \"us-east-1\"\n}\n\nvariable \"instance_type\" {\n  type    = string\n  default = \"t2.micro\"\n}\n\ndata \"amazon-ami\" \"aws\" {\n  filters = {\n    architecture                       = \"x86_64\"\n    \"block-device-mapping.volume-type\" = \"gp2\"\n    name                               = \"*ubuntu-jammy-22.04-amd64-server-*\"\n    root-device-type                   = \"ebs\"\n    virtualization-type                = \"hvm\"\n  }\n  most_recent = true\n  owners      = [\"099720109477\"]\n  region      = var.aws_region\n}\n\nsource \"amazon-ebs\" \"ubuntu-ami\" {\n  ami_description = \"An example of how to create a custom AMI with a simple web app on top of Ubuntu\"\n  ami_name        = \"${var.ami_name_base}-${formatdate(\"YYYYMMDD-hhmm\", timestamp())}\"\n  encrypt_boot    = false\n  instance_type   = var.instance_type\n  region          = var.aws_region\n  source_ami      = data.amazon-ami.aws.id\n  ssh_username    = \"ubuntu\"\n}\n\nsource \"docker\" \"ubuntu-docker\" {\n  changes = [\"ENTRYPOINT [\\\"\\\"]\"]\n  commit  = true\n  image   = \"gruntwork/ubuntu-test:22.04\"\n}\n\nbuild {\n  sources = [\"source.amazon-ebs.ubuntu-ami\", \"source.docker.ubuntu-docker\"]\n\n  provisioner \"shell\" {\n    inline = [\"echo 'Sleeping for a few seconds to give Ubuntu time to boot up'\", \"sleep 30\"]\n    only   = [\"amazon-ebs.ubuntu-ami\"]\n  }\n\n  provisioner \"file\" {\n    destination = \"/tmp/packer-docker-example\"\n    source      = path.root\n  }\n\n  provisioner \"shell\" {\n    inline = [\"/tmp/packer-docker-example/configure-sinatra-app.sh\"]\n  }\n\n  post-processor \"docker-tag\" {\n    only       = [\"docker.ubuntu-docker\"]\n    repository = \"gruntwork/packer-docker-example\"\n    tag        = [\"latest\"]\n  }\n}\n"
  },
  {
    "path": "examples/packer-docker-example/configure-sinatra-app.sh",
    "content": "#!/bin/bash\n# Install and configure a simple web app built on top of Ruby and Sinatra\n\nset -e\n\nreadonly APP_RB_SRC=\"/tmp/packer-docker-example/app.rb\"\nreadonly APP_RB_DST=\"/home/ubuntu/app.rb\"\n\necho \"Installing Ruby\"\nsudo apt-get update\nsudo apt-get install -y make zlib1g-dev build-essential ruby ruby-dev\n\necho \"Installing Sinatra\"\nsudo gem install sinatra json rackup puma\n\necho \"Moving $APP_RB_SRC to $APP_RB_DST\"\nmkdir -p \"$(dirname \"$APP_RB_DST\")\"\nmv \"$APP_RB_SRC\" \"$APP_RB_DST\""
  },
  {
    "path": "examples/packer-docker-example/docker-compose.yml",
    "content": "# This file can be used with Docker and Docker Compose to run the web app in the Packer template 100% locally, without\n# having to deploy anything to AWS.\nversion: '3'\nservices:\n  web_app:\n    # The name we use for the Docker image in build.json (or build.pkr.hcl)\n    image: gruntwork/packer-docker-example\n\n    # Run the sample web app on port 8080\n    command: [\"ruby\", \"/home/ubuntu/app.rb\", \"${SERVER_PORT}\", \"${SERVER_TEXT}\"]\n\n    # Bind-mount the Ruby app so we can have \"hot reload\" during testing\n    volumes:\n      - ./app.rb:/home/ubuntu/app.rb\n\n    # Expose the sample app's port on the host OS\n    ports:\n      - \"${SERVER_PORT}:${SERVER_PORT}\"\n"
  },
  {
    "path": "examples/packer-hello-world-example/README.md",
    "content": "# Packer \"Hello, World\" Example\n\nThis folder contains the simplest possible Packer template—one that builds a Docker image with a text file that says\n\"Hello, World\"!—to demonstrate how you can use Terratest to write automated tests for your Packer templates.\n\nCheck out [test/packer_hello_world_example_test.go](/test/packer_hello_world_example_test.go) to see how you can write\nautomated tests for this simple template.\n\n\n## Installation steps\n1. Install [Packer](https://www.packer.io/) and make sure it's on your `PATH`.\n1. Install [Docker](https://www.docker.com/) and make sure it's on your `PATH`.\n\n\n## Building the Packer template (Packer >= 1.7.0)\n1. Run `packer init build.pkr.hcl`.\n1. Run `packer build build.pkr.hcl`.\n\n\n## Building the Packer template (Packer < 1.7.0)\n1. Run `packer build build.json`.\n\n\n## Run Docker\n1. Run `docker run -it --rm gruntwork/packer-hello-world-example cat /test.txt`.\n1. You should see the text \"Hello, World!\"\n\n\n## Running automated tests against the Packer template\n\n1. Install [Packer](https://www.packer.io/) and make sure it's on your `PATH`.\n1. Install [Docker](https://www.docker.com/) and make sure it's on your `PATH`.\n1. Install [Golang](https://golang.org/) and make sure this code is checked out into your `GOPATH`.\n1. `cd test`\n1. `go test -v -run TestPackerHelloWorldExample`\n"
  },
  {
    "path": "examples/packer-hello-world-example/build.pkr.hcl",
    "content": "packer {\n  required_plugins {\n    docker = {\n      version = \">=v1.0.1\"\n      source  = \"github.com/hashicorp/docker\"\n    }\n  }\n}\n\nsource \"docker\" \"ubuntu-docker\" {\n  changes = [\"ENTRYPOINT [\\\"\\\"]\"]\n\n  commit   = true\n  image    = \"gruntwork/ubuntu-test:16.04\"\n  platform = \"linux/amd64\"\n}\n\nbuild {\n  sources = [\"source.docker.ubuntu-docker\"]\n\n  provisioner \"shell\" {\n    inline = [\"echo 'Hello, World!' > /test.txt\"]\n  }\n\n  post-processor \"docker-tag\" {\n    repository = \"gruntwork/packer-hello-world-example\"\n    tag = [\"latest\"]\n  }\n}\n"
  },
  {
    "path": "examples/terraform-asg-scp-example/README.md",
    "content": "# Terraform ASG SCP Example\n\nThis folder contains a simple Terraform module that deploys resources in [AWS](https://aws.amazon.com/) to demonstrate\nhow you can use Terratest to write automated tests for your AWS Terraform code. This module deploys an ASG with one instance.\nThe EC2 Instance allows SSH requests on the port specified by the `ssh_port` variable. \n\nCheck out [test/terraform_scp_example_test.go](/test/terraform_scp_example_test.go) to see how you can write\nautomated tests for this module.\n\nNote that the example in this module is still fairly simplified, as the EC2 Instance doesn't do a whole lot! For a more\ncomplicated, real-world, end-to-end example of a Terraform module and web server, see\n[terraform-packer-example](/examples/terraform-packer-example).\n\n**WARNING**: This module and the automated tests for it deploy real resources into your AWS account which can cost you\nmoney. The resources are all part of the [AWS Free Tier](https://aws.amazon.com/free/), so if you haven't used that up,\nit should be free, but you are completely responsible for all AWS charges.\n\n## Overview \n\nWhen a test fails, it is often important to be able to quickly get to logs and config files from your deployed apps and services. Currently, getting at this information is a bit of a pain. Often times, it would be necessary to \"catch it in the act\". Usually this would require running tests and then \"pausing\"/not tearing down the infrastructure, ssh-ing to individual instances and then viewing the logs/config files that way. This in not very convenient and gets even more tricky when trying to get the same results for tests being executed by your CI server.\n\nYou can use terratest to help with this task by specifying `RemoteFileSpecification` structs that describe which files you want to copy from your instances:\n\n```go\nlogstashSpec := aws.RemoteFileSpecification{\n\tSshUser:sshUserName,\n\tUseSudo:true,\n\tKeyPair:keyPair,\n\tLocalDestinationDir:filepath.Join(\"/tmp\", \"logs\", t.Name(), \"logstash\"),\n\tAsgNames: strings.Split(strings.Replace(terraform.OutputRequired(t, terraformOptions, \"logstash_server_asg_names\"), \"\\n\", \"\", -1), \",\"),\n\tRemotePathToFileFilter: map[string][]string {\n\t\t\"/var/log/logstash\":{\"*\"},\n\t\t\"/etc/logstash/conf.d\" : {\"*\"},\n\t},\n}\n```\n\nOnce you've described what files you want, grabbing them from ASGs is simple with:\n```go\naws.FetchFilesFromAllAsgsE(t, awsRegion, logstashSpec)\n```\n\nor directly from EC2 instances with:\n```go\naws.FetchFilesFromInstance(t, awsRegion, sshUserName, keyPair, appServerInstanceId, true, appServerConfig, filepath.Join(\"/tmp\", \"logs\", t.Name(), \"app_server\"), []string{\"*.yml\", \"caFile\", \"*.key\", \"*.pem\"})\n```\n\nFinally, to put all of this together, in your go test you could do something like:\n\n```go\ndefer test_structure.RunTestStage(t, \"grab_logs\", func() {\n\tif t.Failed() {\n\t\ttakeElkMultiClusterLogSnapshot(t, examplesDir, awsRegion, \"ubuntu\")\n\t}\n})\n```\n\nThe code above will run at the very end of your test and grab a snapshot of all of the log descriptors you've defined specifically when your tests fail. We usually like to defer the logging snapshot right before we defer the terraform teardown. This way, if our tests fail, we are able to grab a snapshot of all the relevant logs and config files across our whole deployment!\n\nYou can even take this a step further and pair this with your CI's artifact storage mechanism to have all of your logs and config files attached to your broken CI build when tests failed. For example, with CircleCI, you could do something like:\n\n```yml\n      - store_artifacts:\n          path: /tmp/logs\n```\n\nNow, to get at your logs when your tests fail, you will just be able to click the links right in your CI.\n\n![logs](https://user-images.githubusercontent.com/34349331/46639252-086e0a00-cb33-11e8-8dd2-9be73ca2af56.gif)\n\n## Running this module manually\n\n1. Sign up for [AWS](https://aws.amazon.com/).\n1. Configure your AWS credentials using one of the [supported methods for AWS CLI\n   tools](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html), such as setting the\n   `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` environment variables. If you're using the `~/.aws/config` file for profiles then export `AWS_SDK_LOAD_CONFIG` as \"True\".\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`.\n1. Run `terraform init`.\n1. Run `terraform apply`.\n1. When you're done, run `terraform destroy`.\n\n\n\n\n## Running automated tests against this module\n\n1. Sign up for [AWS](https://aws.amazon.com/).\n1. Configure your AWS credentials using one of the [supported methods for AWS CLI\n   tools](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html), such as setting the\n   `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` environment variables. If you're using the `~/.aws/config` file for profiles then export `AWS_SDK_LOAD_CONFIG` as \"True\".\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`.\n1. Install [Golang](https://golang.org/) and make sure this code is checked out into your `GOPATH`.\n1. `cd test`\n1. `dep ensure`\n1. `go test -v -run TestTerraformScpExample`\n"
  },
  {
    "path": "examples/terraform-asg-scp-example/main.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# PIN TERRAFORM VERSION TO >= 0.12\n# The examples have been upgraded to 0.12 syntax\n# ---------------------------------------------------------------------------------------------------------------------\n\nterraform {\n  # This module is now only being tested with Terraform 0.13.x. However, to make upgrading easier, we are setting\n  # 0.12.26 as the minimum version, as that version added support for required_providers with source URLs, making it\n  # forwards compatible with 0.13.x code.\n  required_version = \">= 0.12.26\"\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY AN ASG WITH ONE INSTANCE THAT ALLOWS CONNECTIONS VIA SSH\n# See test/terraform_scp_example.go for how to write automated tests for this code.\n# ---------------------------------------------------------------------------------------------------------------------\n\nprovider \"aws\" {\n  region = var.aws_region\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY AN ASG WITH ONE NODE TO TEST HOW WE CAN SCP FROM THE EC2 INSTANCE IN THIS ASG\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"aws_launch_template\" \"sample_launch_template\" {\n  name_prefix            = var.instance_name\n  image_id               = data.aws_ami.ubuntu.id\n  instance_type          = var.instance_type\n  vpc_security_group_ids = [aws_security_group.example.id]\n  key_name               = var.key_pair_name\n}\n\nresource \"aws_autoscaling_group\" \"sample_asg\" {\n  vpc_zone_identifier = data.aws_subnets.default_subnets.ids\n\n  desired_capacity = 1\n  max_size         = 1\n  min_size         = 1\n\n  launch_template {\n    id      = aws_launch_template.sample_launch_template.id\n    version = \"$Latest\"\n  }\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# CREATE A SECURITY GROUP TO CONTROL WHAT REQUESTS CAN GO IN AND OUT OF THE EC2 INSTANCES\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"aws_security_group\" \"example\" {\n  name = var.instance_name\n\n  egress {\n    from_port   = 0\n    to_port     = 0\n    protocol    = \"-1\"\n    cidr_blocks = [\"0.0.0.0/0\"]\n  }\n\n  ingress {\n    from_port = var.ssh_port\n    to_port   = var.ssh_port\n    protocol  = \"tcp\"\n\n    # To keep this example simple, we allow incoming SSH requests from any IP. In real-world usage, you should only\n    # allow SSH requests from trusted servers, such as a bastion host or VPN server.\n    cidr_blocks = [\"0.0.0.0/0\"]\n  }\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# LOOK UP THE LATEST UBUNTU AMI\n# ---------------------------------------------------------------------------------------------------------------------\n\ndata \"aws_ami\" \"ubuntu\" {\n  most_recent = true\n  owners      = [\"099720109477\"] # Canonical\n\n  filter {\n    name   = \"virtualization-type\"\n    values = [\"hvm\"]\n  }\n\n  filter {\n    name   = \"architecture\"\n    values = [\"x86_64\"]\n  }\n\n  filter {\n    name   = \"image-type\"\n    values = [\"machine\"]\n  }\n\n  filter {\n    name   = \"name\"\n    values = [\"ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*\"]\n  }\n}\n\ndata \"aws_vpc\" \"default\" {\n  default = true\n}\n\ndata \"aws_subnets\" \"default_subnets\" {\n  filter {\n    name   = \"vpc-id\"\n    values = [data.aws_vpc.default.id]\n  }\n  filter {\n    name   = \"defaultForAz\"\n    values = [true]\n  }\n}\n\n"
  },
  {
    "path": "examples/terraform-asg-scp-example/outputs.tf",
    "content": "output \"asg_name\" {\n  value = aws_autoscaling_group.sample_asg.name\n}\n\n"
  },
  {
    "path": "examples/terraform-asg-scp-example/variables.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# ENVIRONMENT VARIABLES\n# Define these secrets as environment variables\n# ---------------------------------------------------------------------------------------------------------------------\n\n# AWS_ACCESS_KEY_ID\n# AWS_SECRET_ACCESS_KEY\n\n# ---------------------------------------------------------------------------------------------------------------------\n# REQUIRED PARAMETERS\n# You must provide a value for each of these parameters.\n# ---------------------------------------------------------------------------------------------------------------------\n\nvariable \"key_pair_name\" {\n  description = \"The EC2 Key Pair to associate with the EC2 Instance for SSH access.\"\n  type        = string\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# OPTIONAL PARAMETERS\n# These parameters have reasonable defaults.\n# ---------------------------------------------------------------------------------------------------------------------\n\nvariable \"aws_region\" {\n  description = \"The AWS region to deploy into\"\n  type        = string\n  default     = \"us-east-1\"\n}\n\nvariable \"instance_name\" {\n  description = \"The Name tag to set for the EC2 Instance.\"\n  type        = string\n  default     = \"terratest-example\"\n}\n\nvariable \"ssh_port\" {\n  description = \"The port the EC2 Instance should listen on for SSH requests.\"\n  type        = number\n  default     = 22\n}\n\nvariable \"instance_type\" {\n  description = \"The EC2 instance type to run.\"\n  type        = string\n  default     = \"t2.micro\"\n}"
  },
  {
    "path": "examples/terraform-aws-dynamodb-example/README.md",
    "content": "# Terraform AWS DynamoDB Example\n\nThis folder contains a simple Terraform module that deploys a [DynamoDB](https://aws.amazon.com/dynamodb/) table\nwith server-side encryption, point in time recovery and a TTL (time to live) attribute\nto demonstrate how you can use Terratest to write automated tests for your AWS Terraform code. \n\nCheck out [test/terraform_aws_dynamodb_example_test.go](/test/terraform_aws_dynamodb_example_test.go) to see how you can \ntest this module and validate the configuration of the parameters and options.\n\n**WARNING**: This module and the automated tests for it deploy real resources into your AWS account which can cost you\nmoney.\n\n## Running this module manually\n\n1. Sign up for [AWS](https://aws.amazon.com/).\n1. Configure your AWS credentials using one of the [supported methods for AWS CLI\n   tools](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html), such as setting the\n   `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` environment variables. If you're using the `~/.aws/config` file for profiles then export `AWS_SDK_LOAD_CONFIG` as \"True\".\n1. Set the AWS region you want to use as the environment variable `AWS_DEFAULT_REGION`.\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`.\n1. Run `terraform init`.\n1. Run `terraform apply`.\n1. When you're done, run `terraform destroy`.\n\n## Running automated tests against this module\n\n1. Sign up for [AWS](https://aws.amazon.com/).\n1. Configure your AWS credentials using one of the [supported methods for AWS CLI\n   tools](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html), such as setting the\n   `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` environment variables. If you're using the `~/.aws/config` file for profiles then export `AWS_SDK_LOAD_CONFIG` as \"True\".\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`.\n1. Install [Golang](https://golang.org/) and make sure this code is checked out into your `GOPATH`.\n1. `cd test`\n1. `dep ensure`\n1. `go test -v -run TestTerraformAwsDynamoDBExample`\n"
  },
  {
    "path": "examples/terraform-aws-dynamodb-example/main.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# PIN TERRAFORM VERSION TO >= 0.12\n# The examples have been upgraded to 0.12 syntax\n# ---------------------------------------------------------------------------------------------------------------------\nprovider \"aws\" {\n  region = var.region\n}\n\nterraform {\n  # This module is now only being tested with Terraform 0.13.x. However, to make upgrading easier, we are setting\n  # 0.12.26 as the minimum version, as that version added support for required_providers with source URLs, making it\n  # forwards compatible with 0.13.x code.\n  required_version = \">= 0.12.26\"\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# CREATE THE DYNAMODB TABLE\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"aws_dynamodb_table\" \"example\" {\n  name         = var.table_name\n  hash_key     = \"userId\"\n  range_key    = \"department\"\n  billing_mode = \"PAY_PER_REQUEST\"\n\n  server_side_encryption {\n    enabled = true\n  }\n  point_in_time_recovery {\n    enabled = true\n  }\n\n  attribute {\n    name = \"userId\"\n    type = \"S\"\n  }\n  attribute {\n    name = \"department\"\n    type = \"S\"\n  }\n\n  ttl {\n    enabled        = true\n    attribute_name = \"expires\"\n  }\n\n  tags = {\n    Environment = \"production\"\n  }\n}\n\n"
  },
  {
    "path": "examples/terraform-aws-dynamodb-example/variables.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# REQUIRED PARAMETERS\n# ---------------------------------------------------------------------------------------------------------------------\nvariable \"region\" {\n  description = \"The AWS region to deploy to\"\n  type        = string\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# OPTIONAL PARAMETERS\n# These parameters have reasonable defaults.\n# ---------------------------------------------------------------------------------------------------------------------\n\nvariable \"table_name\" {\n  description = \"The name to set for the dynamoDB table.\"\n  type        = string\n  default     = \"terratest-example\"\n}\n"
  },
  {
    "path": "examples/terraform-aws-ec2-windows-example/README.md",
    "content": "# Windows Instance Example\n\nThis folder provides a Packer template that can be used to build an Amazon Machine Image (AMI) of a Windows 2016 Server that comes pre-installed with: \n\n- The [Chocolately package manager](https://chocolatey.org/why-chocolatey) which makes it easy to install additional software packages onto Windows\n- Git\n- Python 3\n\nIn addition, this folder provides an example of how to launch a Windows instance based off this AMI that can be connected to via a Remote Desktop Protocol (RDP) client for the purposes of testing software or experimentation. \n\nThis setup is ideal for \"hot-reloading\" code that you're actively developing and testing it against the Windows server. You can develop your code in your usual environment, perhaps a Mac or Linux laptop, yet see your changes reflected on the Windows server in seconds, by sharing a folder from your development machine with the Windows server via the RDP client. \n\n## Quick start \n\nPre-requistes: \n\n- [Packer version v1.8.1 or newer](https://github.com/hashicorp/packer)\n- [Terraform v1.0 or newer](https://github.com/hashicorp/terraform)\n- An AWS account with valid security credentials\n\nFirst, we'll build the AMI for the Windows Instance. Change into the packer directory: \n\n`cd packer` \n\nIn order to build an Amazon Machine Image with Packer, you'll need to export your AWS account credentials. You can export your AWS credentials as the environment variables `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`. \n\nFor more information on authenticating to your AWS account from the command line, see our blog post [Authenticating to AWS with Environment Variables](https://blog.gruntwork.io/authenticating-to-aws-with-environment-variables-e793d6f6d02e).\n\nWith your credentials properly exported, you can now run the packer build: \n\n`packer build build.pkr.hcl`\n\nThis may take upwards of 25 minutes to complete, but generally completes in about 5 minutes. Keep an eye on your EC2 dashboard and ensure that you have selected the correct region and that you are on the AMI view. Once your AMI status has changed from \"Pending\" to \"Available\", you can copy your AMI ID. \n\nCreate a new file named `terraform.tfvars` in this same directory and enter the following variables: \n\n```hcl\nami_id           = \"<the AMI ID you copied in the previous step>\"\nregion           = \"us-east-1\"\nroot_volume_size = 100\n```\nSave the file. \n\nYou're now ready to run terraform plan and check the output before proceeding: \n\n`terraform plan`\n\nTake a look at the plan output and ensure everything looks correct. You should see a single EC2 instance being created along with supporting resources such as a security group and security group rules. \n\nOnce you're satisfied that the plan looks good, run terraform apply to create the infrastructure: \n\n`terraform apply --auto-approve`\n\nOnce your resources apply successfully you'll see a similar output message containing the public IPv4 address of your Windows instance: \n\n`instance_ip = \"35.84.139.82\"`\n\n"
  },
  {
    "path": "examples/terraform-aws-ec2-windows-example/main.tf",
    "content": "# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n# LAUNCH THE WINDOWS INSTANCE\n# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nterraform {\n  # This module is now only being tested with Terraform 1.1.x. However, to make upgrading easier, we are setting 1.0.0 as the minimum version.\n  required_version = \">= 1.0.0\"\n  required_providers {\n    aws = {\n      source  = \"hashicorp/aws\"\n      version = \"< 4.0\"\n    }\n  }\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# CONFIGURE OUR AWS CONNECTION\n# ---------------------------------------------------------------------------------------------------------------------\n\nprovider \"aws\" {\n  # The AWS region in which all resources will be created\n  region = var.region\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY INTO THE DEFAULT VPC AND SUBNETS\n# To keep this example simple, we are deploying into the Default VPC and its subnets. In real-world usage, you should\n# deploy into a custom VPC and private subnets.\n# ---------------------------------------------------------------------------------------------------------------------\n\ndata \"aws_vpc\" \"default\" {\n  default = true\n}\n\ndata \"aws_subnet_ids\" \"all\" {\n  vpc_id = data.aws_vpc.default.id\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# CREATE A SECURITY GROUP TO ALLOW ACCESS TO THE RDS INSTANCE\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"aws_security_group\" \"windows_instance\" {\n  name   = var.name\n  vpc_id = data.aws_vpc.default.id\n}\n\nresource \"aws_security_group_rule\" \"allow_rdp\" {\n  type              = \"ingress\"\n  security_group_id = aws_security_group.windows_instance.id\n\n  from_port   = \"3389\"\n  to_port     = \"3389\"\n  protocol    = \"tcp\"\n  cidr_blocks = [\"0.0.0.0/0\"]\n}\n\nresource \"aws_security_group_rule\" \"allow_egress\" {\n  type              = \"egress\"\n  security_group_id = aws_security_group.windows_instance.id\n\n  from_port   = 0\n  to_port     = 0\n  protocol    = \"-1\"\n  cidr_blocks = [\"0.0.0.0/0\"]\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# LAUNCH THE WINDOWS INSTANCE \n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"aws_instance\" \"instance\" {\n  ami                    = var.ami\n  instance_type          = var.instance_type\n  vpc_security_group_ids = [aws_security_group.windows_instance.id]\n\n  tags = {\n    Name = var.instance_type\n  }\n}\n\n"
  },
  {
    "path": "examples/terraform-aws-ec2-windows-example/outputs.tf",
    "content": "output \"windows_instance_public_ip\" {\n  description = \"The IPv4 address of the Windows instance. Enter this value into your RDP client when connecting to your instance.\"\n  value       = aws_instance.instance.public_ip\n}\n"
  },
  {
    "path": "examples/terraform-aws-ec2-windows-example/packer/build.pkr.hcl",
    "content": "variable \"instance_type\" {\n type = string\n description = \"The EC2 instance size / type to launch\"\n}\n\nvariable \"region\" {\n  type = string\n  description = \"The AWS region to deploy the Windows instance into\"\n}\n\n\ndata \"amazon-ami\" \"windows_server_2016\" {\n  filters = {\n    name                = \"Windows_Server-2016-English-Full-Base-*\"\n    root-device-type    = \"ebs\"\n    virtualization-type = \"hvm\"\n  }\n  most_recent = true\n  owners      = [\"801119661308\"]\n  region      = var.region \n}\n\nlocals {\n  build_version = \"${legacy_isotime(\"2006.01.02.150405\")}\"\n}\n\nsource \"amazon-ebs\" \"windows_server_2016\" {\n  ami_name                    = \"WIN2016-CUSTOM-${local.build_version}\"\n  associate_public_ip_address = true\n  communicator                = \"winrm\"\n  instance_type               = var.instance_type \n  region                      = var.region \n  source_ami                  = \"${data.amazon-ami.windows_server_2016.id}\"\n  user_data_file              = \"${path.root}/scripts/bootstrap_windows.txt\"\n  winrm_timeout               = \"15m\"\n  winrm_password              = \"SuperS3cr3t!!!!\"\n  winrm_username              = \"Administrator\"\n\n}\n\nbuild {\n  sources = [\"source.amazon-ebs.windows_server_2016\"]\n\n  # Install Chocolatey package manager, then install any Chocolatey packages defined in scripts/install_packages.ps1\n  provisioner \"powershell\" {\n    scripts = [\"${path.root}/scripts/install_chocolatey.ps1\", \"${path.root}/scripts/install_packages.ps1\"]\n  }\n\n  provisioner \"windows-restart\" {\n    restart_timeout = \"35m\"\n  }\n}\n"
  },
  {
    "path": "examples/terraform-aws-ec2-windows-example/packer/scripts/bootstrap_windows.txt",
    "content": "<powershell>\n# This script is adapted from: https://learn.hashicorp.com/tutorials/packer/aws-windows-image?in=packer/integrations\n\n# Set administrator password\nnet user Administrator SuperS3cr3t!!!!\nwmic useraccount where \"name='Administrator'\" set PasswordExpires=FALSE\n\n# First, make sure WinRM can't be connected to\nnetsh advfirewall firewall set rule name=\"Windows Remote Management (HTTP-In)\" new enable=yes action=block\n\n# Delete any existing WinRM listeners\nwinrm delete winrm/config/listener?Address=*+Transport=HTTP  2>$Null\nwinrm delete winrm/config/listener?Address=*+Transport=HTTPS 2>$Null\n\n# Disable group policies which block basic authentication and unencrypted login\n\nSet-ItemProperty -Path HKLM:\\Software\\Policies\\Microsoft\\Windows\\WinRM\\Client -Name AllowBasic -Value 1\nSet-ItemProperty -Path HKLM:\\Software\\Policies\\Microsoft\\Windows\\WinRM\\Client -Name AllowUnencryptedTraffic -Value 1\nSet-ItemProperty -Path HKLM:\\Software\\Policies\\Microsoft\\Windows\\WinRM\\Service -Name AllowBasic -Value 1\nSet-ItemProperty -Path HKLM:\\Software\\Policies\\Microsoft\\Windows\\WinRM\\Service -Name AllowUnencryptedTraffic -Value 1\n\n\n# Create a new WinRM listener and configure\nwinrm create winrm/config/listener?Address=*+Transport=HTTP\nwinrm set winrm/config/winrs '@{MaxMemoryPerShellMB=\"0\"}'\nwinrm set winrm/config '@{MaxTimeoutms=\"7200000\"}'\nwinrm set winrm/config/service '@{AllowUnencrypted=\"true\"}'\nwinrm set winrm/config/service '@{MaxConcurrentOperationsPerUser=\"12000\"}'\nwinrm set winrm/config/service/auth '@{Basic=\"true\"}'\nwinrm set winrm/config/client/auth '@{Basic=\"true\"}'\n\n# Configure UAC to allow privilege elevation in remote shells\n$Key = 'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Policies\\System'\n$Setting = 'LocalAccountTokenFilterPolicy'\nSet-ItemProperty -Path $Key -Name $Setting -Value 1 -Force\n\n# Configure and restart the WinRM Service; Enable the required firewall exception\nStop-Service -Name WinRM\nSet-Service -Name WinRM -StartupType Automatic\nnetsh advfirewall firewall set rule name=\"Windows Remote Management (HTTP-In)\" new action=allow localip=any remoteip=any\nStart-Service -Name WinRM\n</powershell>\n\n"
  },
  {
    "path": "examples/terraform-aws-ec2-windows-example/packer/scripts/install_chocolatey.ps1",
    "content": "Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))\n"
  },
  {
    "path": "examples/terraform-aws-ec2-windows-example/packer/scripts/install_packages.ps1",
    "content": "choco install -y python3\nchoco install -y git\n"
  },
  {
    "path": "examples/terraform-aws-ec2-windows-example/variables.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# ENVIRONMENT VARIABLES\n# Define these secrets as environment variables\n# ---------------------------------------------------------------------------------------------------------------------\n\n# AWS_ACCESS_KEY_ID\n# AWS_SECRET_ACCESS_KEY\n\n# ---------------------------------------------------------------------------------------------------------------------\n# REQUIRED PARAMETERS\n# You must provide a value for each of these parameters.\n# ---------------------------------------------------------------------------------------------------------------------\n\nvariable \"region\" {\n  description = \"The AWS region in which all resources will be created\"\n  type        = string\n  default     = \"us-west-2\"\n}\n\nvariable \"ami\" {\n  description = \"The ID of the AMI to run on the Windows instance.\"\n  type        = string\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# OPTIONAL PARAMETERS\n# These parameters have reasonable defaults.\n# ---------------------------------------------------------------------------------------------------------------------\n\nvariable \"name\" {\n  description = \"The name of the Windows instance\"\n  type        = string\n  default     = \"windows_test_instance\"\n}\n\nvariable \"instance_type\" {\n  description = \"The instance type to deploy.\"\n  type        = string\n  default     = \"t3.small\"\n}\n\nvariable \"root_volume_size\" {\n  description = \"The size in GiB of the root volume. Must match the root volume size of the target AMI.\"\n  type        = number\n  default     = 30\n}\n\n"
  },
  {
    "path": "examples/terraform-aws-ecs-example/README.md",
    "content": "# Terraform AWS ECS Example\n\nThis folder contains a simple Terraform module that deploys a simple ECS service in [AWS](https://aws.amazon.com/)\nto demonstrate how you can use Terratest to write automated tests for your AWS Terraform code. \n\nThis module registers a task definition with [AWS Fargate](https://aws.amazon.com/fargate/) launch type and associates it with a [service](https://docs.aws.amazon.com/AmazonECS/latest/userguide/ecs_services.html)\nto run and maintain a specified number of instances.\n\nCheck out [test/terraform_aws_ecs_example_test.go](/test/terraform_aws_ecs_example_test.go) to see how you can write\nautomated tests for this module and validate the configuration of the parameters and options.\n\n**WARNING**: This module and the automated tests for it deploy real resources into your AWS account which can cost you\nmoney.\n\n## Running this module manually\n\n1. Sign up for [AWS](https://aws.amazon.com/).\n1. Configure your AWS credentials using one of the [supported methods for AWS CLI\n   tools](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html), such as setting the\n   `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` environment variables. If you're using the `~/.aws/config` file for profiles then export `AWS_SDK_LOAD_CONFIG` as \"True\".\n1. Set the AWS region you want to use as the environment variable `AWS_DEFAULT_REGION`.\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`.\n1. Run `terraform init`.\n1. Run `terraform apply`.\n1. When you're done, run `terraform destroy`.\n\n## Running automated tests against this module\n\n1. Sign up for [AWS](https://aws.amazon.com/).\n1. Configure your AWS credentials using one of the [supported methods for AWS CLI\n   tools](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html), such as setting the\n   `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` environment variables. If you're using the `~/.aws/config` file for profiles then export `AWS_SDK_LOAD_CONFIG` as \"True\".\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`.\n1. Install [Golang](https://golang.org/) and make sure this code is checked out into your `GOPATH`.\n1. `cd test`\n1. `dep ensure`\n1. `go test -v -run TestTerraformAwsEcsExample`\n"
  },
  {
    "path": "examples/terraform-aws-ecs-example/main.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# PIN TERRAFORM VERSION TO >= 0.12\n# The examples have been upgraded to 0.12 syntax\n# ---------------------------------------------------------------------------------------------------------------------\nprovider \"aws\" {\n  region = var.region\n}\n\nterraform {\n  # This module is now only being tested with Terraform 0.13.x. However, to make upgrading easier, we are setting\n  # 0.12.26 as the minimum version, as that version added support for required_providers with source URLs, making it\n  # forwards compatible with 0.13.x code.\n  required_version = \">= 0.12.26\"\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY INTO THE DEFAULT VPC AND SUBNETS\n# To keep this example simple, we are deploying into the Default VPC and its subnets. In real-world usage, you should\n# deploy into a custom VPC and private subnets.\n# ---------------------------------------------------------------------------------------------------------------------\n\ndata \"aws_vpc\" \"default\" {\n  default = true\n}\n\ndata \"aws_subnets\" \"all\" {\n  filter {\n    name   = \"vpc-id\"\n    values = [data.aws_vpc.default.id]\n  }\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# CREATE THE ECS CLUSTER\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"aws_ecs_cluster\" \"example\" {\n  name = var.cluster_name\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# CREATE THE ECS SERVICE AND ITS TASK DEFINITION\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"aws_ecs_service\" \"example\" {\n  name            = var.service_name\n  cluster         = aws_ecs_cluster.example.arn\n  task_definition = aws_ecs_task_definition.example.arn\n  desired_count   = 0\n  launch_type     = \"FARGATE\"\n\n  network_configuration {\n    subnets = data.aws_subnets.all.ids\n  }\n}\n\nresource \"aws_ecs_task_definition\" \"example\" {\n  family                   = \"terratest\"\n  network_mode             = \"awsvpc\"\n  cpu                      = 256\n  memory                   = 512\n  requires_compatibilities = [\"FARGATE\"]\n  execution_role_arn       = aws_iam_role.execution.arn\n  container_definitions    = <<-JSON\n    [\n      {\n        \"image\": \"terraterst-example\",\n        \"name\": \"terratest\",\n        \"networkMode\": \"awsvpc\"\n      }\n    ]\nJSON\n\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# CREATE THE ECS TASK EXECUTION ROLE AND ATTACH APPROPRIATE AWS MANAGED POLICY\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"aws_iam_role\" \"execution\" {\n  name               = \"${var.cluster_name}-ecs-execution\"\n  assume_role_policy = data.aws_iam_policy_document.assume-execution.json\n}\n\nresource \"aws_iam_role_policy_attachment\" \"execution\" {\n  role       = aws_iam_role.execution.id\n  policy_arn = \"arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy\"\n}\n\ndata \"aws_iam_policy_document\" \"assume-execution\" {\n  statement {\n    effect  = \"Allow\"\n    actions = [\"sts:AssumeRole\"]\n    principals {\n      type        = \"Service\"\n      identifiers = [\"ecs-tasks.amazonaws.com\"]\n    }\n  }\n}\n\n"
  },
  {
    "path": "examples/terraform-aws-ecs-example/outputs.tf",
    "content": "output \"task_definition\" {\n  value = aws_ecs_task_definition.example.arn\n}\n\n"
  },
  {
    "path": "examples/terraform-aws-ecs-example/variables.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# ENVIRONMENT VARIABLES\n# Define these secrets as environment variables\n# ---------------------------------------------------------------------------------------------------------------------\n\n# AWS_ACCESS_KEY_ID\n# AWS_SECRET_ACCESS_KEY\n\n# ---------------------------------------------------------------------------------------------------------------------\n# REQUIRED PARAMETERS\n# You must provide a value for each of these parameters.\n# ---------------------------------------------------------------------------------------------------------------------\nvariable \"region\" {\n  description = \"The AWS region to deploy to\"\n  type        = string\n}\n# ---------------------------------------------------------------------------------------------------------------------\n# OPTIONAL PARAMETERS\n# These parameters have reasonable defaults.\n# ---------------------------------------------------------------------------------------------------------------------\n\nvariable \"cluster_name\" {\n  description = \"The name to set for the ECS cluster.\"\n  type        = string\n  default     = \"terratest-example\"\n}\n\nvariable \"service_name\" {\n  description = \"The name to set for the ECS service.\"\n  type        = string\n  default     = \"terratest-example\"\n}\n\n"
  },
  {
    "path": "examples/terraform-aws-example/README.md",
    "content": "# Terraform AWS Example\n\nThis folder contains a simple Terraform module that deploys resources in [AWS](https://aws.amazon.com/) to demonstrate\nhow you can use Terratest to write automated tests for your AWS Terraform code. This module deploys an [EC2\nInstance](https://aws.amazon.com/ec2/) and gives that Instance a `Name` tag with the value specified in the\n`instance_name` variable.\n\nCheck out [test/terraform_aws_example_test.go](/test/terraform_aws_example_test.go) to see how you can write\nautomated tests for this module.\n\nNote that the EC2 Instance in this module doesn't actually do anything; it just runs a Vanilla Ubuntu 16.04 AMI for\ndemonstration purposes. For slightly more complicated, real-world examples of Terraform modules, see\n[terraform-http-example](/examples/terraform-http-example) and [terraform-ssh-example](/examples/terraform-ssh-example).\n\n**WARNING**: This module and the automated tests for it deploy real resources into your AWS account which can cost you\nmoney. The resources are all part of the [AWS Free Tier](https://aws.amazon.com/free/), so if you haven't used that up,\nit should be free, but you are completely responsible for all AWS charges.\n\n\n\n\n\n## Running this module manually\n\n1. Sign up for [AWS](https://aws.amazon.com/).\n1. Configure your AWS credentials using one of the [supported methods for AWS CLI\n   tools](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html), such as setting the\n   `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` environment variables. If you're using the `~/.aws/config` file for profiles then export `AWS_SDK_LOAD_CONFIG` as \"True\".\n1. Set the AWS region you want to use as the environment variable `AWS_DEFAULT_REGION`.\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`.\n1. Run `terraform init`.\n1. Run `terraform apply`.\n1. When you're done, run `terraform destroy`.\n\n\n\n\n## Running automated tests against this module\n\n1. Sign up for [AWS](https://aws.amazon.com/).\n1. Configure your AWS credentials using one of the [supported methods for AWS CLI\n   tools](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html), such as setting the\n   `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` environment variables. If you're using the `~/.aws/config` file for profiles then export `AWS_SDK_LOAD_CONFIG` as \"True\".\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`.\n1. Install [Golang](https://golang.org/) and make sure this code is checked out into your `GOPATH`.\n1. `cd test`\n1. `dep ensure`\n1. `go test -v -run TestTerraformAwsExample`\n"
  },
  {
    "path": "examples/terraform-aws-example/main.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# PIN TERRAFORM VERSION TO >= 0.12\n# The examples have been upgraded to 0.12 syntax\n# ---------------------------------------------------------------------------------------------------------------------\n\nterraform {\n  # This module is now only being tested with Terraform 0.13.x. However, to make upgrading easier, we are setting\n  # 0.12.26 as the minimum version, as that version added support for required_providers with source URLs, making it\n  # forwards compatible with 0.13.x code.\n  required_version = \">= 0.12.26\"\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY AN EC2 INSTANCE RUNNING UBUNTU\n# See test/terraform_aws_example_test.go for how to write automated tests for this code.\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"aws_instance\" \"example\" {\n  ami           = data.aws_ami.ubuntu.id\n  instance_type = var.instance_type\n\n  tags = {\n    Name = var.instance_name\n  }\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# LOOK UP THE LATEST UBUNTU AMI\n# ---------------------------------------------------------------------------------------------------------------------\n\ndata \"aws_ami\" \"ubuntu\" {\n  most_recent = true\n  owners      = [\"099720109477\"] # Canonical\n\n  filter {\n    name   = \"virtualization-type\"\n    values = [\"hvm\"]\n  }\n\n  filter {\n    name   = \"architecture\"\n    values = [\"x86_64\"]\n  }\n\n  filter {\n    name   = \"image-type\"\n    values = [\"machine\"]\n  }\n\n  filter {\n    name   = \"name\"\n    values = [\"ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*\"]\n  }\n}\n\n"
  },
  {
    "path": "examples/terraform-aws-example/outputs.tf",
    "content": "output \"instance_id\" {\n  value = aws_instance.example.id\n}\n\n"
  },
  {
    "path": "examples/terraform-aws-example/variables.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# ENVIRONMENT VARIABLES\n# Define these secrets as environment variables\n# ---------------------------------------------------------------------------------------------------------------------\n\n# AWS_ACCESS_KEY_ID\n# AWS_SECRET_ACCESS_KEY\n\n# ---------------------------------------------------------------------------------------------------------------------\n# REQUIRED PARAMETERS\n# You must provide a value for each of these parameters.\n# ---------------------------------------------------------------------------------------------------------------------\n\n# ---------------------------------------------------------------------------------------------------------------------\n# OPTIONAL PARAMETERS\n# These parameters have reasonable defaults.\n# ---------------------------------------------------------------------------------------------------------------------\n\nvariable \"instance_name\" {\n  description = \"The Name tag to set for the EC2 Instance.\"\n  type        = string\n  default     = \"terratest-example\"\n}\n\nvariable \"instance_type\" {\n  description = \"The EC2 instance type to run.\"\n  type        = string\n  default     = \"t2.micro\"\n}\n"
  },
  {
    "path": "examples/terraform-aws-hello-world-example/README.md",
    "content": "# Terraform AWS \"Hello, World\" Example\n\nThis folder contains a simple Terraform module that deploys resources in [AWS](https://aws.amazon.com/) to demonstrate\nhow you can use Terratest to write automated tests for your AWS Terraform code. This module deploys an [EC2\nInstance](https://aws.amazon.com/ec2/) which runs a web server that responds with \"Hello, World!\" on port 8080.\n\nCheck out [test/terraform_aws_hello_world_example_test.go](/test/terraform_aws_hello_world_example_test.go) to see how \nyou can write automated tests for this module.\n\n**WARNING**: This module and the automated tests for it deploy real resources into your AWS account which can cost you\nmoney. The resources are all part of the [AWS Free Tier](https://aws.amazon.com/free/), so if you haven't used that up,\nit should be free, but you are completely responsible for all AWS charges.\n\n\n\n\n\n## Running this module manually\n\n1. Sign up for [AWS](https://aws.amazon.com/).\n1. Configure your AWS credentials using one of the [supported methods for AWS CLI\n   tools](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html), such as setting the\n   `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` environment variables. If you're using the `~/.aws/config` file for profiles then export `AWS_SDK_LOAD_CONFIG` as \"True\".\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`.\n1. Run `terraform init`.\n1. Run `terraform apply`.\n1. When you're done, run `terraform destroy`.\n\n\n\n\n## Running automated tests against this module\n\n1. Sign up for [AWS](https://aws.amazon.com/).\n1. Configure your AWS credentials using one of the [supported methods for AWS CLI\n   tools](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html), such as setting the\n   `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` environment variables. If you're using the `~/.aws/config` file for profiles then export `AWS_SDK_LOAD_CONFIG` as \"True\".\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`.\n1. Install [Golang](https://golang.org/) and make sure this code is checked out into your `GOPATH`.\n1. `cd test`\n1. `go test -v -run TestTerraformAwsHelloWorldExample`\n"
  },
  {
    "path": "examples/terraform-aws-hello-world-example/main.tf",
    "content": "terraform {\n  # This module is now only being tested with Terraform 0.13.x. However, to make upgrading easier, we are setting\n  # 0.12.26 as the minimum version, as that version added support for required_providers with source URLs, making it\n  # forwards compatible with 0.13.x code.\n  required_version = \">= 0.12.26\"\n}\n\nprovider \"aws\" {\n  region = \"us-east-2\"\n}\n\n# website::tag::1:: Deploy an EC2 Instance.\nresource \"aws_instance\" \"example\" {\n  # website::tag::2:: Run an Ubuntu 18.04 AMI on the EC2 instance.\n  ami                    = \"ami-0d5d9d301c853a04a\"\n  instance_type          = \"t2.micro\"\n  vpc_security_group_ids = [aws_security_group.instance.id]\n\n  # website::tag::3:: When the instance boots, start a web server on port 8080 that responds with \"Hello, World!\".\n  user_data = <<EOF\n#!/bin/bash\necho \"Hello, World!\" > index.html\nnohup busybox httpd -f -p 8080 &\nEOF\n}\n\n# website::tag::4:: Allow the instance to receive requests on port 8080.\nresource \"aws_security_group\" \"instance\" {\n  ingress {\n    from_port   = 8080\n    to_port     = 8080\n    protocol    = \"tcp\"\n    cidr_blocks = [\"0.0.0.0/0\"]\n  }\n}\n\n# website::tag::5:: Output the instance's public IP address.\noutput \"public_ip\" {\n  value = aws_instance.example.public_ip\n}"
  },
  {
    "path": "examples/terraform-aws-lambda-example/.gitignore",
    "content": "*.zip"
  },
  {
    "path": "examples/terraform-aws-lambda-example/README.md",
    "content": "# Terraform Lambda Example\n\nThis folder contains a Terraform module to demonstrate how you can use Terratest to deploy a lambda function\nfor your Terraform code. This module takes in an input variable called `function_name`, and uses the function name as\nan identifier for the lambda and associated resources (e.g. IAM role).\n\nCheck out [test/terraform_aws_lambda_example_test.go](/test/terraform_aws_lambda_example_test.go) to see how you can write\nautomated tests for this simple module.\n\nThe function that this module creates is a simple one whose input can cause it to error or echo messages it receives.\n\n## Running this module manually\n\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`.\n1. Run `terraform init`.\n1. Run `terraform apply`.\n1. When you're done, run `terraform destroy`.\n\n## Running automated tests against this module\n\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`.\n1. Install [Golang](https://golang.org/) and make sure this code is checked out into your `GOPATH`.\n1. `cd test`\n1. `dep ensure`\n1. `go test -v -run TestTerraformAwsLambdaExample`\n"
  },
  {
    "path": "examples/terraform-aws-lambda-example/main.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# AWS LAMBDA TERRAFORM EXAMPLE\n# See test/terraform_aws_lambda_example_test.go for how to write automated tests for this code.\n# ---------------------------------------------------------------------------------------------------------------------\nprovider \"aws\" {\n  region = var.region\n}\n\nterraform {\n  # This module is now only being tested with Terraform 0.13.x. However, to make upgrading easier, we are setting\n  # 0.12.26 as the minimum version, as that version added support for required_providers with source URLs, making it\n  # forwards compatible with 0.13.x code.\n  required_version = \">= 0.12.26\"\n}\n\nprovider \"archive\" {\n  version = \"1.3\"\n}\n\ndata \"archive_file\" \"zip\" {\n  type        = \"zip\"\n  source_dir  = \"${path.module}/src\"\n  output_path = \"${path.module}/${var.function_name}.zip\"\n}\n\nresource \"aws_lambda_function\" \"lambda\" {\n  filename         = data.archive_file.zip.output_path\n  source_code_hash = data.archive_file.zip.output_base64sha256\n  function_name    = var.function_name\n  role             = aws_iam_role.lambda.arn\n  handler          = \"bootstrap\"\n  runtime          = \"provided.al2023\"\n}\n\nresource \"aws_iam_role\" \"lambda\" {\n  name               = var.function_name\n  assume_role_policy = data.aws_iam_policy_document.policy.json\n}\n\ndata \"aws_iam_policy_document\" \"policy\" {\n  statement {\n    actions = [\"sts:AssumeRole\"]\n    principals {\n      type        = \"Service\"\n      identifiers = [\"lambda.amazonaws.com\"]\n    }\n  }\n}\n"
  },
  {
    "path": "examples/terraform-aws-lambda-example/src/README.md",
    "content": "# AWS Lambda Function Handler Source\n\nThe lambda executable `handler` was built using\n\n``` shell\ngo get github.com/aws/aws-lambda-go/lambda\nGOOS=linux GOARCH=amd64 go build -tags lambda.norpc -o bootstrap .\n```\n"
  },
  {
    "path": "examples/terraform-aws-lambda-example/src/bootstrap.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/aws/aws-lambda-go/lambda\"\n)\n\ntype Event struct {\n\tShouldFail bool   `json:\"ShouldFail\"`\n\tEcho       string `json:\"Echo\"`\n}\n\n// HandleRequest Fails if ShouldFail is `true`, otherwise echos the input.\nfunc HandleRequest(ctx context.Context, evnt *Event) (string, error) {\n\tif evnt == nil {\n\t\treturn \"\", fmt.Errorf(\"received nil event\")\n\t}\n\tif evnt.ShouldFail {\n\t\treturn \"\", fmt.Errorf(\"failed to handle %#v\", evnt)\n\t}\n\treturn evnt.Echo, nil\n}\n\nfunc main() {\n\tlambda.Start(HandleRequest)\n}\n"
  },
  {
    "path": "examples/terraform-aws-lambda-example/variables.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# ENVIRONMENT VARIABLES\n# Define these secrets as environment variables\n# ---------------------------------------------------------------------------------------------------------------------\n\n# AWS_ACCESS_KEY_ID\n# AWS_SECRET_ACCESS_KEY\n\n# ---------------------------------------------------------------------------------------------------------------------\n# REQUIRED PARAMETERS\n# You must provide a value for each of these parameters.\n# ---------------------------------------------------------------------------------------------------------------------\nvariable \"region\" {\n  description = \"The AWS region to deploy to\"\n  type        = string\n}\n\nvariable \"function_name\" {\n  description = \"The name of the function to provision\"\n}\n# ---------------------------------------------------------------------------------------------------------------------\n# OPTIONAL PARAMETERS\n# These parameters have reasonable defaults.\n# ---------------------------------------------------------------------------------------------------------------------\n"
  },
  {
    "path": "examples/terraform-aws-network-example/README.md",
    "content": "# Terraform AWS Network Example\n\nThis folder contains a Terraform module that deploys a simple network setup to demonstrate how you can use Terratest to write automated tests for your AWS Terraform code. This module deploys two subnets within one availability zone. One subnet is public - it has a route to an internet gateway. The other subnet is private. There is a NAT gateway deployed and configured for it. \nCheck out [test/terraform_aws_network_example_test.go](/test/terraform_aws_network_example_test.go) to see how you can write automated tests for this module and verify the basic parameters of the VPC and subnets.\n\n**WARNING**: This module and the automated tests for it deploy real resources into your AWS account which can cost you\nmoney. The resources are all part of the [AWS Free Tier](https://aws.amazon.com/rds/free/), so if you haven't used that up,\nit should be free, but you are completely responsible for all AWS charges.\n\n\n\n## Running this module manually\n\n1. Sign up for [AWS](https://aws.amazon.com/).\n1. Configure your AWS credentials using one of the [supported methods for AWS CLI\n   tools](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html), such as setting the\n   `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` environment variables. If you're using the `~/.aws/config` file for profiles then export `AWS_SDK_LOAD_CONFIG` as \"True\".\n1. Set the AWS region you want to use as the environment variable `AWS_DEFAULT_REGION`.\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`.\n1. Run `terraform init`.\n1. Run `terraform apply`.\n1. When you're done, run `terraform destroy`.\n\n\n\n\n## Running automated tests against this module\n\n1. Sign up for [AWS](https://aws.amazon.com/).\n1. Configure your AWS credentials using one of the [supported methods for AWS CLI\n   tools](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html), such as setting the\n   `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` environment variables. If you're using the `~/.aws/config` file for profiles then export `AWS_SDK_LOAD_CONFIG` as \"True\".\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`.\n1. Install [Golang](https://golang.org/) and make sure this code is checked out into your `GOPATH`.\n1. `cd test`\n1. `dep ensure`\n1. `go test -v -run TestTerraformAwsNetworkExample`\n"
  },
  {
    "path": "examples/terraform-aws-network-example/main.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# PIN TERRAFORM VERSION TO >= 0.12\n# The examples have been upgraded to 0.12 syntax\n# ---------------------------------------------------------------------------------------------------------------------\n\nterraform {\n  # This module is now only being tested with Terraform 0.13.x. However, to make upgrading easier, we are setting\n  # 0.12.26 as the minimum version, as that version added support for required_providers with source URLs, making it\n  # forwards compatible with 0.13.x code.\n  required_version = \">= 0.12.26\"\n}\n\ndata \"aws_availability_zones\" \"available\" {\n  state = \"available\"\n}\n\nprovider \"aws\" {\n  region = var.aws_region\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY A SIMPLE NETWORK\n# The network has an internet gateway and two subnets - private and public - in the same availability zone.\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"aws_vpc\" \"main\" {\n  cidr_block = var.main_vpc_cidr\n\n  tags = {\n    Name = var.tag_name\n  }\n}\n\nresource \"aws_internet_gateway\" \"main_gateway\" {\n  vpc_id = aws_vpc.main.id\n\n  tags = {\n    Name = var.tag_name\n  }\n}\n\nresource \"aws_subnet\" \"private\" {\n  vpc_id                  = aws_vpc.main.id\n  cidr_block              = var.private_subnet_cidr\n  map_public_ip_on_launch = false\n\n  tags = {\n    Name = var.tag_name\n  }\n\n  availability_zone = data.aws_availability_zones.available.names[0]\n}\n\nresource \"aws_subnet\" \"public\" {\n  vpc_id                  = aws_vpc.main.id\n  cidr_block              = var.public_subnet_cidr\n  map_public_ip_on_launch = true\n\n  tags = {\n    Name = var.tag_name\n  }\n\n  availability_zone = data.aws_availability_zones.available.names[0]\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# CREATE AND ATTACH A ROUTING TABLE FOR THE PUBLIC NETWORK\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"aws_route_table\" \"public\" {\n  vpc_id = aws_vpc.main.id\n\n  route {\n    cidr_block = \"91.189.0.0/24\"\n    gateway_id = aws_internet_gateway.main_gateway.id\n  }\n\n  tags = {\n    Name = var.tag_name\n  }\n}\n\nresource \"aws_route_table_association\" \"public\" {\n  subnet_id      = aws_subnet.public.id\n  route_table_id = aws_route_table.public.id\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# CREATE NAT GATEWAY FOR THE PRIVATE SUBNET\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"aws_eip\" \"nat\" {\n  domain = \"vpc\"\n}\n\nresource \"aws_nat_gateway\" \"nat\" {\n  allocation_id = aws_eip.nat.id\n  subnet_id     = aws_subnet.public.id\n  depends_on    = [aws_internet_gateway.main_gateway]\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# CREATE AND ATTACH A ROUTING TABLE FOR THE PRIVATE NETWORK\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"aws_route_table\" \"private\" {\n  vpc_id = aws_vpc.main.id\n\n  route {\n    cidr_block     = \"0.0.0.0/0\"\n    nat_gateway_id = aws_nat_gateway.nat.id\n  }\n\n  tags = {\n    Name = var.tag_name\n  }\n}\n\nresource \"aws_route_table_association\" \"private\" {\n  subnet_id      = aws_subnet.private.id\n  route_table_id = aws_route_table.private.id\n}\n\n"
  },
  {
    "path": "examples/terraform-aws-network-example/output.tf",
    "content": "output \"main_vpc_id\" {\n  value       = aws_vpc.main.id\n  description = \"The main VPC id\"\n}\n\noutput \"public_subnet_id\" {\n  value       = aws_subnet.public.id\n  description = \"The public subnet id\"\n}\n\noutput \"private_subnet_id\" {\n  value       = aws_subnet.private.id\n  description = \"The private subnet id\"\n}\n\n"
  },
  {
    "path": "examples/terraform-aws-network-example/variables.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# ENVIRONMENT VARIABLES\n# Define these secrets as environment variables\n# ---------------------------------------------------------------------------------------------------------------------\n\n# AWS_ACCESS_KEY_ID\n# AWS_SECRET_ACCESS_KEY\n\n# ---------------------------------------------------------------------------------------------------------------------\n# REQUIRED PARAMETERS\n# You must provide a value for each of these parameters.\n# ---------------------------------------------------------------------------------------------------------------------\n\nvariable \"main_vpc_cidr\" {\n  description = \"The CIDR of the main VPC\"\n  type        = string\n}\n\nvariable \"public_subnet_cidr\" {\n  description = \"The CIDR of public subnet\"\n  type        = string\n}\n\nvariable \"private_subnet_cidr\" {\n  description = \"The CIDR of the private subnet\"\n  type        = string\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# OPTIONAL PARAMETERS\n# These parameters have reasonable defaults.\n# ---------------------------------------------------------------------------------------------------------------------\n\nvariable \"aws_region\" {\n  description = \"The AWS region to deploy into\"\n  type        = string\n  default     = \"us-east-1\"\n}\n\nvariable \"tag_name\" {\n  description = \"A name used to tag the resource\"\n  type        = string\n  default     = \"terraform-network-example\"\n}\n\n"
  },
  {
    "path": "examples/terraform-aws-rds-example/README.md",
    "content": "# Terraform AWS RDS Example\n\nThis folder contains a simple Terraform module that deploys a database instance (MySQL by default) in [AWS](https://aws.amazon.com/)\nto demonstrate how you can use Terratest to write automated tests for your AWS Terraform code. This module deploys an [RDS\nInstance](https://aws.amazon.com/rds/) and associates it with an option group and parameter group to customize it.\n\nCheck out [test/terraform_aws_rds_example_test.go](/test/terraform_aws_rds_example_test.go) to see how you can write\nautomated tests for this module and validate the configuration of the parameters and options.\n\nThis module does not use the database instance created in any way. It can be used though to validate any combination of inputs\npassed while creating database instances in AWS RDS. Hence the plain text simple password used here should not have any security\nimplications.\n\n**WARNING**: This module and the automated tests for it deploy real resources into your AWS account which can cost you\nmoney. The resources are all part of the [AWS Free Tier](https://aws.amazon.com/rds/free/), so if you haven't used that up,\nit should be free, but you are completely responsible for all AWS charges.\n\n\n\n\n\n## Running this module manually\n\n1. Sign up for [AWS](https://aws.amazon.com/).\n1. Configure your AWS credentials using one of the [supported methods for AWS CLI\n   tools](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html), such as setting the\n   `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` environment variables. If you're using the `~/.aws/config` file for profiles then export `AWS_SDK_LOAD_CONFIG` as \"True\".\n1. Set the AWS region you want to use as the environment variable `AWS_DEFAULT_REGION`.\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`.\n1. Run `terraform init`.\n1. Run `terraform apply`.\n1. When you're done, run `terraform destroy`.\n\n\n\n\n## Running automated tests against this module\n\n1. Sign up for [AWS](https://aws.amazon.com/).\n1. Configure your AWS credentials using one of the [supported methods for AWS CLI\n   tools](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html), such as setting the\n   `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` environment variables. If you're using the `~/.aws/config` file for profiles then export `AWS_SDK_LOAD_CONFIG` as \"True\".\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`.\n1. Install [Golang](https://golang.org/) and make sure this code is checked out into your `GOPATH`.\n1. `cd test`\n1. `dep ensure`\n1. `go test -v -run TestTerraformAwsRdsExample`\n"
  },
  {
    "path": "examples/terraform-aws-rds-example/main.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# PIN TERRAFORM VERSION TO >= 0.12\n# The examples have been upgraded to 0.12 syntax\n# ---------------------------------------------------------------------------------------------------------------------\n\nprovider \"aws\" {\n  region = var.region\n}\n\nterraform {\n  # This module is now only being tested with Terraform 0.13.x. However, to make upgrading easier, we are setting\n  # 0.12.31 as the minimum version, as that version added support for required_providers with source URLs, making it\n  # forwards compatible with 0.13.x code.\n  required_version = \">= 0.12.31\"\n\n  required_providers {\n    aws = {\n      source  = \"hashicorp/aws\"\n      version = \">= 4.61.0, < 5.0.0\"\n    }\n  }\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY INTO THE DEFAULT VPC AND SUBNETS\n# To keep this example simple, we are deploying into the Default VPC and its subnets. In real-world usage, you should\n# deploy into a custom VPC and private subnets. Given the subnet group needs to span multiple AZs and hence subnets we\n# have deployed it across all the subnets of the default VPC.\n# ---------------------------------------------------------------------------------------------------------------------\n\ndata \"aws_vpc\" \"default\" {\n  default = true\n}\n\ndata \"aws_subnets\" \"all\" {\n  filter {\n    name   = \"vpc-id\"\n    values = [data.aws_vpc.default.id]\n  }\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# CREATE AN SUBNET GROUP ACROSS ALL THE SUBNETS OF THE DEFAULT ASG TO HOST THE RDS INSTANCE\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"aws_db_subnet_group\" \"example\" {\n  name       = var.name\n  subnet_ids = data.aws_subnets.all.ids\n\n  tags = {\n    Name = var.name\n  }\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# CREATE A CUSTOM PARAMETER GROUP AND AN OPTION GROUP FOR CONFIGURABILITY\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"aws_db_option_group\" \"example\" {\n  name                 = var.name\n  engine_name          = var.engine_name\n  major_engine_version = var.major_engine_version\n\n  tags = {\n    Name = var.name\n  }\n\n  dynamic \"option\" {\n    for_each = var.engine_name == \"mysql\" ? [1] : []\n    content {\n      option_name = \"MARIADB_AUDIT_PLUGIN\"\n\n      option_settings {\n        name  = \"SERVER_AUDIT_EVENTS\"\n        value = \"CONNECT\"\n      }\n    }\n  }\n}\n\nresource \"aws_db_parameter_group\" \"example\" {\n  name   = var.name\n  family = var.family\n\n  tags = {\n    Name = var.name\n  }\n\n  dynamic \"parameter\" {\n    for_each = var.engine_name == \"mysql\" ? [1] : []\n    content {\n      name  = \"general_log\"\n      value = \"0\"\n\n    }\n  }\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# CREATE A SECURITY GROUP TO ALLOW ACCESS TO THE RDS INSTANCE\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"aws_security_group\" \"db_instance\" {\n  name   = var.name\n  vpc_id = data.aws_vpc.default.id\n}\n\nresource \"aws_security_group_rule\" \"allow_db_access\" {\n  type              = \"ingress\"\n  from_port         = var.port\n  to_port           = var.port\n  protocol          = \"tcp\"\n  security_group_id = aws_security_group.db_instance.id\n  cidr_blocks       = [\"0.0.0.0/0\"]\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# CREATE THE DATABASE INSTANCE\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"aws_db_instance\" \"example\" {\n  identifier             = var.name\n  engine                 = var.engine_name\n  engine_version         = var.engine_version\n  port                   = var.port\n  db_name                = var.database_name\n  username               = var.username\n  password               = var.password\n  instance_class         = var.instance_class\n  allocated_storage      = var.allocated_storage\n  skip_final_snapshot    = true\n  license_model          = var.license_model\n  db_subnet_group_name   = aws_db_subnet_group.example.id\n  vpc_security_group_ids = [aws_security_group.db_instance.id]\n  publicly_accessible    = true\n  parameter_group_name   = aws_db_parameter_group.example.id\n  option_group_name      = aws_db_option_group.example.id\n\n  tags = {\n    Name = var.name\n  }\n}\n"
  },
  {
    "path": "examples/terraform-aws-rds-example/outputs.tf",
    "content": "output \"db_instance_id\" {\n  value = aws_db_instance.example.id\n}\n\n"
  },
  {
    "path": "examples/terraform-aws-rds-example/variables.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# ENVIRONMENT VARIABLES\n# Define these secrets as environment variables\n# ---------------------------------------------------------------------------------------------------------------------\n\n# AWS_ACCESS_KEY_ID\n# AWS_SECRET_ACCESS_KEY\n\n# ---------------------------------------------------------------------------------------------------------------------\n# REQUIRED PARAMETERS\n# You must provide a value for each of these parameters.\n# Given these are credentials, security of the values should be considered.\n# ---------------------------------------------------------------------------------------------------------------------\nvariable \"region\" {\n  description = \"The AWS region to deploy to\"\n  type        = string\n}\n\nvariable \"username\" {\n  description = \"Master username of the DB\"\n  type        = string\n}\n\nvariable \"password\" {\n  description = \"Master password of the DB\"\n  type        = string\n}\n\nvariable \"database_name\" {\n  description = \"Name of the database to be created\"\n  type        = string\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# OPTIONAL PARAMETERS\n# These parameters have reasonable defaults.\n# ---------------------------------------------------------------------------------------------------------------------\n\nvariable \"name\" {\n  description = \"Name of the database\"\n  type        = string\n  default     = \"terratest-example\"\n}\n\nvariable \"engine_name\" {\n  description = \"Name of the database engine\"\n  type        = string\n  default     = \"mysql\"\n}\n\nvariable \"family\" {\n  description = \"Family of the database\"\n  type        = string\n  default     = \"mysql5.7\"\n}\n\nvariable \"port\" {\n  description = \"Port which the database should run on\"\n  type        = number\n  default     = 3306\n}\n\nvariable \"major_engine_version\" {\n  description = \"MAJOR.MINOR version of the DB engine\"\n  type        = string\n  default     = \"5.7\"\n}\n\nvariable \"engine_version\" {\n  description = \"Version of the database to be launched\"\n  default     = \"5.7.21\"\n  type        = string\n}\n\nvariable \"allocated_storage\" {\n  description = \"Disk space to be allocated to the DB instance\"\n  type        = number\n  default     = 5\n}\n\nvariable \"license_model\" {\n  description = \"License model of the DB instance\"\n  type        = string\n  default     = \"general-public-license\"\n}\n\nvariable \"instance_class\" {\n  description = \"Instance class to be used to run the database\"\n  type        = string\n  default     = \"db.t2.micro\"\n}\n\n"
  },
  {
    "path": "examples/terraform-aws-s3-example/README.md",
    "content": "# Terraform AWS S3 Example\n\nThis folder contains a simple Terraform module that deploys resources in [AWS](https://aws.amazon.com/) to demonstrate\nhow you can use Terratest to write automated tests for your AWS Terraform code. This module deploys 2 [S3\nBuckets](https://aws.amazon.com/s3/) - one S3 Bucket with logging and versioning enabled, and another \"targetBucket\" one to serve as a\nlogging location for the first S3 Bucket. This module gives both Buckets a `Name` & `Environment` tag with the value \nspecified in the `tag_bucket_name` and `tag_bucket_environment` variables, respectively. This module also contains a terraform variable \nthat will create a basic bucket policy that will restrict the \"origin\" bucket to only accept SSL connections.\n\nCheck out [test/terraform_aws_s3_example_test.go](/test/terraform_aws_s3_example_test.go) to see how you can write\nautomated tests for this module.\n\nNote that the S3 Buckets in this module will not contain any actual objects/files after creation; they will only contain a \nversioning and logging configuration, as well as tags. For slightly more complicated, real-world examples of Terraform modules (with \nother AWS services), see [terraform-http-example](/examples/terraform-http-example) and \n[terraform-ssh-example](/examples/terraform-ssh-example).\n\n**WARNING**: This module and the automated tests for it deploy real resources into your AWS account which can cost you\nmoney. The resources are all part of the [AWS Free Tier](https://aws.amazon.com/free/), so if you haven't used that up,\nit should be free, but you are completely responsible for all AWS charges.\n\n\n\n\n\n## Running this module manually\n\n1. Sign up for [AWS](https://aws.amazon.com/).\n1. Configure your AWS credentials using one of the [supported methods for AWS CLI\n   tools](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html), such as setting the\n   `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` environment variables. If you're using the `~/.aws/config` file for profiles then export `AWS_SDK_LOAD_CONFIG` as \"True\".\n1. Set the AWS region you want to use as the environment variable `AWS_DEFAULT_REGION`.\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`.\n1. Run `terraform init`.\n1. Run `terraform apply`.\n1. When you're done, run `terraform destroy`.\n\n\n\n\n## Running automated tests against this module\n\n1. Sign up for [AWS](https://aws.amazon.com/).\n1. Configure your AWS credentials using one of the [supported methods for AWS CLI\n   tools](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html), such as setting the\n   `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` environment variables. If you're using the `~/.aws/config` file for profiles then export `AWS_SDK_LOAD_CONFIG` as \"True\".\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`.\n1. Install [Golang](https://golang.org/) and make sure this code is checked out into your `GOPATH`.\n1. `cd test`\n1. `dep ensure`\n1. `go test -v -run TestTerraformAwsS3Example`\n"
  },
  {
    "path": "examples/terraform-aws-s3-example/main.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# PIN TERRAFORM VERSION TO >= 0.12\n# The examples have been upgraded to 0.12 syntax\n# ---------------------------------------------------------------------------------------------------------------------\nprovider \"aws\" {\n  region = var.region\n}\n\nterraform {\n  # This module is now only being tested with Terraform 0.13.x. However, to make upgrading easier, we are setting\n  # 0.12.26 as the minimum version, as that version added support for required_providers with source URLs, making it\n  # forwards compatible with 0.13.x code.\n  required_version = \">= 0.12.26\"\n  required_providers {\n    aws = {\n      source = \"hashicorp/aws\"\n      # https://github.com/hashicorp/terraform-provider-aws/issues/33478\n      version = \"5.16.0\"\n    }\n  }\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY A S3 BUCKET WITH VERSIONING ENABLED INCLUDING TAGS\n# See test/terraform_aws_s3_example_test.go for how to write automated tests for this code.\n# ---------------------------------------------------------------------------------------------------------------------\n\n# Deploy and configure test S3 bucket with versioning and access log\nresource \"aws_s3_bucket\" \"test_bucket\" {\n  bucket = \"${local.aws_account_id}-${var.tag_bucket_name}\"\n\n  tags = {\n    Name        = var.tag_bucket_name\n    Environment = var.tag_bucket_environment\n  }\n}\n\nresource \"aws_s3_bucket_logging\" \"test_bucket\" {\n  bucket        = aws_s3_bucket.test_bucket.id\n  target_bucket = aws_s3_bucket.test_bucket_logs.id\n  target_prefix = \"TFStateLogs/\"\n}\n\nresource \"aws_s3_bucket_versioning\" \"test_bucket\" {\n  bucket = aws_s3_bucket.test_bucket.id\n  versioning_configuration {\n    status = \"Enabled\"\n  }\n}\n\nresource \"aws_s3_bucket_ownership_controls\" \"test_bucket\" {\n  bucket = aws_s3_bucket.test_bucket.id\n  rule {\n    object_ownership = \"ObjectWriter\"\n  }\n  depends_on = [aws_s3_bucket.test_bucket]\n}\n\nresource \"aws_s3_bucket_acl\" \"test_bucket\" {\n  bucket     = aws_s3_bucket.test_bucket.id\n  acl        = \"private\"\n  depends_on = [aws_s3_bucket_ownership_controls.test_bucket]\n}\n\n\n# Deploy S3 bucket to collect access logs for test bucket\nresource \"aws_s3_bucket\" \"test_bucket_logs\" {\n  bucket = \"${local.aws_account_id}-${var.tag_bucket_name}-logs\"\n\n  tags = {\n    Name        = \"${local.aws_account_id}-${var.tag_bucket_name}-logs\"\n    Environment = var.tag_bucket_environment\n  }\n\n  force_destroy = true\n}\n\nresource \"aws_s3_bucket_ownership_controls\" \"test_bucket_logs\" {\n  bucket = aws_s3_bucket.test_bucket_logs.id\n  rule {\n    object_ownership = \"ObjectWriter\"\n  }\n  depends_on = [aws_s3_bucket.test_bucket_logs]\n}\n\nresource \"aws_s3_bucket_acl\" \"test_bucket_logs\" {\n  bucket     = aws_s3_bucket.test_bucket_logs.id\n  acl        = \"log-delivery-write\"\n  depends_on = [aws_s3_bucket_ownership_controls.test_bucket_logs]\n}\n\n# Configure bucket access policies\n\nresource \"aws_s3_bucket_policy\" \"bucket_access_policy\" {\n  count  = var.with_policy ? 1 : 0\n  bucket = aws_s3_bucket.test_bucket.id\n  policy = data.aws_iam_policy_document.s3_bucket_policy.json\n}\n\ndata \"aws_iam_policy_document\" \"s3_bucket_policy\" {\n  statement {\n    effect = \"Allow\"\n    principals {\n      # TF-UPGRADE-TODO: In Terraform v0.10 and earlier, it was sometimes necessary to\n      # force an interpolation expression to be interpreted as a list by wrapping it\n      # in an extra set of list brackets. That form was supported for compatibility in\n      # v0.11, but is no longer supported in Terraform v0.12.\n      #\n      # If the expression in the following list itself returns a list, remove the\n      # brackets to avoid interpretation as a list of lists. If the expression\n      # returns a single list item then leave it as-is and remove this TODO comment.\n      identifiers = [local.aws_account_id]\n      type        = \"AWS\"\n    }\n    actions   = [\"*\"]\n    resources = [\"${aws_s3_bucket.test_bucket.arn}/*\"]\n  }\n\n  statement {\n    effect = \"Deny\"\n    principals {\n      identifiers = [\"*\"]\n      type        = \"AWS\"\n    }\n    actions   = [\"*\"]\n    resources = [\"${aws_s3_bucket.test_bucket.arn}/*\"]\n\n    condition {\n      test     = \"Bool\"\n      variable = \"aws:SecureTransport\"\n      values = [\n        \"false\",\n      ]\n    }\n  }\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# LOCALS\n# Used to represent any data that requires complex expressions/interpolations\n# ---------------------------------------------------------------------------------------------------------------------\n\ndata \"aws_caller_identity\" \"current\" {\n}\n\nlocals {\n  aws_account_id = data.aws_caller_identity.current.account_id\n}\n\n"
  },
  {
    "path": "examples/terraform-aws-s3-example/outputs.tf",
    "content": "output \"bucket_id\" {\n  value = aws_s3_bucket.test_bucket.id\n}\n\noutput \"bucket_arn\" {\n  value = aws_s3_bucket.test_bucket.arn\n}\n\noutput \"logging_target_bucket\" {\n  value = aws_s3_bucket_logging.test_bucket.target_bucket\n}\n\noutput \"logging_target_prefix\" {\n  value = aws_s3_bucket_logging.test_bucket.target_prefix\n}\n"
  },
  {
    "path": "examples/terraform-aws-s3-example/variables.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# ENVIRONMENT VARIABLES\n# Define these secrets as environment variables\n# ---------------------------------------------------------------------------------------------------------------------\n\n# AWS_ACCESS_KEY_ID\n# AWS_SECRET_ACCESS_KEY\n\n# ---------------------------------------------------------------------------------------------------------------------\n# REQUIRED PARAMETERS\n# You must provide a value for each of these parameters.\n# ---------------------------------------------------------------------------------------------------------------------\nvariable \"region\" {\n  description = \"The AWS region to deploy to\"\n  type        = string\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# OPTIONAL PARAMETERS\n# These parameters have reasonable defaults.\n# ---------------------------------------------------------------------------------------------------------------------\n\nvariable \"with_policy\" {\n  description = \"If set to `true`, the bucket will be created with a bucket policy.\"\n  type        = bool\n  default     = false\n}\n\nvariable \"tag_bucket_name\" {\n  description = \"The Name tag to set for the S3 Bucket.\"\n  type        = string\n  default     = \"Test Bucket\"\n}\n\nvariable \"tag_bucket_environment\" {\n  description = \"The Environment tag to set for the S3 Bucket.\"\n  type        = string\n  default     = \"Test\"\n}\n\n"
  },
  {
    "path": "examples/terraform-aws-ssm-example/README.md",
    "content": "# Terraform AWS SSM Example\n\nThis folder contains a simple Terraform module that deploys an instance in [AWS](https://aws.amazon.com/)\nand registers it in the AWS SSM Catalog to demonstrate how you can use Terratest to write automated tests for your AWS Terraform code.\n\nCheck out [test/terraform_aws_ssm_example_test.go](/test/terraform_aws_ssm_example_test.go) to see how\nyou can write automated tests for this module.\n\n**WARNING**: This module and the automated tests for it deploy real resources into your AWS account which can cost you\nmoney. The resources are all part of the [AWS Free Tier](https://aws.amazon.com/free/), so if you haven't used that up,\nit should be free, but you are completely responsible for all AWS charges.\n\n\n\n## Running this module manually\n\n1. Sign up for [AWS](https://aws.amazon.com/).\n1. Configure your AWS credentials using one of the [supported methods for AWS CLI\n   tools](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html), such as setting the\n   `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` environment variables. If you're using the `~/.aws/config` file for profiles then export `AWS_SDK_LOAD_CONFIG` as \"True\".\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`.\n1. Run `terraform init`.\n1. Run `terraform apply`.\n1. When you're done, run `terraform destroy`.\n\n\n\n\n## Running automated tests against this module\n\n1. Sign up for [AWS](https://aws.amazon.com/).\n1. Configure your AWS credentials using one of the [supported methods for AWS CLI\n   tools](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html), such as setting the\n   `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` environment variables. If you're using the `~/.aws/config` file for profiles then export `AWS_SDK_LOAD_CONFIG` as \"True\".\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`.\n1. Install [Golang](https://golang.org/) and make sure this code is checked out into your `GOPATH`.\n1. `cd test`\n1. `go test -v -run TestTerraformAwsSsmExample`\n"
  },
  {
    "path": "examples/terraform-aws-ssm-example/main.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# PIN TERRAFORM VERSION TO >= 0.12\n# The examples have been upgraded to 0.12 syntax\n# ---------------------------------------------------------------------------------------------------------------------\n\nterraform {\n  # This module is now only being tested with Terraform 0.13.x. However, to make upgrading easier, we are setting\n  # 0.12.26 as the minimum version, as that version added support for required_providers with source URLs, making it\n  # forwards compatible with 0.13.x code.\n  required_version = \">= 0.12.26\"\n}\n\nprovider \"aws\" {\n  region = var.region\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY AN INSTANCE WITH SSM SUPPORT\n# ---------------------------------------------------------------------------------------------------------------------\n\ndata \"aws_iam_policy_document\" \"example\" {\n  version = \"2012-10-17\"\n\n  statement {\n    sid = \"1\"\n\n    actions = [\n      \"sts:AssumeRole\",\n    ]\n\n    principals {\n      type        = \"Service\"\n      identifiers = [\"ec2.amazonaws.com\"]\n    }\n  }\n}\n\nresource \"aws_iam_role\" \"example\" {\n  name_prefix        = \"example\"\n  assume_role_policy = data.aws_iam_policy_document.example.json\n}\n\nresource \"aws_iam_role_policy_attachment\" \"example_ssm\" {\n  role       = aws_iam_role.example.name\n  policy_arn = \"arn:aws:iam::aws:policy/service-role/AmazonEC2RoleforSSM\"\n}\n\nresource \"aws_iam_instance_profile\" \"example\" {\n  name_prefix = \"example\"\n  role        = aws_iam_role.example.name\n}\n\ndata \"aws_ami\" \"amazon_linux_2\" {\n  most_recent = true\n  owners      = [\"amazon\"]\n\n  filter {\n    name   = \"name\"\n    values = [\"amzn2-ami-hvm*\"]\n  }\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# The instance must have a public ip to be able to contact AWS SSM\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"aws_instance\" \"example\" {\n  ami                         = data.aws_ami.amazon_linux_2.id\n  instance_type               = var.instance_type\n  associate_public_ip_address = true\n  iam_instance_profile        = aws_iam_instance_profile.example.name\n}\n"
  },
  {
    "path": "examples/terraform-aws-ssm-example/outputs.tf",
    "content": "output \"instance_id\" {\n  value = aws_instance.example.id\n}\n"
  },
  {
    "path": "examples/terraform-aws-ssm-example/variables.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# ENVIRONMENT VARIABLES\n# Define these secrets as environment variables\n# ---------------------------------------------------------------------------------------------------------------------\n\n# AWS_ACCESS_KEY_ID\n# AWS_SECRET_ACCESS_KEY\n\n# ---------------------------------------------------------------------------------------------------------------------\n# REQUIRED PARAMETERS\n# You must provide a value for each of these parameters.\n# ---------------------------------------------------------------------------------------------------------------------\n\n# ---------------------------------------------------------------------------------------------------------------------\n# OPTIONAL PARAMETERS\n# These parameters have reasonable defaults.\n# ---------------------------------------------------------------------------------------------------------------------\n\nvariable \"region\" {\n  type        = string\n  description = \"The AWS region to deploy into\"\n  default     = \"us-east-1\"\n}\n\nvariable \"instance_type\" {\n  description = \"The EC2 instance type to run.\"\n  type        = string\n  default     = \"t2.micro\"\n}"
  },
  {
    "path": "examples/terraform-backend-example/README.md",
    "content": "# Terraform Backend Example\n\nThis folder contains a simple Terraform module that demonstrates how you can use Terratest to configure a [Terraform \nBackend](https://www.terraform.io/docs/backends/) at test time. This module doesn't really do anything other than set \nup S3 as a backend, and allow Terratest to fill in that backend's configuration.\n\nCheck out [test/terraform_backend_example_test.go](/test/terraform_backend_example_test.go) to see how you can write\nautomated tests for this module.\n\n**WARNING**: This module and the automated tests for it deploy real resources into your AWS account which can cost you\nmoney. The resources are all part of the [AWS Free Tier](https://aws.amazon.com/free/), so if you haven't used that up,\nit should be free, but you are completely responsible for all AWS charges.\n\n\n\n\n\n## Running this module manually\n\n1. Sign up for [AWS](https://aws.amazon.com/).\n1. Configure your AWS credentials using one of the [supported methods for AWS CLI\n   tools](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html), such as setting the\n   `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` environment variables. If you're using the `~/.aws/config` file for profiles then export `AWS_SDK_LOAD_CONFIG` as \"True\".\n1. Set the AWS region you want to use as the environment variable `AWS_DEFAULT_REGION`.\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`.\n1. Run `terraform init`.\n1. Run `terraform apply`.\n1. When you're done, run `terraform destroy`.\n\n\n\n\n## Running automated tests against this module\n\n1. Sign up for [AWS](https://aws.amazon.com/).\n1. Configure your AWS credentials using one of the [supported methods for AWS CLI\n   tools](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html), such as setting the\n   `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` environment variables. If you're using the `~/.aws/config` file for profiles then export `AWS_SDK_LOAD_CONFIG` as \"True\".\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`.\n1. Install [Golang](https://golang.org/) and make sure this code is checked out into your `GOPATH`.\n1. `cd test`\n1. `dep ensure`\n1. `go test -v -run TestTerraformBackendExample`\n"
  },
  {
    "path": "examples/terraform-backend-example/main.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# AN EXAMPLE OF HOW TO CONFIGURE A TERRAFORM BACKEND WITH TERRATEST\n# Note that the example code here doesn't do anything other than set up a backend that Terratest will configure.\n# ---------------------------------------------------------------------------------------------------------------------\n\nterraform {\n  # Leave the config for this backend unspecified so Terraform can fill it in. This is known as \"partial configuration\":\n  # https://www.terraform.io/docs/backends/config.html#partial-configuration\n  backend \"s3\" {}\n  # This module is now only being tested with Terraform 0.13.x. However, to make upgrading easier, we are setting\n  # 0.12.26 as the minimum version, as that version added support for required_providers with source URLs, making it\n  # forwards compatible with 0.13.x code.\n  required_version = \">= 0.12.26\"\n}\n\nvariable \"foo\" {\n  description = \"Some data to store as an output of this module\"\n  type        = string\n}\n\noutput \"foo\" {\n  value = var.foo\n}\n"
  },
  {
    "path": "examples/terraform-basic-example/README.md",
    "content": "# Terraform Basic Example\n\nThis folder contains a very simple Terraform module to demonstrate how you can use Terratest to write automated tests\nfor your Terraform code. This module takes in an input variable called `example`, renders it using a `template_file`\ndata source, and outputs the result in an output variable called `example`.\n\nCheck out [test/terraform_basic_example_test.go](/test/terraform_basic_example_test.go) to see how you can write\nautomated tests for this simple module.\n\nNote that this module doesn't do anything useful; it's just here to demonstrate the simplest usage pattern for\nTerratest. For a slightly more complicated, real-world example of a Terraform module and the corresponding tests, see\n[terraform-aws-example](/examples/terraform-aws-example).\n\n\n\n\n## Running this module manually\n\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`.\n1. Run `terraform init`.\n1. Run `terraform apply`.\n1. When you're done, run `terraform destroy`.\n\n\n\n\n## Running automated tests against this module\n\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`.\n1. Install [Golang](https://golang.org/) and make sure this code is checked out into your `GOPATH`.\n1. `cd test`\n1. `dep ensure`\n1. `go test -v -run TestTerraformBasicExample`"
  },
  {
    "path": "examples/terraform-basic-example/main.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# PIN TERRAFORM VERSION TO >= 0.12\n# The examples have been upgraded to 0.12 syntax\n# ---------------------------------------------------------------------------------------------------------------------\n\nterraform {\n  # This module is now only being tested with Terraform 0.13.x. However, to make upgrading easier, we are setting\n  # 0.12.26 as the minimum version, as that version added support for required_providers with source URLs, making it\n  # forwards compatible with 0.13.x code.\n  required_version = \">= 0.12.26\"\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# BASIC TERRAFORM EXAMPLE\n# See test/terraform_aws_example.go for how to write automated tests for this code.\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"local_file\" \"example\" {\n  content  = \"${var.example} + ${var.example2}\"\n  filename = \"example.txt\"\n}\n\nresource \"local_file\" \"example2\" {\n  content  = var.example2\n  filename = \"example2.txt\"\n}\n\n"
  },
  {
    "path": "examples/terraform-basic-example/outputs.tf",
    "content": "output \"example\" {\n  value = var.example\n}\n\noutput \"example2\" {\n  value = var.example2\n}\n\noutput \"example_list\" {\n  value = var.example_list\n}\n\noutput \"example_map\" {\n  value = var.example_map\n}\n\noutput \"example_any\" {\n  value = var.example_any\n}\n"
  },
  {
    "path": "examples/terraform-basic-example/varfile.tfvars",
    "content": "example2 = \"test\"\n"
  },
  {
    "path": "examples/terraform-basic-example/variables.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# ENVIRONMENT VARIABLES\n# Define these secrets as environment variables\n# ---------------------------------------------------------------------------------------------------------------------\n\n# AWS_ACCESS_KEY_ID\n# AWS_SECRET_ACCESS_KEY\n\n# ---------------------------------------------------------------------------------------------------------------------\n# REQUIRED PARAMETERS\n# You must provide a value for each of these parameters.\n# ---------------------------------------------------------------------------------------------------------------------\n\n# ---------------------------------------------------------------------------------------------------------------------\n# OPTIONAL PARAMETERS\n# These parameters have reasonable defaults.\n# ---------------------------------------------------------------------------------------------------------------------\n\nvariable \"example\" {\n  description = \"Example variable\"\n  type        = string\n  default     = \"example\"\n}\n\nvariable \"example2\" {\n  description = \"Example variable 2\"\n  type        = string\n  default     = \"\"\n}\n\nvariable \"example_list\" {\n  description = \"An example variable that is a list.\"\n  type        = list(string)\n  default     = []\n}\n\nvariable \"example_map\" {\n  description = \"An example variable that is a map.\"\n  type        = map(string)\n  default     = {}\n}\n\nvariable \"example_any\" {\n  description = \"An example variable that is can be anything\"\n  type        = any\n  default     = null\n}\n"
  },
  {
    "path": "examples/terraform-database-example/REAME.md",
    "content": "# Terraform Database Example\n\nThis example demonstrates how to create a PostgreSQL instance using Docker with Terraform.\n\nCheck out [test/terraform_database_example_test.go](/test/terraform_database_example_test.go) to learn how to write\nautomated tests for a database. To run the Go test code, you need to provide the host, port, username, password, and\ndatabase name of an existing database, which you should have already created on a cloud platform or using Docker before\nrunning the tests. Currently, only Microsoft SQL Server, PostgreSQL, and MySQL are supported.\n\n## Running this module manually\n\n1. Install Terraform and ensure it's available in your `PATH`.\n1. Run `terraform init`.\n1. Run `terraform apply`.\n1. When finished, run `terraform destroy` to remove the resources.\n\n## Running automated tests against this module\n\n1. Install [Terraform](https://www.terraform.io/) and ensure it's available in your `PATH`.\n1. Install [Golang](https://golang.org/) and ensure this code is checked out into your `GOPATH`.\n1. Run `go mod tidy` to manage dependencies.\n1. Run `go test -v test/terraform_database_example_test.go` to execute the tests."
  },
  {
    "path": "examples/terraform-database-example/main.tf",
    "content": "terraform {\n  required_providers {\n    docker = {\n      source  = \"kreuzwerker/docker\"\n      version = \"~> 3.0\"\n    }\n  }\n\n  required_version = \">= 1.3.0\"\n}\n\nprovider \"docker\" {\n}\n\nresource \"docker_network\" \"postgres_network\" {\n  name = \"postgres_network\"\n}\n\nresource \"docker_volume\" \"postgres_volume\" {\n  name = \"postgres_data\"\n}\n\nresource \"docker_container\" \"postgres\" {\n  name  = \"postgres\"\n  image = \"postgres:15\"\n\n  env = [\n    \"POSTGRES_USER=${var.username}\",\n    \"POSTGRES_PASSWORD=${var.password}\",\n    \"POSTGRES_DB=${var.database_name}\"\n  ]\n\n  ports {\n    internal = 5432\n    external = var.port\n  }\n\n  networks_advanced {\n    name = docker_network.postgres_network.name\n  }\n\n  restart = \"always\"\n}\n"
  },
  {
    "path": "examples/terraform-database-example/outputs.tf",
    "content": "output \"host\" {\n  value = var.host\n}\n\noutput \"port\" {\n  value = var.port\n}\n\noutput \"username\" {\n  value = var.username\n}\n\noutput \"password\" {\n  value = var.password\n}\n\noutput \"database_name\" {\n  value = var.database_name\n}\n"
  },
  {
    "path": "examples/terraform-database-example/variables.tf",
    "content": "variable \"host\" {\n  default = \"localhost\"\n}\n\nvariable \"port\" {\n  default = \"32768\"\n}\n\nvariable \"username\" {\n  default = \"docker\"\n}\n\nvariable \"password\" {\n  default = \"docker\"\n}\n\nvariable \"database_name\" {\n  default = \"docker\"\n}\n"
  },
  {
    "path": "examples/terraform-gcp-example/README.md",
    "content": "# Terraform GCP Example\n\nThis folder contains a simple Terraform module that deploys resources in [GCP](https://cloud.google.com/) to demonstrate\nhow you can use Terratest to write automated tests for your GCP Terraform code. This module deploys a [Compute\nInstance](https://cloud.google.com/compute/) and gives that Instance a `Name` with the value specified in the\n`instance_name` variable. It also creates a Cloud Storage Bucket using the `bucket_name` and `bucket_location` variables.\n\nCheck out [test/terraform_gcp_example_test.go](/test/gcp/terraform_gcp_example_test.go) to see how you can write\nautomated tests for this module.\n\nNote that the Compute Instance in this module doesn't actually do anything; it just runs a Vanilla Ubuntu 16.04 Image for\ndemonstration purposes. For slightly more complicated, real-world examples of Terraform modules, see\n[terraform-http-example](/examples/terraform-http-example) and [terraform-ssh-example](/examples/terraform-ssh-example).\n\n**WARNING**: This module and the automated tests for it deploy real resources into your GCP account which can cost you\nmoney. The resources are all part of the [GCP Free Tier](https://cloud.google.com/free/), so if you haven't used that up,\nit should be free, but you are completely responsible for all GCP charges.\n\n## Running this module manually\n\n1. Sign up for [GCP](https://cloud.google.com/).\n1. Configure your GCP credentials using one of the [supported methods for GCP CLI\n   tools](https://cloud.google.com/sdk/docs/quickstarts).\n1. Install [Terraform](https://www.terraform.io/) and make sure it's in your `PATH`.\n1. Ensure the desired Project ID is set: `export GOOGLE_CLOUD_PROJECT=terratest-ABCXYZ`.\n1. Run `terraform init`.\n1. Run `terraform apply`.\n1. When you're done, run `terraform destroy`.\n\n## Running automated tests against this module\n\n1. Sign up for [GCP](https://cloud.google.com/free/).\n1. Configure your GCP credentials using the [GCP CLI\n   tools](https://cloud.google.com/sdk/docs/quickstarts).\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`.\n1. Install [Golang](https://golang.org/) and make sure this code is checked out into your `GOPATH`.\n1. Set `GOOGLE_CLOUD_PROJECT` environment variable to your project name.\n1. `cd test`\n1. `dep ensure`\n1. `go test -v -run TestTerraformGcpExample`\n"
  },
  {
    "path": "examples/terraform-gcp-example/main.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# PIN TERRAFORM VERSION TO >= 0.12\n# The examples have been upgraded to 0.12 syntax\n# ---------------------------------------------------------------------------------------------------------------------\n\nterraform {\n  # This module is now only being tested with Terraform 0.13.x. However, to make upgrading easier, we are setting\n  # 0.12.26 as the minimum version, as that version added support for required_providers with source URLs, making it\n  # forwards compatible with 0.13.x code.\n  required_version = \">= 0.12.26\"\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY A CLOUD INSTANCE RUNNING UBUNTU\n# See test/terraform_gcp_example_test.go for how to write automated tests for this code.\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"google_compute_instance\" \"example\" {\n  project      = var.gcp_project_id\n  name         = var.instance_name\n  machine_type = var.machine_type\n  zone         = var.zone\n\n  boot_disk {\n    initialize_params {\n      image = \"ubuntu-os-cloud/ubuntu-2204-lts\"\n    }\n  }\n\n  network_interface {\n    network = \"default\"\n    access_config {\n    }\n  }\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# CREATE A GOOGLE STORAGE BUCKET\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"google_storage_bucket\" \"example_bucket\" {\n  project  = var.gcp_project_id\n  name     = var.bucket_name\n  location = var.bucket_location\n}\n\n"
  },
  {
    "path": "examples/terraform-gcp-example/outputs.tf",
    "content": "output \"instance_name\" {\n  value = google_compute_instance.example.name\n}\n\noutput \"public_ip\" {\n  value = google_compute_instance.example.network_interface[0].access_config[0].nat_ip\n}\n\noutput \"bucket_url\" {\n  value = google_storage_bucket.example_bucket.url\n}\n\n"
  },
  {
    "path": "examples/terraform-gcp-example/variables.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# ENVIRONMENT VARIABLES\n# You must define the following environment variables.\n# ---------------------------------------------------------------------------------------------------------------------\n\n# GOOGLE_CREDENTIALS\n# or\n# GOOGLE_APPLICATION_CREDENTIALS\n\nvariable \"gcp_project_id\" {\n  description = \"The ID of the GCP project in which these resources will be created.\"\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# REQUIRED PARAMETERS\n# You must provide a value for each of these parameters.\n# ---------------------------------------------------------------------------------------------------------------------\n\n# ---------------------------------------------------------------------------------------------------------------------\n# OPTIONAL PARAMETERS\n# These parameters have reasonable defaults.\n# ---------------------------------------------------------------------------------------------------------------------\n\nvariable \"instance_name\" {\n  description = \"The Name to use for the Cloud Instance.\"\n  type        = string\n  default     = \"terratest-example\"\n}\n\nvariable \"machine_type\" {\n  description = \"The Machine Type to use for the Cloud Instance.\"\n  type        = string\n  default     = \"f1-micro\"\n}\n\nvariable \"zone\" {\n  description = \"The Zone to launch the Cloud Instance into.\"\n  type        = string\n  default     = \"us-central1-a\"\n}\n\nvariable \"bucket_name\" {\n  description = \"The Name of the example Bucket to create.\"\n  type        = string\n  default     = \"gruntwork-terratest-bucket\"\n}\n\nvariable \"bucket_location\" {\n  description = \"The location to store the Bucket. This value can be regional or multi-regional.\"\n  type        = string\n  default     = \"US\"\n}\n\n"
  },
  {
    "path": "examples/terraform-gcp-hello-world-example/README.md",
    "content": "# Terraform GCP \"Hello, World\" Example\n\nThis folder contains a simple Terraform module that deploys resources in [GCP](https://cloud.google.com/) to demonstrate\nhow you can use Terratest to write automated tests for your GCP Terraform code. This module deploys a [Compute\nInstance](https://cloud.google.com/compute/) and gives that Instance a `Name` with the value specified in the\n`instance_name` variable.\n\nCheck out [test/terraform_gcp_hello_world_example_test.go](/test/gcp/terraform_gcp_hello_world_example_test.go) to see how \nyou can write automated tests for this module.\n\nNote that the Compute Instance in this module doesn't actually do anything; it just runs a Vanilla Ubuntu 18.04 Image for\ndemonstration purposes. For slightly more complicated, real-world examples of Terraform modules, see\n[terraform-http-example](/examples/terraform-http-example) and [terraform-ssh-example](/examples/terraform-ssh-example).\n\n**WARNING**: This module and the automated tests for it deploy real resources into your GCP account which can cost you\nmoney. The resources are all part of the [GCP Free Tier](https://cloud.google.com/free/), so if you haven't used that up,\nit should be free, but you are completely responsible for all GCP charges.\n\n\n\n\n## Running this module manually\n\n1. Sign up for [GCP](https://cloud.google.com/).\n1. Configure your GCP credentials using one of the [supported methods for GCP CLI\n   tools](https://cloud.google.com/sdk/docs/quickstarts).\n1. Install [Terraform](https://www.terraform.io/) and make sure it's in your `PATH`.\n1. Ensure the desired Project ID is set: `export GOOGLE_CLOUD_PROJECT=terratest-ABCXYZ`.\n1. Run `terraform init`.\n1. Run `terraform apply`.\n1. When you're done, run `terraform destroy`.\n\n\n\n\n## Running automated tests against this module\n\n1. Sign up for [GCP](https://cloud.google.com/free/).\n1. Configure your GCP credentials using the [GCP CLI\n   tools](https://cloud.google.com/sdk/docs/quickstarts).\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`.\n1. Install [Golang](https://golang.org/) and make sure this code is checked out into your `GOPATH`.\n1. Set `GOOGLE_CLOUD_PROJECT` environment variable to your project name.\n1. `cd test`\n1. `go test -v -run TestTerraformGcpHelloWorldExample`\n"
  },
  {
    "path": "examples/terraform-gcp-hello-world-example/main.tf",
    "content": "terraform {\n  # This module is now only being tested with Terraform 0.13.x. However, to make upgrading easier, we are setting\n  # 0.12.26 as the minimum version, as that version added support for required_providers with source URLs, making it\n  # forwards compatible with 0.13.x code.\n  required_version = \">= 0.12.26\"\n}\n\nprovider \"google\" {\n  region = \"us-east1\"\n}\n\n# website::tag::1:: Deploy a cloud instance\nresource \"google_compute_instance\" \"example\" {\n  name         = var.instance_name\n  machine_type = \"f1-micro\"\n  zone         = \"us-east1-b\"\n\n  # website::tag::2:: Run Ubuntu 22.04 on the instance\n  boot_disk {\n    initialize_params {\n      image = \"ubuntu-os-cloud/ubuntu-2204-lts\"\n    }\n  }\n\n  network_interface {\n    network = \"default\"\n    access_config {}\n  }\n}\n\n# website::tag::3:: Allow the user to pass in a custom name for the instance\nvariable \"instance_name\" {\n  description = \"The Name to use for the Cloud Instance.\"\n  default     = \"gcp-hello-world-example\"\n}\n"
  },
  {
    "path": "examples/terraform-gcp-ig-example/README.md",
    "content": "# Terraform GCP Managed Instance Group Example\n\nThis folder contains a simple Terraform configuration that deploys resources in [GCP](https://cloud.google.com/) to demonstrate\nhow you can use Terratest to write automated tests for your GCP Terraform code. This module deploys an [Instance Group](\nhttps://cloud.google.com/compute/docs/instance-groups/).\n\nCheck out [test/terraform_gcp_ig_example_test.go](/test/gcp/terraform_gcp_ig_example_test.go) to see how you can write\nautomated tests for this module.\n\nNote that the Instance Group in this module doesn't actually do anything; it just runs a cluster of vanilla Ubuntu 16.04\nImages for demonstration purposes. For slightly more complicated, real-world examples of Terraform modules, see\n[terraform-http-example](/examples/terraform-http-example) and [terraform-ssh-example](/examples/terraform-ssh-example).\n\n**WARNING**: This module and the automated tests for it deploy real resources into your GCP account which can cost you\nmoney. By launching multiple Instances as part of an Instance Group, these resources may go beyond the [GCP Free Tier](\nhttps://cloud.google.com/free/). Naturally, you are completely responsible for all GCP charges.\n\n## Running this module manually\n\n1. Sign up for [GCP](https://cloud.google.com/).\n1. Configure your GCP credentials using one of the [supported methods for GCP CLI\n   tools](https://cloud.google.com/sdk/docs/quickstarts).\n1. Install [Terraform](https://www.terraform.io/) and make sure it's in your `PATH`.\n1. Ensure the desired Project ID is set: `export GOOGLE_CLOUD_PROJECT=terratest-ABCXYZ`.\n1. Run `terraform init`.\n1. Run `terraform apply`.\n1. When you're done, run `terraform destroy`.\n\n## Running automated tests against this module\n\n1. Sign up for [GCP](https://cloud.google.com/free/).\n1. Configure your GCP credentials using the [GCP CLI\n   tools](https://cloud.google.com/sdk/docs/quickstarts).\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`.\n1. Install [Golang](https://golang.org/) and make sure this code is checked out into your `GOPATH`.\n1. Set `GOOGLE_CLOUD_PROJECT` environment variable to your project name.\n1. `cd test`\n1. `dep ensure`\n1. `go test -v -run TestTerraformGcpInstanceGroupExample`\n"
  },
  {
    "path": "examples/terraform-gcp-ig-example/main.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# PIN TERRAFORM VERSION TO >= 0.12\n# The examples have been upgraded to 0.12 syntax\n# ---------------------------------------------------------------------------------------------------------------------\n\nterraform {\n  # This module is now only being tested with Terraform 0.13.x. However, to make upgrading easier, we are setting\n  # 0.12.26 as the minimum version, as that version added support for required_providers with source URLs, making it\n  # forwards compatible with 0.13.x code.\n  required_version = \">= 0.12.26\"\n}\n\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY A REGIONAL MANAGED INSTANCE GROUP\n# See test/terraform_gcp_ig_example_test.go for how to write automated tests for this code.\n# ---------------------------------------------------------------------------------------------------------------------\n\n# Create a Regional Managed Instance Group\nresource \"google_compute_region_instance_group_manager\" \"example\" {\n  project = var.gcp_project_id\n  region  = var.gcp_region\n\n  name               = \"${var.cluster_name}-ig\"\n  base_instance_name = var.cluster_name\n  version {\n    name              = \"terratest\"\n    instance_template = google_compute_instance_template.example.self_link\n  }\n\n  target_size = var.cluster_size\n}\n\n# Create the Instance Template that will be used to populate the Managed Instance Group.\nresource \"google_compute_instance_template\" \"example\" {\n  project = var.gcp_project_id\n\n  name_prefix  = var.cluster_name\n  machine_type = var.machine_type\n\n  scheduling {\n    automatic_restart   = true\n    on_host_maintenance = \"MIGRATE\"\n    preemptible         = false\n  }\n\n  disk {\n    boot         = true\n    auto_delete  = true\n    source_image = \"ubuntu-os-cloud/ubuntu-2204-lts\"\n  }\n\n  network_interface {\n    network = \"default\"\n\n    # The presence of this property assigns a public IP address to each Compute Instance. We intentionally leave it\n    # blank so that an external IP address is selected automatically.\n    access_config {\n    }\n  }\n}\n\n"
  },
  {
    "path": "examples/terraform-gcp-ig-example/outputs.tf",
    "content": "output \"instance_group_name\" {\n  value = google_compute_region_instance_group_manager.example.name\n}\n\n"
  },
  {
    "path": "examples/terraform-gcp-ig-example/variables.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# ENVIRONMENT VARIABLES\n# You must define the following environment variables.\n# ---------------------------------------------------------------------------------------------------------------------\n\n# GOOGLE_CREDENTIALS\n# or\n# GOOGLE_APPLICATION_CREDENTIALS\n\n# ---------------------------------------------------------------------------------------------------------------------\n# REQUIRED PARAMETERS\n# You must provide a value for each of these parameters.\n# ---------------------------------------------------------------------------------------------------------------------\n\nvariable \"gcp_project_id\" {\n  description = \"The ID of the GCP project in which these resources will be created.\"\n  type        = string\n}\n\nvariable \"gcp_region\" {\n  description = \"The region in which all GCP resources will be created.\"\n  type        = string\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# OPTIONAL PARAMETERS\n# These parameters have reasonable defaults.\n# ---------------------------------------------------------------------------------------------------------------------\n\nvariable \"cluster_name\" {\n  description = \"The unique identifier for the resources created by this Terraform configuration.\"\n  type        = string\n  default     = \"terratest-example\"\n}\n\nvariable \"cluster_size\" {\n  description = \"The number of Compute Instances to run in the Managed Instance Group.\"\n  type        = number\n  default     = 3\n}\n\nvariable \"machine_type\" {\n  description = \"The Machine Type to use for the Compute Instances.\"\n  type        = string\n  default     = \"f1-micro\"\n}\n\n"
  },
  {
    "path": "examples/terraform-hello-world-example/README.md",
    "content": "# Terraform \"Hello, World\" Example\n\nThis folder contains the simplest possible Terraform module—one that just outputs \"Hello, World\"—to demonstrate how you \ncan use Terratest to write automated tests for your Terraform code. \n\nCheck out [test/terraform_hello_world_example_test.go](/test/terraform_hello_world_example_test.go) to see how you can \nwrite automated tests for this simple module.\n\nNote that this module doesn't do anything useful; it's just here to demonstrate the simplest usage pattern for\nTerratest. For a slightly more complicated example of a Terraform module and the corresponding tests, see\n[terraform-basic-example](/examples/terraform-basic-example).\n\n\n\n\n## Running this module manually\n\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`.\n1. Run `terraform init`.\n1. Run `terraform apply`.\n1. When you're done, run `terraform destroy`.\n\n\n\n\n## Running automated tests against this module\n\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`.\n1. Install [Golang](https://golang.org/) and make sure this code is checked out into your `GOPATH`.\n1. `cd test`\n1. `go test -v -run TestTerraformHelloWorldExample`"
  },
  {
    "path": "examples/terraform-hello-world-example/main.tf",
    "content": "terraform {\n  # This module is now only being tested with Terraform 0.13.x. However, to make upgrading easier, we are setting\n  # 0.12.26 as the minimum version, as that version added support for required_providers with source URLs, making it\n  # forwards compatible with 0.13.x code.\n  required_version = \">= 0.12.26\"\n}\n\n# website::tag::1:: The simplest possible Terraform module: it just outputs \"Hello, World!\"\noutput \"hello_world\" {\n  value = \"Hello, World!\"\n}"
  },
  {
    "path": "examples/terraform-http-example/README.md",
    "content": "# Terraform HTTP Example\n\nThis folder contains a simple Terraform module that deploys resources in [AWS](https://aws.amazon.com/) to demonstrate\nhow you can use Terratest to write automated tests for your AWS Terraform code. This module deploys an [EC2\nInstance](https://aws.amazon.com/ec2/) in the AWS region specified in the `aws_region` variable. The EC2 Instance runs\na simple web server that listens for HTTP requests on the port specified by the `instance_port` variable and returns\nthe text specified by the `instance_text` variable.\n\nCheck out [test/terraform_http_example_test.go](/test/terraform_http_example_test.go) to see how you can write\nautomated tests for this module.\n\nNote that the example in this module is still fairly simplified, as the \"web server\" we run just servers up a static\n`index.html`, and not in a particularly production-ready manner! For a more complicated, real-world, end-to-end\nexample of a Terraform module and web server, see [terraform-packer-example](/examples/terraform-packer-example).\n\n**WARNING**: This module and the automated tests for it deploy real resources into your AWS account which can cost you\nmoney. The resources are all part of the [AWS Free Tier](https://aws.amazon.com/free/), so if you haven't used that up,\nit should be free, but you are completely responsible for all AWS charges.\n\n\n\n\n\n## Running this module manually\n\n1. Sign up for [AWS](https://aws.amazon.com/).\n1. Configure your AWS credentials using one of the [supported methods for AWS CLI\n   tools](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html), such as setting the\n   `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` environment variables. If you're using the `~/.aws/config` file for profiles then export `AWS_SDK_LOAD_CONFIG` as \"True\".\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`.\n1. Run `terraform init`.\n1. Run `terraform apply`.\n1. The `instance_url` output variable shows you the URL of the web server. Try opening it in your browser!\n1. When you're done, run `terraform destroy`.\n\n\n\n\n## Running automated tests against this module\n\n1. Sign up for [AWS](https://aws.amazon.com/).\n1. Configure your AWS credentials using one of the [supported methods for AWS CLI\n   tools](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html), such as setting the\n   `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` environment variables. If you're using the `~/.aws/config` file for profiles then export `AWS_SDK_LOAD_CONFIG` as \"True\".\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`.\n1. Install [Golang](https://golang.org/) and make sure this code is checked out into your `GOPATH`.\n1. `cd test`\n1. `dep ensure`\n1. `go test -v -run TestTerraformHttpExample`\n"
  },
  {
    "path": "examples/terraform-http-example/main.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# PIN TERRAFORM VERSION TO >= 0.12\n# The examples have been upgraded to 0.12 syntax\n# ---------------------------------------------------------------------------------------------------------------------\n\nterraform {\n  # This module is now only being tested with Terraform 0.13.x. However, to make upgrading easier, we are setting\n  # 0.12.26 as the minimum version, as that version added support for required_providers with source URLs, making it\n  # forwards compatible with 0.13.x code.\n  required_version = \">= 0.12.26\"\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY AN EC2 INSTANCE THAT RUNS A SIMPLE \"HELLO, WORLD\" WEB SERVER\n# See test/terraform_http_example.go for how to write automated tests for this code.\n# ---------------------------------------------------------------------------------------------------------------------\n\nprovider \"aws\" {\n  region = var.aws_region\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY THE EC2 INSTANCE\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"aws_instance\" \"example\" {\n  ami                    = data.aws_ami.ubuntu.id\n  instance_type          = var.instance_type\n  user_data              = data.template_file.user_data.rendered\n  vpc_security_group_ids = [aws_security_group.example.id]\n\n  tags = {\n    Name = var.instance_name\n  }\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# CREATE A SECURITY GROUP TO CONTROL WHAT REQUESTS CAN GO IN AND OUT OF THE EC2 INSTANCE\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"aws_security_group\" \"example\" {\n  name = var.instance_name\n\n  ingress {\n    from_port = var.instance_port\n    to_port   = var.instance_port\n    protocol  = \"tcp\"\n\n    # To keep this example simple, we allow incoming HTTP requests from any IP. In real-world usage, you may want to\n    # lock this down to just the IPs of trusted servers (e.g., of a load balancer).\n    cidr_blocks = [\"0.0.0.0/0\"]\n  }\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# CREATE THE USER DATA SCRIPT THAT WILL RUN DURING BOOT ON THE EC2 INSTANCE\n# ---------------------------------------------------------------------------------------------------------------------\n\ndata \"template_file\" \"user_data\" {\n  template = file(\"${path.module}/user-data/user-data.sh\")\n\n  vars = {\n    instance_text = var.instance_text\n    instance_port = var.instance_port\n  }\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# LOOK UP THE LATEST UBUNTU AMI\n# ---------------------------------------------------------------------------------------------------------------------\n\ndata \"aws_ami\" \"ubuntu\" {\n  most_recent = true\n  owners      = [\"099720109477\"] # Canonical\n\n  filter {\n    name   = \"virtualization-type\"\n    values = [\"hvm\"]\n  }\n\n  filter {\n    name   = \"architecture\"\n    values = [\"x86_64\"]\n  }\n\n  filter {\n    name   = \"image-type\"\n    values = [\"machine\"]\n  }\n\n  filter {\n    name   = \"name\"\n    values = [\"ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*\"]\n  }\n}\n\n"
  },
  {
    "path": "examples/terraform-http-example/outputs.tf",
    "content": "output \"instance_id\" {\n  value = aws_instance.example.id\n}\n\noutput \"public_ip\" {\n  value = aws_instance.example.public_ip\n}\n\noutput \"instance_url\" {\n  value = \"http://${aws_instance.example.public_ip}:${var.instance_port}\"\n}\n\n"
  },
  {
    "path": "examples/terraform-http-example/user-data/user-data.sh",
    "content": "#!/bin/bash\n# This script is meant to be run in the User Data of an EC2 Instance while it's booting. It starts a simple\n# \"Hello, World\" web server.\n\nset -e\n\n# Send the log output from this script to user-data.log, syslog, and the console\n# From: https://alestic.com/2010/12/ec2-user-data-output/\nexec > >(tee /var/log/user-data.log|logger -t user-data -s 2>/dev/console) 2>&1\n\n# The variables below are filled in using Terraform interpolation\necho \"${instance_text}\" > index.html\nnohup busybox httpd -f -p \"${instance_port}\" &"
  },
  {
    "path": "examples/terraform-http-example/variables.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# ENVIRONMENT VARIABLES\n# Define these secrets as environment variables\n# ---------------------------------------------------------------------------------------------------------------------\n\n# AWS_ACCESS_KEY_ID\n# AWS_SECRET_ACCESS_KEY\n\n# ---------------------------------------------------------------------------------------------------------------------\n# REQUIRED PARAMETERS\n# You must provide a value for each of these parameters.\n# ---------------------------------------------------------------------------------------------------------------------\n\n# ---------------------------------------------------------------------------------------------------------------------\n# OPTIONAL PARAMETERS\n# These parameters have reasonable defaults.\n# ---------------------------------------------------------------------------------------------------------------------\n\nvariable \"aws_region\" {\n  description = \"The AWS region to deploy into\"\n  type        = string\n  default     = \"us-east-1\"\n}\n\nvariable \"instance_name\" {\n  description = \"The Name tag to set for the EC2 Instance.\"\n  type        = string\n  default     = \"terratest-example\"\n}\n\nvariable \"instance_port\" {\n  description = \"The port the EC2 Instance should listen on for HTTP requests.\"\n  type        = number\n  default     = 8080\n}\n\nvariable \"instance_text\" {\n  description = \"The text the EC2 Instance should return when it gets an HTTP request.\"\n  type        = string\n  default     = \"Hello, World!\"\n}\n\nvariable \"instance_type\" {\n  description = \"The EC2 instance type to run.\"\n  type        = string\n  default     = \"t2.micro\"\n}\n"
  },
  {
    "path": "examples/terraform-opa-example/README.md",
    "content": "# Terraform OPA Example\n\nThis folder contains an [OPA](https://www.openpolicyagent.org/) policy that validates that all module blocks use a\nsource that comes from the `gruntwork-io` GitHub org (the [enforce_source.rego](./policy/enforce_source.rego) file).\nTo test this policy, we provided two Terraform modules, [pass](./pass) and [fail](./fail), which will demonstrate how\nOPA looks when run against a module that passes the checks, and one that fails the checks.\n\nCheck out [test/terraform_opa_example_test.go](/test/terraform_opa_example_test.go) to see how you can write automated\ntests for this module.\n\n\n## Running this module manually\n\n1. Install [OPA](https://www.openpolicyagent.org/) and make sure it's on your `PATH`.\n1. Install [hcl2json](https://github.com/tmccombs/hcl2json) and make sure it's on your `PATH`. We need this to convert\n   the terraform source code to json as OPA currently doesn't support parsing HCL.\n1. Convert each terraform source code in the `pass` or `fail` folder to json by feeding it to `hcl2json`:\n\n       hcl2json pass/main.tf > pass/main.json\n\n1. Run each converted terraform json file against the OPA policy:\n\n       opa eval --fail \\\n         -i pass/main.json \\\n         -d policy/enforce_source.rego \\\n         'data.enforce_source.allow'\n\n\n## Running automated tests against this module\n\n1. Install [OPA](https://www.openpolicyagent.org/) and make sure it's on your `PATH`.\n1. Install [Golang](https://golang.org/).\n1. `cd test`\n1. `go test -v -run TestOPAEvalTerraformModule`\n\n## Using extra command line arguments\n\nIf you need to pass additional command line arguments to OPA eval, you can use the `ExtraArgs` field. These arguments are placed after the `eval` subcommand and before the standard arguments:\n\n```go\n// For OPA eval flags (e.g., --v0-compatible for OPA v0.x compatibility)\nopaOpts := &opa.EvalOptions{\n    RulePath: \"../examples/terraform-opa-example/policy/enforce_source.rego\",\n    FailMode: opa.FailUndefined,\n    ExtraArgs: []string{\"--v0-compatible\"},\n}\n\n// For multiple eval subcommand flags\nopaOpts := &opa.EvalOptions{\n    RulePath: \"../examples/terraform-opa-example/policy/enforce_source.rego\",\n    FailMode: opa.FailUndefined,\n    ExtraArgs: []string{\"--v0-compatible\", \"--format\", \"json\"},\n}\n```\n"
  },
  {
    "path": "examples/terraform-opa-example/fail/main_fail.tf",
    "content": "module \"instance_types\" {\n  # website::tag::1:: We expect this to fail the OPA check since it is sourcing the module locally and not from gruntwork-io GitHub.\n  source     = \"../pass\"\n  aws_region = var.aws_region\n}\n"
  },
  {
    "path": "examples/terraform-opa-example/fail/output.tf",
    "content": "output \"recommended_instance_type\" {\n  description = \"The recommended instance type to use in this AWS region. This will be the first instance type in var.instance_types which is available in all AZs in this region.\"\n  value       = module.instance_types.recommended_instance_type\n}\n"
  },
  {
    "path": "examples/terraform-opa-example/fail/variables.tf",
    "content": "variable \"aws_region\" {\n  description = \"Region to run the instance type checks on\"\n  type        = string\n}\n"
  },
  {
    "path": "examples/terraform-opa-example/pass/main_pass.tf",
    "content": "provider \"aws\" {\n  region = var.aws_region\n}\n\nmodule \"instance_types\" {\n  # website::tag::1:: We expect this to pass the OPA check since it is sourcing the module from gruntwork-io GitHub.\n  source         = \"git::git@github.com:gruntwork-io/terraform-aws-utilities.git//modules/instance-type?ref=v0.6.0\"\n  instance_types = [\"t2.micro\", \"t3.micro\"]\n}\n"
  },
  {
    "path": "examples/terraform-opa-example/pass/output.tf",
    "content": "output \"recommended_instance_type\" {\n  description = \"The recommended instance type to use in this AWS region. This will be the first instance type in var.instance_types which is available in all AZs in this region.\"\n  value       = module.instance_types.recommended_instance_type\n}\n"
  },
  {
    "path": "examples/terraform-opa-example/pass/variables.tf",
    "content": "variable \"aws_region\" {\n  description = \"Region to run the instance type checks on\"\n  type        = string\n}\n"
  },
  {
    "path": "examples/terraform-opa-example/policy/enforce_source.rego",
    "content": "# An example rego policy of how to enforce that all module blocks in terraform json representation source the module\n# from the gruntwork-io github repo on the json representation of the terraform source files. A module block in the json\n# representation looks like the\n# following:\n#\n# {\n#   \"module\": {\n#     \"MODULE_LABEL\": [{\n#       #BLOCK_CONTENT\n#     }]\n#   }\n# }\npackage enforce_source\n\n\n# website::tag::1:: Only define the allow variable and set to true if the violation set is empty.\nallow = true if {\n    count(violation) == 0\n}\n\n# website::tag::1:: Add modules with module_label to the violation set if the source attribute does not start with a string indicating it came from gruntwork-io GitHub org.\nviolation contains module_label if {\n    some module_label, i\n    startswith(input.module[module_label][i].source, \"git::git@github.com:gruntwork-io\") == false\n}\n"
  },
  {
    "path": "examples/terraform-opa-example/policy/enforce_source_v0.rego",
    "content": "# An example rego policy of how to enforce that all module blocks in terraform json representation source the module\n# from the gruntwork-io github repo on the json representation of the terraform source files. A module block in the json\n# representation looks like the\n# following:\n#\n# {\n#   \"module\": {\n#     \"MODULE_LABEL\": [{\n#       #BLOCK_CONTENT\n#     }]\n#   }\n# }\npackage enforce_source\n\n# This version uses OPA v0.x syntax (also compatible with v1.x when using --v0-compatible flag)\n\n# website::tag::1:: Only define the allow variable and set to true if the violation set is empty.\nallow = true {\n    count(violation) == 0\n}\n\n# website::tag::1:: Add modules with module_label to the violation set if the source attribute does not start with a string indicating it came from gruntwork-io GitHub org.\nviolation[module_label] {\n    some module_label, i\n    startswith(input.module[module_label][i].source, \"git::git@github.com:gruntwork-io\") == false\n}"
  },
  {
    "path": "examples/terraform-packer-example/README.md",
    "content": "# Terraform Packer Example\n\nThis folder contains a Terraform module that deploys resources in [AWS](https://aws.amazon.com/) to demonstrate\nhow you can use Terratest to write automated tests for your AWS Terraform code. This module deploys an [EC2\nInstance](https://aws.amazon.com/ec2/) in the AWS region specified in the `aws_region` variable. The EC2 Instance runs\nthe AMI specified via the `ami_id` variable. It is assumed that this is an AMI built using the Packer template in\n[packer-docker-example](/examples/packer-docker-example), which contains a simple Ruby web app. This module will\nconfigure a User Data script to start the web app, configuring it to listen for HTTP requests on the port specified by\nthe `instance_port` variable and return the text specified by the `instance_text` variable.\n\nCheck out [test/terraform_packer_example_test.go](/test/terraform_packer_example_test.go) to see how you can write\nautomated tests for this module. Note that this is an end-to-end integration test that will build the AMI, deploy it\nusing Terraform, validate the web app is working, and then clean everything up. The test is broken down into \"stages\"\nso that, when iterating locally, you can choose to skip any of the stages by setting an environment variable. For\nexample, if you've already built the AMI and don't want to rebuild it each time you re-run the test, you can set the\nenvironment variable `SKIP_build_ami=true`.\n\n**WARNING**: This module and the automated tests for it deploy real resources into your AWS account which can cost you\nmoney. The resources are all part of the [AWS Free Tier](https://aws.amazon.com/free/), so if you haven't used that up,\nit should be free, but you are completely responsible for all AWS charges.\n\n\n\n\n\n## Running this module manually\n\n1. Sign up for [AWS](https://aws.amazon.com/).\n1. Configure your AWS credentials using one of the [supported methods for AWS CLI\n   tools](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html), such as setting the\n   `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` environment variables. If you're using the `~/.aws/config` file for profiles then export `AWS_SDK_LOAD_CONFIG` as \"True\".\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`.\n1. Install [Packer](https://www.packer.io/) and make sure it's on your `PATH`.\n1. Follow the instructions in [packer-docker-example](/examples/packer-docker-example) to build an AMI. Note down the\n   AMI ID.\n1. Open `variables.tf` and set the `ami_id` variable to the ID of the AMI you just built.\n1. Run `terraform init`.\n1. Run `terraform apply`.\n1. The `instance_url` output variable shows you the URL of the web server. Try opening it in your browser!\n1. When you're done, run `terraform destroy`.\n\n\n\n\n## Running automated tests against this module\n\n1. Sign up for [AWS](https://aws.amazon.com/).\n1. Configure your AWS credentials using one of the [supported methods for AWS CLI\n   tools](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html), such as setting the\n   `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` environment variables. If you're using the `~/.aws/config` file for profiles then export `AWS_SDK_LOAD_CONFIG` as \"True\".\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`.\n1. Install [Packer](https://www.packer.io/) and make sure it's on your `PATH`.\n1. Install [Golang](https://golang.org/) and make sure this code is checked out into your `GOPATH`.\n1. `cd test`\n1. `dep ensure`\n1. `go test -v -run TestTerraformPackerExample`\n"
  },
  {
    "path": "examples/terraform-packer-example/main.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# PIN TERRAFORM VERSION TO >= 0.12\n# The examples have been upgraded to 0.12 syntax\n# ---------------------------------------------------------------------------------------------------------------------\n\nterraform {\n  # This module is now only being tested with Terraform 0.13.x. However, to make upgrading easier, we are setting\n  # 0.12.26 as the minimum version, as that version added support for required_providers with source URLs, making it\n  # forwards compatible with 0.13.x code.\n  required_version = \">= 0.12.26\"\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY AN EC2 INSTANCE THAT RUNS A SIMPLE RUBY WEB APP BUILT USING A PACKER TEMPLATE\n# See test/terraform_packer_example.go for how to write automated tests for this code.\n# ---------------------------------------------------------------------------------------------------------------------\n\nprovider \"aws\" {\n  region = var.aws_region\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY THE EC2 INSTANCE\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"aws_instance\" \"example\" {\n  ami           = var.ami_id\n  instance_type = var.instance_type\n\n  user_data = templatefile(\"${path.module}/user-data/user-data.sh\", {\n    instance_text = var.instance_text\n    instance_port = var.instance_port\n  })\n\n  vpc_security_group_ids = [aws_security_group.example.id]\n\n  tags = {\n    Name = var.instance_name\n  }\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# CREATE A SECURITY GROUP TO CONTROL WHAT REQUESTS CAN GO IN AND OUT OF THE EC2 INSTANCE\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"aws_security_group\" \"example\" {\n  name = var.instance_name\n\n  ingress {\n    from_port = var.instance_port\n    to_port   = var.instance_port\n    protocol  = \"tcp\"\n\n    # To keep this example simple, we allow incoming HTTP requests from any IP. In real-world usage, you may want to\n    # lock this down to just the IPs of trusted servers (e.g., of a load balancer).\n    cidr_blocks = [\"0.0.0.0/0\"]\n  }\n}\n"
  },
  {
    "path": "examples/terraform-packer-example/outputs.tf",
    "content": "output \"instance_id\" {\n  value = aws_instance.example.id\n}\n\noutput \"public_ip\" {\n  value = aws_instance.example.public_ip\n}\n\noutput \"instance_url\" {\n  value = \"http://${aws_instance.example.public_ip}:${var.instance_port}\"\n}\n\n"
  },
  {
    "path": "examples/terraform-packer-example/user-data/user-data.sh",
    "content": "#!/bin/bash\n# This script is meant to be run in the User Data of an EC2 Instance while it's booting. It starts a Ruby web app.\n# This script assumes it is running in an AMI built from the Packer templates in examples/packer-docker-example\n# (either build.json or build.pkr.hcl).\n\nset -e\n\n# Send the log output from this script to user-data.log, syslog, and the console\n# From: https://alestic.com/2010/12/ec2-user-data-output/\nexec > >(tee /var/log/user-data.log|logger -t user-data -s 2>/dev/console) 2>&1\n\n# The variables below are filled in using Terraform interpolation\nnohup ruby /home/ubuntu/app.rb \"${instance_port}\" \"${instance_text}\" &\n"
  },
  {
    "path": "examples/terraform-packer-example/variables.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# ENVIRONMENT VARIABLES\n# Define these secrets as environment variables\n# ---------------------------------------------------------------------------------------------------------------------\n\n# AWS_ACCESS_KEY_ID\n# AWS_SECRET_ACCESS_KEY\n\n# ---------------------------------------------------------------------------------------------------------------------\n# REQUIRED PARAMETERS\n# You must provide a value for each of these parameters.\n# ---------------------------------------------------------------------------------------------------------------------\n\nvariable \"ami_id\" {\n  description = \"The ID of the AMI to run on each EC2 Instance. Should be an AMI built from the Packer templates in examples/packer-docker-example (build.json or build.pkr.hcl).\"\n  type        = string\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# OPTIONAL PARAMETERS\n# These parameters have reasonable defaults.\n# ---------------------------------------------------------------------------------------------------------------------\n\nvariable \"aws_region\" {\n  description = \"The AWS region to deploy into\"\n  type        = string\n  default     = \"us-east-1\"\n}\n\nvariable \"instance_name\" {\n  description = \"The Name tag to set for the EC2 Instance.\"\n  type        = string\n  default     = \"terratest-example\"\n}\n\nvariable \"instance_port\" {\n  description = \"The port the EC2 Instance should listen on for HTTP requests.\"\n  type        = number\n  default     = 8080\n}\n\nvariable \"instance_text\" {\n  description = \"The text the EC2 Instance should return when it gets an HTTP request.\"\n  type        = string\n  default     = \"Hello, World!\"\n}\n\nvariable \"instance_type\" {\n  description = \"The EC2 instance type to run.\"\n  type        = string\n  default     = \"t2.micro\"\n}\n"
  },
  {
    "path": "examples/terraform-redeploy-example/README.md",
    "content": "# Terraform Redeploy Example\n\nThis folder contains a simple Terraform module that deploys resources in [AWS](https://aws.amazon.com/) to demonstrate\nhow you can use Terratest to write automated tests for your AWS Terraform code. This module deploys an [Auto Scaling\nGroup (ASG)](https://docs.aws.amazon.com/autoscaling/ec2/userguide/AutoScalingGroup.html) in the AWS region specified\nin the `aws_region` variable and a [Load Balancer](https://aws.amazon.com/elasticloadbalancing/) to route traffic\nacross the ASG. Each EC2 Instance in the ASG runs a simple web server that listens for HTTP requests on the port\nspecified by the `instance_port` variable and returns the text specified by the `instance_text` variable. The ASG is\nconfigured to support zero-downtime deployments, which is something we verify in the automated test.\n\nCheck out [test/terraform_redeploy_example_test.go](/test/terraform_redeploy_example_test.go) to see how you can write\nautomated tests for this module.\n\nNote that the example in this module is still fairly simplified, as the \"web server\" we run just servers up a static\n`index.html`, and not in a particularly production-ready manner! For a more complicated, real-world, end-to-end\nexample of a Terraform module and web server, see [terraform-packer-example](/examples/terraform-packer-example).\n\n**WARNING**: This module and the automated tests for it deploy real resources into your AWS account which can cost you\nmoney. The resources are all part of the [AWS Free Tier](https://aws.amazon.com/free/), so if you haven't used that up,\nit should be free, but you are completely responsible for all AWS charges.\n\n\n\n\n\n## Running this module manually\n\n1. Sign up for [AWS](https://aws.amazon.com/).\n1. Configure your AWS credentials using one of the [supported methods for AWS CLI\n   tools](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html), such as setting the\n   `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` environment variables. If you're using the `~/.aws/config` file for profiles then export `AWS_SDK_LOAD_CONFIG` as \"True\".\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`.\n1. Run `terraform init`.\n1. Run `terraform apply`.\n1. The `url` output variable shows you the URL of the load balancer. Try opening it in your browser!\n1. When you're done, run `terraform destroy`.\n\n\n\n\n## Running automated tests against this module\n\n1. Sign up for [AWS](https://aws.amazon.com/).\n1. Configure your AWS credentials using one of the [supported methods for AWS CLI\n   tools](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html), such as setting the\n   `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` environment variables. If you're using the `~/.aws/config` file for profiles then export `AWS_SDK_LOAD_CONFIG` as \"True\".\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`.\n1. Install [Golang](https://golang.org/) and make sure this code is checked out into your `GOPATH`.\n1. `cd test`\n1. `dep ensure`\n1. `go test -v -run TestTerraformRedeployExample`\n"
  },
  {
    "path": "examples/terraform-redeploy-example/main.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# PIN TERRAFORM VERSION TO >= 0.12\n# The examples have been upgraded to 0.12 syntax\n# ---------------------------------------------------------------------------------------------------------------------\n\nterraform {\n  # This module is now only being tested with Terraform 0.13.x. However, to make upgrading easier, we are setting\n  # 0.12.26 as the minimum version, as that version added support for required_providers with source URLs, making it\n  # forwards compatible with 0.13.x code.\n  required_version = \">= 0.12.26\"\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY AN AUTO SCALING GROUP (ASG) WITH AN APPLICATION LOAD BALANCER (ALB) IN FRONT OF IT\n# ---------------------------------------------------------------------------------------------------------------------\n\nprovider \"aws\" {\n  region = var.aws_region\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# CREATE THE ASG\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"aws_autoscaling_group\" \"web_servers\" {\n  # Note that we intentionally depend on the Launch Configuration name so that creating a new Launch Configuration\n  # (e.g. to deploy a new AMI) creates a new Auto Scaling Group. This will allow for rolling deployments.\n  name = aws_launch_configuration.web_servers.name\n\n  launch_configuration = aws_launch_configuration.web_servers.name\n\n  min_size         = 3\n  max_size         = 3\n  desired_capacity = 3\n  min_elb_capacity = 3\n\n  # Deploy into all the subnets (and therefore AZs) available\n  vpc_zone_identifier = data.aws_subnets.default.ids\n\n  # Automatically register this ASG's Instances in the ALB and use the ALB's health check to determine when an Instance\n  # needs to be replaced\n  health_check_type = \"ELB\"\n\n  target_group_arns = [aws_alb_target_group.web_servers.arn]\n\n  tag {\n    key                 = \"Name\"\n    value               = var.instance_name\n    propagate_at_launch = true\n  }\n\n  # To support rolling deployments, we tell Terraform to create a new ASG before deleting the old one. Note: as\n  # soon as you set create_before_destroy = true in one resource, you must also set it in every resource that it\n  # depends on, or you'll get an error about cyclic dependencies (especially when removing resources).\n  lifecycle {\n    create_before_destroy = true\n  }\n\n  # This needs to be here to ensure the ALB has at least one listener rule before the ASG is created. Otherwise, on the\n  # very first deployment, the ALB won't bother doing any health checks, which means min_elb_capacity will not be\n  # achieved, and the whole deployment will fail.\n  depends_on = [aws_alb_listener.http]\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# CREATE THE LAUNCH CONFIGURATION\n# This is a \"template\" that defines the configuration for each EC2 Instance in the ASG\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"aws_launch_configuration\" \"web_servers\" {\n  image_id        = data.aws_ami.ubuntu.id\n  instance_type   = var.instance_type\n  security_groups = [aws_security_group.web_server.id]\n  user_data       = data.template_file.user_data.rendered\n  key_name        = var.key_pair_name\n\n  # When used with an aws_autoscaling_group resource, the aws_launch_configuration must set create_before_destroy to\n  # true. Note: as soon as you set create_before_destroy = true in one resource, you must also set it in every resource\n  # that it depends on, or you'll get an error about cyclic dependencies (especially when removing resources).\n  lifecycle {\n    create_before_destroy = true\n  }\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# CREATE THE USER DATA SCRIPT THAT WILL RUN DURING BOOT ON THE EC2 INSTANCE\n# ---------------------------------------------------------------------------------------------------------------------\n\ndata \"template_file\" \"user_data\" {\n  template = file(\"${path.module}/user-data/user-data.sh\")\n\n  vars = {\n    instance_text = var.instance_text\n    instance_port = var.instance_port\n  }\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# FOR THIS EXAMPLE, WE JUST RUN A PLAIN UBUNTU 22.04 AMI\n# ---------------------------------------------------------------------------------------------------------------------\n\ndata \"aws_ami\" \"ubuntu\" {\n  most_recent = true\n  owners      = [\"099720109477\"] # Canonical\n\n  filter {\n    name   = \"virtualization-type\"\n    values = [\"hvm\"]\n  }\n\n  filter {\n    name   = \"architecture\"\n    values = [\"x86_64\"]\n  }\n\n  filter {\n    name   = \"image-type\"\n    values = [\"machine\"]\n  }\n\n  filter {\n    name   = \"name\"\n    values = [\"ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*\"]\n  }\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# CREATE A SECURITY GROUP TO CONTROL WHAT TRAFFIC CAN GO IN AND OUT OF THE EC2 INSTANCE\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"aws_security_group\" \"web_server\" {\n  name   = var.instance_name\n  vpc_id = data.aws_vpc.default.id\n\n  # This is here because aws_launch_configuration.web_servers sets create_before_destroy to true and depends on this\n  # resource\n  lifecycle {\n    create_before_destroy = true\n  }\n}\n\nresource \"aws_security_group_rule\" \"web_server_allow_http_inbound\" {\n  type              = \"ingress\"\n  from_port         = var.instance_port\n  to_port           = var.instance_port\n  protocol          = \"tcp\"\n  security_group_id = aws_security_group.web_server.id\n  cidr_blocks       = [\"0.0.0.0/0\"]\n}\n\nresource \"aws_security_group_rule\" \"web_server_allow_ssh_inbound\" {\n  type              = \"ingress\"\n  from_port         = var.ssh_port\n  to_port           = var.ssh_port\n  protocol          = \"tcp\"\n  security_group_id = aws_security_group.web_server.id\n  cidr_blocks       = [\"0.0.0.0/0\"]\n}\n\nresource \"aws_security_group_rule\" \"web_server_allow_all_outbound\" {\n  type              = \"egress\"\n  from_port         = 0\n  to_port           = 0\n  protocol          = \"-1\"\n  security_group_id = aws_security_group.web_server.id\n  cidr_blocks       = [\"0.0.0.0/0\"]\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# CREATE AN ALB TO DISTRIBUTE TRAFFIC ACROSS THE ASG\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"aws_alb\" \"web_servers\" {\n  name            = var.instance_name\n  security_groups = [aws_security_group.alb.id]\n  subnets         = data.aws_subnets.default.ids\n\n  # This is here because aws_alb_listener.http depends on this resource and sets create_before_destroy to true\n  lifecycle {\n    create_before_destroy = true\n  }\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# CREATE AN ALB LISTENER FOR HTTP REQUESTS\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"aws_alb_listener\" \"http\" {\n  load_balancer_arn = aws_alb.web_servers.arn\n  port              = var.alb_port\n  protocol          = \"HTTP\"\n\n  default_action {\n    type             = \"forward\"\n    target_group_arn = aws_alb_target_group.web_servers.arn\n  }\n\n  # This is here because aws_autoscaling_group.web_servers depends on this resource and sets create_before_destroy\n  # to true\n  lifecycle {\n    create_before_destroy = true\n  }\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# CREATE AN ALB TARGET GROUP FOR THE ASG\n# This target group will perform health checks on the web servers in the ASG\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"aws_alb_target_group\" \"web_servers\" {\n  depends_on = [aws_alb.web_servers]\n\n  name     = var.instance_name\n  port     = var.instance_port\n  protocol = \"HTTP\"\n  vpc_id   = data.aws_vpc.default.id\n\n  # Give existing connections 10 seconds to complete before deregistering an instance. The default delay is 300 seconds\n  # (5 minutes), which significantly slows down redeploys. In theory, the ALB should deregister the instance as long as\n  # there are no open connections; in practice, it waits the full five minutes every time. If your requests are\n  # generally processed quickly, set this to something lower (such as 10 seconds) to keep redeploys fast.\n  deregistration_delay = 10\n\n  health_check {\n    path                = \"/\"\n    interval            = 15\n    healthy_threshold   = 2\n    unhealthy_threshold = 2\n    timeout             = 5\n  }\n\n  # This is here because aws_autoscaling_group.web_servers depends on this resource and sets create_before_destroy\n  # to true\n  lifecycle {\n    create_before_destroy = true\n  }\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# CREATE AN ALB LISTENER RULE TO SEND ALL REQUESTS TO THE ASG\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"aws_alb_listener_rule\" \"send_all_to_web_servers\" {\n  listener_arn = aws_alb_listener.http.arn\n  priority     = 100\n\n  action {\n    type             = \"forward\"\n    target_group_arn = aws_alb_target_group.web_servers.arn\n  }\n\n  condition {\n    path_pattern {\n      values = [\"*\"]\n    }\n  }\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# CREATE A SECURITY GROUP TO CONTROL WHAT TRAFFIC CAN GO IN AND OUT OF THE ALB\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"aws_security_group\" \"alb\" {\n  name   = \"${var.instance_name}-alb\"\n  vpc_id = data.aws_vpc.default.id\n}\n\nresource \"aws_security_group_rule\" \"alb_allow_http_inbound\" {\n  type              = \"ingress\"\n  from_port         = var.alb_port\n  to_port           = var.alb_port\n  protocol          = \"tcp\"\n  security_group_id = aws_security_group.alb.id\n  cidr_blocks       = [\"0.0.0.0/0\"]\n}\n\n# We need to allow outbound connections from the ALB so it can perform health checks\nresource \"aws_security_group_rule\" \"allow_all_outbound\" {\n  type              = \"egress\"\n  from_port         = 0\n  to_port           = 0\n  protocol          = \"-1\"\n  security_group_id = aws_security_group.alb.id\n  cidr_blocks       = [\"0.0.0.0/0\"]\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY INTO THE DEFAULT VPC AND SUBNETS\n# To keep this example simple, we are deploying into the Default VPC and its subnets. In real-world usage, you should\n# deploy into a custom VPC and private subnets.\n# ---------------------------------------------------------------------------------------------------------------------\n\ndata \"aws_vpc\" \"default\" {\n  default = true\n}\n\ndata \"aws_subnets\" \"default\" {\n  filter {\n    name   = \"vpc-id\"\n    values = [data.aws_vpc.default.id]\n  }\n  filter {\n    name   = \"defaultForAz\"\n    values = [true]\n  }\n}\n\n"
  },
  {
    "path": "examples/terraform-redeploy-example/outputs.tf",
    "content": "output \"alb_dns_name\" {\n  value = aws_alb.web_servers.dns_name\n}\n\noutput \"url\" {\n  value = \"http://${aws_alb.web_servers.dns_name}:${var.alb_port}\"\n}\n\noutput \"asg_name\" {\n  value = aws_autoscaling_group.web_servers.name\n}\n\n"
  },
  {
    "path": "examples/terraform-redeploy-example/user-data/user-data.sh",
    "content": "#!/bin/bash\n# This script is meant to be run in the User Data of an EC2 Instance while it's booting. It starts a simple\n# \"Hello, World\" web server.\n\nset -e\n\n# Send the log output from this script to user-data.log, syslog, and the console\n# From: https://alestic.com/2010/12/ec2-user-data-output/\nexec > >(tee /var/log/user-data.log|logger -t user-data -s 2>/dev/console) 2>&1\n\n# The variables below are filled in using Terraform interpolation\necho \"${instance_text}\" > index.html\nnohup busybox httpd -f -p \"${instance_port}\" &"
  },
  {
    "path": "examples/terraform-redeploy-example/variables.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# ENVIRONMENT VARIABLES\n# Define these secrets as environment variables\n# ---------------------------------------------------------------------------------------------------------------------\n\n# AWS_ACCESS_KEY_ID\n# AWS_SECRET_ACCESS_KEY\n\n# ---------------------------------------------------------------------------------------------------------------------\n# REQUIRED PARAMETERS\n# You must provide a value for each of these parameters.\n# ---------------------------------------------------------------------------------------------------------------------\n\n# ---------------------------------------------------------------------------------------------------------------------\n# OPTIONAL PARAMETERS\n# These parameters have reasonable defaults.\n# ---------------------------------------------------------------------------------------------------------------------\n\nvariable \"aws_region\" {\n  description = \"The AWS region to deploy into (e.g. us-east-1).\"\n  type        = string\n  default     = \"us-east-1\"\n}\n\nvariable \"instance_name\" {\n  description = \"The names for the ASG and other resources in this module\"\n  type        = string\n  default     = \"asg-alb-example\"\n}\n\nvariable \"instance_port\" {\n  description = \"The port each EC2 Instance should listen on for HTTP requests.\"\n  type        = number\n  default     = 8080\n}\n\nvariable \"ssh_port\" {\n  description = \"The port each EC2 Instance should listen on for SSH requests.\"\n  type        = number\n  default     = 22\n}\n\nvariable \"instance_text\" {\n  description = \"The text each EC2 Instance should return when it gets an HTTP request.\"\n  type        = string\n  default     = \"Hello, World!\"\n}\n\nvariable \"alb_port\" {\n  description = \"The port the ALB should listen on for HTTP requests\"\n  type        = number\n  default     = 80\n}\n\nvariable \"key_pair_name\" {\n  description = \"The EC2 Key Pair to associate with the EC2 Instance for SSH access.\"\n  type        = string\n  default     = \"\"\n}\n\nvariable \"instance_type\" {\n  description = \"The EC2 instance type to run.\"\n  type        = string\n  default     = \"t2.micro\"\n}\n"
  },
  {
    "path": "examples/terraform-remote-exec-example/README.md",
    "content": "# Terraform remote-exec example\n\nThis folder contains a simple Terraform module that deploys resources in [AWS](https://aws.amazon.com/) to demonstrate\nhow you can use Terratest to test modules which use `remote-exec`, `files`, and other ssh-based \n[provisioners](https://www.terraform.io/docs/provisioners/index.html).\n\nCheck out [test/terraform_remote_exec_example_test.go](/test/terraform_remote_exec_example_test.go) to see how you can write\nautomated tests for this module.\n\n**WARNING**: This module and the automated tests for it deploy real resources into your AWS account which can cost you\nmoney. The resources are all part of the [AWS Free Tier](https://aws.amazon.com/free/), so if you haven't used that up,\nit should be free, but you are completely responsible for all AWS charges.\n\n## Running this module manually\n\n1. Sign up for [AWS](https://aws.amazon.com/).\n1. Configure your AWS credentials using one of the [supported methods for AWS CLI\n   tools](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html), such as setting the\n   `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` environment variables. If you're using the `~/.aws/config` file for profiles then export `AWS_SDK_LOAD_CONFIG` as \"True\".\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`.\n1. Run `terraform init`.\n1. Run `terraform apply`.\n1. When you're done, run `terraform destroy`.\n\n## Running automated tests against this module\n\n1. Sign up for [AWS](https://aws.amazon.com/).\n1. Configure your AWS credentials using one of the [supported methods for AWS CLI\n   tools](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html), such as setting the\n   `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` environment variables. If you're using the `~/.aws/config` file for profiles then export `AWS_SDK_LOAD_CONFIG` as \"True\".\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`.\n1. Install [Golang](https://golang.org/) and make sure this code is checked out into your `GOPATH`.\n1. `cd test`\n1. `dep ensure`\n1. `go test -v -run TestTerraformRemoteExecExample`\n"
  },
  {
    "path": "examples/terraform-remote-exec-example/files/get-public-ip.sh",
    "content": "#!/bin/bash\n\n# For example purposes, print the public IP address of this instance. This example uses Instance Metadata Service version 2.\nTOKEN=$(curl -s -X PUT \"http://169.254.169.254/latest/api/token\" -H \"X-aws-ec2-metadata-token-ttl-seconds: 21600\") \\\n&& curl -H \"X-aws-ec2-metadata-token: $TOKEN\" -s http://169.254.169.254/latest/meta-data/public-ipv4\n"
  },
  {
    "path": "examples/terraform-remote-exec-example/main.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# PIN TERRAFORM VERSION TO >= 0.12\n# The examples have been upgraded to 0.12 syntax\n# ---------------------------------------------------------------------------------------------------------------------\n\nterraform {\n  # This module is now only being tested with Terraform 0.13.x. However, to make upgrading easier, we are setting\n  # 0.12.26 as the minimum version, as that version added support for required_providers with source URLs, making it\n  # forwards compatible with 0.13.x code.\n  required_version = \">= 0.12.26\"\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY AN INSTANCE, THEN TRIGGER A PROVISIONER\n# See test/terraform_ssh_example.go for how to write automated tests for this code.\n# ---------------------------------------------------------------------------------------------------------------------\n\nprovider \"aws\" {\n  region = var.aws_region\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY THE EC2 INSTANCE WITH A PUBLIC IP\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"aws_instance\" \"example_public\" {\n  ami                    = data.aws_ami.ubuntu.id\n  instance_type          = var.instance_type\n  vpc_security_group_ids = [aws_security_group.example.id]\n  key_name               = var.key_pair_name\n\n  # This EC2 Instance has a public IP and will be accessible directly from the public Internet\n  associate_public_ip_address = true\n\n  tags = {\n    Name = \"${var.instance_name}-public\"\n  }\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# CREATE A SECURITY GROUP TO CONTROL WHAT REQUESTS CAN GO IN AND OUT OF THE EC2 INSTANCES\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"aws_security_group\" \"example\" {\n  name = var.instance_name\n\n  egress {\n    from_port   = 0\n    to_port     = 0\n    protocol    = \"-1\"\n    cidr_blocks = [\"0.0.0.0/0\"]\n  }\n\n  ingress {\n    from_port = var.ssh_port\n    to_port   = var.ssh_port\n    protocol  = \"tcp\"\n\n    # To keep this example simple, we allow incoming SSH requests from any IP. In real-world usage, you should only\n    # allow SSH requests from trusted servers, such as a bastion host or VPN server.\n    cidr_blocks = [\"0.0.0.0/0\"]\n  }\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# Provision the server using remote-exec\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"null_resource\" \"example_provisioner\" {\n  triggers = {\n    public_ip = aws_instance.example_public.public_ip\n  }\n\n  connection {\n    type  = \"ssh\"\n    host  = aws_instance.example_public.public_ip\n    user  = var.ssh_user\n    port  = var.ssh_port\n    agent = true\n  }\n\n  // copy our example script to the server\n  provisioner \"file\" {\n    source      = \"files/get-public-ip.sh\"\n    destination = \"/tmp/get-public-ip.sh\"\n  }\n\n  // change permissions to executable and pipe its output into a new file\n  provisioner \"remote-exec\" {\n    inline = [\n      \"chmod +x /tmp/get-public-ip.sh\",\n      \"/tmp/get-public-ip.sh > /tmp/public-ip\",\n    ]\n  }\n\n  provisioner \"local-exec\" {\n    # copy the public-ip file back to CWD, which will be tested\n    command = \"scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ${var.ssh_user}@${aws_instance.example_public.public_ip}:/tmp/public-ip public-ip\"\n  }\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# LOOK UP THE LATEST UBUNTU AMI\n# ---------------------------------------------------------------------------------------------------------------------\n\ndata \"aws_ami\" \"ubuntu\" {\n  most_recent = true\n  owners      = [\"099720109477\"] # Canonical\n\n  filter {\n    name   = \"virtualization-type\"\n    values = [\"hvm\"]\n  }\n\n  filter {\n    name   = \"architecture\"\n    values = [\"x86_64\"]\n  }\n\n  filter {\n    name   = \"image-type\"\n    values = [\"machine\"]\n  }\n\n  filter {\n    name   = \"name\"\n    values = [\"ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*\"]\n  }\n}\n\n"
  },
  {
    "path": "examples/terraform-remote-exec-example/outputs.tf",
    "content": "output \"public_instance_id\" {\n  value = aws_instance.example_public.id\n}\n\noutput \"public_instance_ip\" {\n  value = aws_instance.example_public.public_ip\n}\n\n"
  },
  {
    "path": "examples/terraform-remote-exec-example/variables.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# ENVIRONMENT VARIABLES\n# Define these secrets as environment variables\n# ---------------------------------------------------------------------------------------------------------------------\n\n# AWS_ACCESS_KEY_ID\n# AWS_SECRET_ACCESS_KEY\n\n# ---------------------------------------------------------------------------------------------------------------------\n# REQUIRED PARAMETERS\n# You must provide a value for each of these parameters.\n# ---------------------------------------------------------------------------------------------------------------------\n\nvariable \"key_pair_name\" {\n  description = \"The EC2 Key Pair to associate with the EC2 Instance for SSH access.\"\n  type        = string\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# OPTIONAL PARAMETERS\n# These parameters have reasonable defaults.\n# ---------------------------------------------------------------------------------------------------------------------\n\nvariable \"aws_region\" {\n  description = \"The AWS region to deploy into\"\n  type        = string\n  default     = \"us-east-1\"\n}\n\nvariable \"instance_name\" {\n  description = \"The Name tag to set for the EC2 Instance.\"\n  type        = string\n  default     = \"terratest-example\"\n}\n\nvariable \"ssh_port\" {\n  description = \"The port the EC2 Instance should listen on for SSH requests.\"\n  type        = number\n  default     = 22\n}\n\nvariable \"ssh_user\" {\n  description = \"SSH user name to use for remote exec connections,\"\n  type        = string\n  default     = \"ubuntu\"\n}\n\nvariable \"instance_type\" {\n  description = \"Instance type to use for EC2 Instance\"\n  type        = string\n  default     = \"t2.micro\"\n}\n\n"
  },
  {
    "path": "examples/terraform-ssh-certificate-example/README.md",
    "content": "# Terraform SSH Password Example\n\nThis folder contains a simple Terraform module that deploys resources in [AWS](https://aws.amazon.com/) to demonstrate\nhow you can use Terratest to write automated tests for your AWS Terraform code. This module deploys an [EC2\nInstance](https://aws.amazon.com/ec2/) with a public IP in the AWS region specified in the `aws_region` variable. The\nEC2 Instance allows SSH requests on the port specified by the `ssh_port` variable, and is configured with a user data\nscript so that it will authentication with ssh certificate.\n\nPlease note that the Terraform deployment outlined in\n[the example directory for this test](/examples/terraform-ssh-certificate) will expect a default VPC to exist in\nthe target region for deployments to go into.\n\nIf this default VPC has been deleted from your AWS account, it can be recreated with the following command:\n\n``` shell\n$ aws ec2 create-default-vpc --region eu-west-2\n{\n    \"Vpc\": {\n        \"CidrBlock\": \"172.31.0.0/16\",\n        \"InstanceTenancy\": \"default\",\n        \"IsDefault\": true,\n        \"State\": \"pending\",\n        ...\n```\n\nCheck out [test/terraform_ssh_password_example_test.go](/test/terraform_ssh_password_example_test.go) to see how you\ncan write automated tests for this module.\n\nNote that the example in this module is still fairly simplified, as the EC2 Instance doesn't do a whole lot! For a more\ncomplicated, real-world, end-to-end example of a Terraform module and web server, see\n[terraform-packer-example](/examples/terraform-packer-example).\n\n**WARNING**: This module and the automated tests for it deploy real resources into your AWS account which can cost you\nmoney. The resources are all part of the [AWS Free Tier](https://aws.amazon.com/free/), so if you haven't used that up,\nit should be free, but you are completely responsible for all AWS charges.\n\n## Running this module manually\n\n1. Sign up for [AWS](https://aws.amazon.com/).\n1. Configure your AWS credentials using one of the [supported methods for AWS CLI\n   tools](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html), such as setting the\n   `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` environment variables. If you're using the `~/.aws/config` file for profiles then export `AWS_SDK_LOAD_CONFIG` as \"True\".\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`.\n1. Run `terraform init`.\n1. Run `terraform apply`.\n1. When you're done, run `terraform destroy`.\n\n## Running automated tests against this module\n\n1. Sign up for [AWS](https://aws.amazon.com/).\n1. Configure your AWS credentials using one of the [supported methods for AWS CLI\n   tools](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html), such as setting the\n   `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` environment variables. If you're using the `~/.aws/config` file for profiles then export `AWS_SDK_LOAD_CONFIG` as \"True\".\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`.\n1. Install [Golang](https://golang.org/) and make sure this code is checked out into your `GOPATH`.\n1. `cd test`\n1. `dep ensure`\n1. `go test -v -run TestTerraformSshCertificateExample`\n"
  },
  {
    "path": "examples/terraform-ssh-certificate-example/main.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# PIN TERRAFORM VERSION TO >= 0.12\n# The examples have been upgraded to 0.12 syntax\n# ---------------------------------------------------------------------------------------------------------------------\n\nterraform {\n  # This module is now only being tested with Terraform 0.13.x. However, to make upgrading easier, we are setting\n  # 0.12.26 as the minimum version, as that version added support for required_providers with source URLs, making it\n  # forwards compatible with 0.13.x code.\n  required_version = \">= 0.12.26\"\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY AN EC2 INSTANCE THAT ALLOWS CONNECTIONS VIA SSH\n# See test/terraform_ssh_password_example.go for how to write automated tests for this code.\n# ---------------------------------------------------------------------------------------------------------------------\n\nprovider \"aws\" {\n  region = var.aws_region\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY THE EC2 INSTANCE WITH A PUBLIC IP\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"aws_instance\" \"example_public\" {\n  ami           = data.aws_ami.ubuntu.id\n  instance_type = var.instance_type\n  user_data = templatefile(\"${path.module}/user_data.sh\", {\n    ssh_ca_public_key = var.ssh_ca_public_key\n  })\n\n  vpc_security_group_ids = [\n    aws_security_group.example.id,\n  ]\n\n  # This EC2 Instance has a public IP and will be accessible directly from the public Internet\n  associate_public_ip_address = \"true\"\n\n  tags = {\n    Name = \"${var.instance_name}-public\"\n  }\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# CREATE A SECURITY GROUP TO CONTROL WHAT REQUESTS CAN GO IN AND OUT OF THE EC2 INSTANCE\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"aws_security_group\" \"example\" {\n  name = var.instance_name\n\n  egress {\n    from_port   = 0\n    to_port     = 0\n    protocol    = \"-1\"\n    cidr_blocks = [\"0.0.0.0/0\"]\n  }\n\n  ingress {\n    from_port = var.ssh_port\n    to_port   = var.ssh_port\n    protocol  = \"tcp\"\n\n    # To keep this example simple, we allow incoming SSH requests from any IP. In real-world usage, you should only\n    # allow SSH requests from trusted servers, such as a bastion host or VPN server.\n    cidr_blocks = [\"0.0.0.0/0\"]\n  }\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# LOOK UP THE LATEST UBUNTU AMI\n# ---------------------------------------------------------------------------------------------------------------------\n\ndata \"aws_ami\" \"ubuntu\" {\n  most_recent = \"true\"\n  owners      = [\"099720109477\"] # Canonical\n\n  filter {\n    name   = \"virtualization-type\"\n    values = [\"hvm\"]\n  }\n\n  filter {\n    name   = \"architecture\"\n    values = [\"x86_64\"]\n  }\n\n  filter {\n    name   = \"image-type\"\n    values = [\"machine\"]\n  }\n\n  filter {\n    name   = \"name\"\n    values = [\"ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*\"]\n  }\n}\n\n"
  },
  {
    "path": "examples/terraform-ssh-certificate-example/outputs.tf",
    "content": "output \"public_instance_id\" {\n  value = aws_instance.example_public.id\n}\n\noutput \"public_instance_ip\" {\n  value = aws_instance.example_public.public_ip\n}\n\n"
  },
  {
    "path": "examples/terraform-ssh-certificate-example/user_data.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\n# Send the log output from this script to user-data.log, syslog, and the console\n# From: https://alestic.com/2010/12/ec2-user-data-output/\nexec > >(tee /var/log/user-data.log | logger -t user-data -s 2>/dev/console) 2>&1\n\n# Create our new 'terratest' user\nadduser --disabled-password --gecos \"\" terratest\n\n# Create CA pubkey file\nmkdir -p /etc/ssh\ncat > /etc/ssh/trusted-user-ca-keys.pub <<'EOKEY'\n${ssh_ca_public_key}\nEOKEY\n\n# Drop-in configuration for sshd\nmkdir -p /etc/ssh/sshd_config.d\necho 'TrustedUserCAKeys /etc/ssh/trusted-user-ca-keys.pub' > /etc/ssh/sshd_config.d/ca.conf\n\n# Bounce the service to apply the config change\nservice ssh restart\n"
  },
  {
    "path": "examples/terraform-ssh-certificate-example/variables.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# ENVIRONMENT VARIABLES\n# Define these secrets as environment variables\n# ---------------------------------------------------------------------------------------------------------------------\n\n# AWS_ACCESS_KEY_ID\n# AWS_SECRET_ACCESS_KEY\n\n# ---------------------------------------------------------------------------------------------------------------------\n# REQUIRED PARAMETERS\n# You must provide a value for each of these parameters.\n# ---------------------------------------------------------------------------------------------------------------------\n\nvariable \"ssh_ca_public_key\" {\n  description = \"The public key for the ssh connection to the instance.\"\n  type        = string\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# OPTIONAL PARAMETERS\n# These parameters have reasonable defaults.\n# ---------------------------------------------------------------------------------------------------------------------\n\nvariable \"aws_region\" {\n  description = \"The AWS region to deploy into\"\n  type        = string\n  default     = \"us-east-1\"\n}\n\nvariable \"instance_name\" {\n  description = \"The Name tag to set for the EC2 Instance.\"\n  type        = string\n  default     = \"terratest-example\"\n}\n\nvariable \"ssh_port\" {\n  description = \"The port the EC2 Instance should listen on for SSH requests.\"\n  type        = number\n  default     = 22\n}\n\nvariable \"instance_type\" {\n  description = \"The EC2 instance type to run.\"\n  type        = string\n  default     = \"t2.micro\"\n}\n"
  },
  {
    "path": "examples/terraform-ssh-example/README.md",
    "content": "# Terraform SSH Example\n\nThis folder contains a simple Terraform module that deploys resources in [AWS](https://aws.amazon.com/) to demonstrate\nhow you can use Terratest to write automated tests for your AWS Terraform code. This module deploys two [EC2\nInstances](https://aws.amazon.com/ec2/), one with a public IP, one with a private IP, in the AWS region specified in\nthe `aws_region` variable. The EC2 Instances allow SSH requests on the port specified by the `ssh_port` variable.\n\nCheck out [test/terraform_ssh_example_test.go](/test/terraform_ssh_example_test.go) to see how you can write\nautomated tests for this module.\n\nNote that the example in this module is still fairly simplified, as the EC2 Instance doesn't do a whole lot! For a more\ncomplicated, real-world, end-to-end example of a Terraform module and web server, see\n[terraform-packer-example](/examples/terraform-packer-example).\n\n**WARNING**: This module and the automated tests for it deploy real resources into your AWS account which can cost you\nmoney. The resources are all part of the [AWS Free Tier](https://aws.amazon.com/free/), so if you haven't used that up,\nit should be free, but you are completely responsible for all AWS charges.\n\n\n\n\n\n## Running this module manually\n\n1. Sign up for [AWS](https://aws.amazon.com/).\n1. Configure your AWS credentials using one of the [supported methods for AWS CLI\n   tools](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html), such as setting the\n   `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` environment variables. If you're using the `~/.aws/config` file for profiles then export `AWS_SDK_LOAD_CONFIG` as \"True\".\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`.\n1. Run `terraform init`.\n1. Run `terraform apply`.\n1. When you're done, run `terraform destroy`.\n\n\n\n\n## Running automated tests against this module\n\n1. Sign up for [AWS](https://aws.amazon.com/).\n1. Configure your AWS credentials using one of the [supported methods for AWS CLI\n   tools](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html), such as setting the\n   `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` environment variables. If you're using the `~/.aws/config` file for profiles then export `AWS_SDK_LOAD_CONFIG` as \"True\".\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`.\n1. Install [Golang](https://golang.org/) and make sure this code is checked out into your `GOPATH`.\n1. `cd test`\n1. `dep ensure`\n1. `go test -v -run TestTerraformSshExample`\n"
  },
  {
    "path": "examples/terraform-ssh-example/main.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# PIN TERRAFORM VERSION TO >= 0.12\n# The examples have been upgraded to 0.12 syntax\n# ---------------------------------------------------------------------------------------------------------------------\n\nterraform {\n  # This module is now only being tested with Terraform 0.13.x. However, to make upgrading easier, we are setting\n  # 0.12.26 as the minimum version, as that version added support for required_providers with source URLs, making it\n  # forwards compatible with 0.13.x code.\n  required_version = \">= 0.12.26\"\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY TWO EC2 INSTANCES THAT ALLOWS CONNECTIONS VIA SSH\n# See test/terraform_ssh_example.go for how to write automated tests for this code.\n# ---------------------------------------------------------------------------------------------------------------------\n\nprovider \"aws\" {\n  region = var.aws_region\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY THE EC2 INSTANCE WITH A PUBLIC IP\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"aws_instance\" \"example_public\" {\n  ami                    = data.aws_ami.ubuntu.id\n  instance_type          = var.instance_type\n  vpc_security_group_ids = [aws_security_group.example.id]\n  key_name               = var.key_pair_name\n\n  # This EC2 Instance has a public IP and will be accessible directly from the public Internet\n  associate_public_ip_address = true\n\n  tags = {\n    Name = \"${var.instance_name}-public\"\n  }\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY THE EC2 INSTANCE WITH A PRIVATE IP\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"aws_instance\" \"example_private\" {\n  ami                    = data.aws_ami.ubuntu.id\n  instance_type          = var.instance_type\n  vpc_security_group_ids = [aws_security_group.example.id]\n  key_name               = var.key_pair_name\n\n  # This EC2 Instance has a private IP and will be accessible only from within the VPC\n  associate_public_ip_address = false\n\n  tags = {\n    Name = \"${var.instance_name}-private\"\n  }\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# CREATE A SECURITY GROUP TO CONTROL WHAT REQUESTS CAN GO IN AND OUT OF THE EC2 INSTANCES\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"aws_security_group\" \"example\" {\n  name = var.instance_name\n\n  egress {\n    from_port   = 0\n    to_port     = 0\n    protocol    = \"-1\"\n    cidr_blocks = [\"0.0.0.0/0\"]\n  }\n\n  ingress {\n    from_port = var.ssh_port\n    to_port   = var.ssh_port\n    protocol  = \"tcp\"\n\n    # To keep this example simple, we allow incoming SSH requests from any IP. In real-world usage, you should only\n    # allow SSH requests from trusted servers, such as a bastion host or VPN server.\n    cidr_blocks = [\"0.0.0.0/0\"]\n  }\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# LOOK UP THE LATEST UBUNTU AMI\n# ---------------------------------------------------------------------------------------------------------------------\n\ndata \"aws_ami\" \"ubuntu\" {\n  most_recent = true\n  owners      = [\"099720109477\"] # Canonical\n\n  filter {\n    name   = \"virtualization-type\"\n    values = [\"hvm\"]\n  }\n\n  filter {\n    name   = \"architecture\"\n    values = [\"x86_64\"]\n  }\n\n  filter {\n    name   = \"image-type\"\n    values = [\"machine\"]\n  }\n\n  filter {\n    name   = \"name\"\n    values = [\"ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*\"]\n  }\n}\n\n"
  },
  {
    "path": "examples/terraform-ssh-example/outputs.tf",
    "content": "output \"public_instance_id\" {\n  value = aws_instance.example_public.id\n}\n\noutput \"public_instance_ip\" {\n  value = aws_instance.example_public.public_ip\n}\n\noutput \"private_instance_id\" {\n  value = aws_instance.example_private.id\n}\n\noutput \"private_instance_ip\" {\n  value = aws_instance.example_private.private_ip\n}\n\n"
  },
  {
    "path": "examples/terraform-ssh-example/variables.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# ENVIRONMENT VARIABLES\n# Define these secrets as environment variables\n# ---------------------------------------------------------------------------------------------------------------------\n\n# AWS_ACCESS_KEY_ID\n# AWS_SECRET_ACCESS_KEY\n\n# ---------------------------------------------------------------------------------------------------------------------\n# REQUIRED PARAMETERS\n# You must provide a value for each of these parameters.\n# ---------------------------------------------------------------------------------------------------------------------\n\nvariable \"key_pair_name\" {\n  description = \"The EC2 Key Pair to associate with the EC2 Instance for SSH access.\"\n  type        = string\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# OPTIONAL PARAMETERS\n# These parameters have reasonable defaults.\n# ---------------------------------------------------------------------------------------------------------------------\n\nvariable \"aws_region\" {\n  description = \"The AWS region to deploy into\"\n  type        = string\n  default     = \"us-east-1\"\n}\n\nvariable \"instance_name\" {\n  description = \"The Name tag to set for the EC2 Instance.\"\n  type        = string\n  default     = \"terratest-example\"\n}\n\nvariable \"ssh_port\" {\n  description = \"The port the EC2 Instance should listen on for SSH requests.\"\n  type        = number\n  default     = 22\n}\n\nvariable \"instance_type\" {\n  description = \"The EC2 instance type to run.\"\n  type        = string\n  default     = \"t2.micro\"\n}\n"
  },
  {
    "path": "examples/terraform-ssh-password-example/README.md",
    "content": "# Terraform SSH Password Example\n\nThis folder contains a simple Terraform module that deploys resources in [AWS](https://aws.amazon.com/) to demonstrate\nhow you can use Terratest to write automated tests for your AWS Terraform code. This module deploys an [EC2\nInstance](https://aws.amazon.com/ec2/) with a public IP in the AWS region specified in the `aws_region` variable. The\nEC2 Instance allows SSH requests on the port specified by the `ssh_port` variable, and is configured with a user data\nscript so that it will accept passwords for authentication.\n\nPlease note that the Terraform deployment outlined in\n[the example directory for this test](/examples/terraform-ssh-password-example) will expect a default VPC to exist in\nthe target region for deployments to go into.\n\nIf this default VPC has been deleted from your AWS account, it can be recreated with the following command:\n\n``` shell\n$ aws ec2 create-default-vpc --region eu-west-2\n{\n    \"Vpc\": {\n        \"CidrBlock\": \"172.31.0.0/16\",\n        \"InstanceTenancy\": \"default\",\n        \"IsDefault\": true,\n        \"State\": \"pending\",\n        ...\n```\n\nCheck out [test/terraform_ssh_password_example_test.go](/test/terraform_ssh_password_example_test.go) to see how you\ncan write automated tests for this module.\n\nNote that the example in this module is still fairly simplified, as the EC2 Instance doesn't do a whole lot! For a more\ncomplicated, real-world, end-to-end example of a Terraform module and web server, see\n[terraform-packer-example](/examples/terraform-packer-example).\n\n**WARNING**: This module and the automated tests for it deploy real resources into your AWS account which can cost you\nmoney. The resources are all part of the [AWS Free Tier](https://aws.amazon.com/free/), so if you haven't used that up,\nit should be free, but you are completely responsible for all AWS charges.\n\n## Running this module manually\n\n1. Sign up for [AWS](https://aws.amazon.com/).\n1. Configure your AWS credentials using one of the [supported methods for AWS CLI\n   tools](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html), such as setting the\n   `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` environment variables. If you're using the `~/.aws/config` file for profiles then export `AWS_SDK_LOAD_CONFIG` as \"True\".\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`.\n1. Run `terraform init`.\n1. Run `terraform apply`.\n1. When you're done, run `terraform destroy`.\n\n## Running automated tests against this module\n\n1. Sign up for [AWS](https://aws.amazon.com/).\n1. Configure your AWS credentials using one of the [supported methods for AWS CLI\n   tools](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html), such as setting the\n   `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` environment variables. If you're using the `~/.aws/config` file for profiles then export `AWS_SDK_LOAD_CONFIG` as \"True\".\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`.\n1. Install [Golang](https://golang.org/) and make sure this code is checked out into your `GOPATH`.\n1. `cd test`\n1. `dep ensure`\n1. `go test -v -run TestTerraformSshPasswordExample`\n"
  },
  {
    "path": "examples/terraform-ssh-password-example/main.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# PIN TERRAFORM VERSION TO >= 0.12\n# The examples have been upgraded to 0.12 syntax\n# ---------------------------------------------------------------------------------------------------------------------\n\nterraform {\n  # This module is now only being tested with Terraform 0.13.x. However, to make upgrading easier, we are setting\n  # 0.12.26 as the minimum version, as that version added support for required_providers with source URLs, making it\n  # forwards compatible with 0.13.x code.\n  required_version = \">= 0.12.26\"\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY AN EC2 INSTANCE THAT ALLOWS CONNECTIONS VIA SSH\n# See test/terraform_ssh_password_example.go for how to write automated tests for this code.\n# ---------------------------------------------------------------------------------------------------------------------\n\nprovider \"aws\" {\n  region = var.aws_region\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# DEPLOY THE EC2 INSTANCE WITH A PUBLIC IP\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"aws_instance\" \"example_public\" {\n  ami           = data.aws_ami.ubuntu.id\n  instance_type = var.instance_type\n  user_data     = data.template_file.user_data.rendered\n\n  vpc_security_group_ids = [\n    aws_security_group.example.id,\n  ]\n\n  # This EC2 Instance has a public IP and will be accessible directly from the public Internet\n  associate_public_ip_address = \"true\"\n\n  tags = {\n    Name = \"${var.instance_name}-public\"\n  }\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# CREATE A SECURITY GROUP TO CONTROL WHAT REQUESTS CAN GO IN AND OUT OF THE EC2 INSTANCE\n# ---------------------------------------------------------------------------------------------------------------------\n\nresource \"aws_security_group\" \"example\" {\n  name = var.instance_name\n\n  egress {\n    from_port   = 0\n    to_port     = 0\n    protocol    = \"-1\"\n    cidr_blocks = [\"0.0.0.0/0\"]\n  }\n\n  ingress {\n    from_port = var.ssh_port\n    to_port   = var.ssh_port\n    protocol  = \"tcp\"\n\n    # To keep this example simple, we allow incoming SSH requests from any IP. In real-world usage, you should only\n    # allow SSH requests from trusted servers, such as a bastion host or VPN server.\n    cidr_blocks = [\"0.0.0.0/0\"]\n  }\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# SET UP A TEMPLATE AROUND THE USER DATA SCRIPT\n# ---------------------------------------------------------------------------------------------------------------------\n\ndata \"template_file\" \"user_data\" {\n  template = file(\"${path.module}/user_data.sh\")\n\n  vars = {\n    terratest_password = var.terratest_password\n  }\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# LOOK UP THE LATEST UBUNTU AMI\n# ---------------------------------------------------------------------------------------------------------------------\n\ndata \"aws_ami\" \"ubuntu\" {\n  most_recent = \"true\"\n  owners      = [\"099720109477\"] # Canonical\n\n  filter {\n    name   = \"virtualization-type\"\n    values = [\"hvm\"]\n  }\n\n  filter {\n    name   = \"architecture\"\n    values = [\"x86_64\"]\n  }\n\n  filter {\n    name   = \"image-type\"\n    values = [\"machine\"]\n  }\n\n  filter {\n    name   = \"name\"\n    values = [\"ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*\"]\n  }\n}\n\n"
  },
  {
    "path": "examples/terraform-ssh-password-example/outputs.tf",
    "content": "output \"public_instance_id\" {\n  value = aws_instance.example_public.id\n}\n\noutput \"public_instance_ip\" {\n  value = aws_instance.example_public.public_ip\n}\n\n"
  },
  {
    "path": "examples/terraform-ssh-password-example/user_data.sh",
    "content": "#!/usr/bin/env bash\nset -euo pipefail\n\n# Send the log output from this script to user-data.log, syslog, and the console\n# From: https://alestic.com/2010/12/ec2-user-data-output/\nexec > >(tee /var/log/user-data.log | logger -t user-data -s 2>/dev/console) 2>&1\n\n# Create our new 'terratest' user\nadduser --disabled-password --gecos \"\" terratest\n\n# Set the user's password based on the random input from 'test/terraform_ssh_password_example_test.go'\n# shellcheck disable=SC2154\necho \"terratest:${terratest_password}\" | chpasswd\n\n# Enable password auth on the SSH service\necho \"PasswordAuthentication yes\" > /etc/ssh/sshd_config.d/01-password-auth.conf\n\n# Bounce the service to apply the config change\nservice ssh restart\n"
  },
  {
    "path": "examples/terraform-ssh-password-example/variables.tf",
    "content": "# ---------------------------------------------------------------------------------------------------------------------\n# ENVIRONMENT VARIABLES\n# Define these secrets as environment variables\n# ---------------------------------------------------------------------------------------------------------------------\n\n# AWS_ACCESS_KEY_ID\n# AWS_SECRET_ACCESS_KEY\n\n# ---------------------------------------------------------------------------------------------------------------------\n# REQUIRED PARAMETERS\n# You must provide a value for each of these parameters.\n# ---------------------------------------------------------------------------------------------------------------------\n\nvariable \"terratest_password\" {\n  description = \"The password to set for the 'terratest' user within the EC2 Instance, for SSH access.\"\n  type        = string\n}\n\n# ---------------------------------------------------------------------------------------------------------------------\n# OPTIONAL PARAMETERS\n# These parameters have reasonable defaults.\n# ---------------------------------------------------------------------------------------------------------------------\n\nvariable \"aws_region\" {\n  description = \"The AWS region to deploy into\"\n  type        = string\n  default     = \"us-east-1\"\n}\n\nvariable \"instance_name\" {\n  description = \"The Name tag to set for the EC2 Instance.\"\n  type        = string\n  default     = \"terratest-example\"\n}\n\nvariable \"ssh_port\" {\n  description = \"The port the EC2 Instance should listen on for SSH requests.\"\n  type        = number\n  default     = 22\n}\n\nvariable \"instance_type\" {\n  description = \"The EC2 instance type to run.\"\n  type        = string\n  default     = \"t2.micro\"\n}\n"
  },
  {
    "path": "examples/terragrunt-example/README.md",
    "content": "# Terragrunt Example\n\nThis folder contains a single Terragrunt unit demonstrating how to test it using Terratest's `terraform` package\nwith `TerraformBinary: \"terragrunt\"`.\n\nCheck out [modules/terragrunt/terragrunt_example_test.go](/modules/terragrunt/terragrunt_example_test.go) to see how you can write automated tests\nfor this module.\n\nFor testing a stack of Terragrunt units with dependencies (using `--all` commands), see\n[terragrunt-multi-module-example](/examples/terragrunt-multi-module-example).\n\n\n\n\n## Running this module manually\n\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`.\n1. Install [Terragrunt](https://terragrunt.gruntwork.io/) and make sure it's on your `PATH`.\n1. Run `terragrunt apply`.\n1. When you're done, run `terragrunt destroy`.\n\n\n\n\n## Running automated tests against this module\n\n1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`.\n1. Install [Terragrunt](https://terragrunt.gruntwork.io/) and make sure it's on your `PATH`.\n1. Install [Golang](https://golang.org/) and make sure this code is checked out into your `GOPATH`.\n1. `cd test`\n1. `go test -v -run TestTerragruntExample`\n"
  },
  {
    "path": "examples/terragrunt-example/main.tf",
    "content": "variable \"input\" {}\nvariable \"other_input\" {}\n\noutput \"output\" {\n  value = \"${var.input} ${var.other_input}\"\n}\n\nlocals {\n  mylocal = \"local variable named mylocal\"\n}\n"
  },
  {
    "path": "examples/terragrunt-example/terragrunt.hcl",
    "content": "inputs = {\n  input = \"one input\" \n  other_input = \"another input\" \n  extraneous_input = \"an unused input\"\n}\n"
  },
  {
    "path": "examples/terragrunt-multi-module-example/README.md",
    "content": "# Terragrunt Multi-Module Example\n\nThis folder contains a Terragrunt configuration with multiple modules that have dependencies (VPC → Database → App),\ndemonstrating how to use Terratest's `terragrunt` package to test stack configurations.\n\nCheck out [modules/terragrunt/terragrunt_example_test.go](/modules/terragrunt/terragrunt_example_test.go) to see how you can write automated tests\nfor this configuration using `ApplyAll` and `DestroyAll`.\n\n## Structure\n\n```\n.\n├── modules/           # Terraform modules\n│   ├── vpc/\n│   ├── database/     # Depends on VPC\n│   └── app/          # Depends on VPC and Database\n└── live/             # Terragrunt configurations\n    ├── vpc/\n    ├── database/\n    └── app/\n```\n\n## Running this module manually\n\n1. Install [Terraform](https://www.terraform.io/) and [Terragrunt](https://terragrunt.gruntwork.io/).\n1. `cd live`\n1. Run `terragrunt apply --all`.\n1. When you're done, run `terragrunt destroy --all`.\n\n## Running automated tests against this module\n\n1. Install [Terraform](https://www.terraform.io/) and [Terragrunt](https://terragrunt.gruntwork.io/).\n1. Install [Golang](https://golang.org/) and make sure this code is checked out into your `GOPATH`.\n1. `cd test`\n1. `go test -v -run TestTerragruntMultiModuleExample`\n"
  },
  {
    "path": "examples/terragrunt-multi-module-example/live/app/terragrunt.hcl",
    "content": "terraform {\n  source = \"../../modules//app\"\n}\n\ndependency \"vpc\" {\n  config_path = \"../vpc\"\n}\n\ndependency \"database\" {\n  config_path = \"../database\"\n}\n\ninputs = {\n  environment       = \"test\"\n  vpc_id            = dependency.vpc.outputs.vpc_id\n  subnet_ids        = dependency.vpc.outputs.subnet_ids\n  database_endpoint = dependency.database.outputs.database_endpoint\n  database_port     = dependency.database.outputs.database_port\n}\n"
  },
  {
    "path": "examples/terragrunt-multi-module-example/live/database/terragrunt.hcl",
    "content": "terraform {\n  source = \"../../modules//database\"\n}\n\ndependency \"vpc\" {\n  config_path = \"../vpc\"\n}\n\ninputs = {\n  environment = \"test\"\n  vpc_id      = dependency.vpc.outputs.vpc_id\n  subnet_ids  = dependency.vpc.outputs.subnet_ids\n}\n"
  },
  {
    "path": "examples/terragrunt-multi-module-example/live/vpc/terragrunt.hcl",
    "content": "terraform {\n  source = \"../../modules//vpc\"\n}\n\ninputs = {\n  environment = \"test\"\n}\n"
  },
  {
    "path": "examples/terragrunt-multi-module-example/modules/app/main.tf",
    "content": "variable \"vpc_id\" {\n  description = \"VPC ID where the app will be deployed\"\n  type        = string\n}\n\nvariable \"subnet_ids\" {\n  description = \"Subnet IDs for the app\"\n  type        = list(string)\n}\n\nvariable \"database_endpoint\" {\n  description = \"Database endpoint to connect to\"\n  type        = string\n}\n\nvariable \"database_port\" {\n  description = \"Database port\"\n  type        = number\n}\n\nvariable \"environment\" {\n  description = \"Environment name\"\n  type        = string\n}\n\noutput \"app_url\" {\n  description = \"Application URL\"\n  value       = \"https://app-${var.environment}.example.com\"\n}\n\noutput \"connection_string\" {\n  description = \"Database connection info\"\n  value       = \"${var.database_endpoint}:${var.database_port}\"\n}\n"
  },
  {
    "path": "examples/terragrunt-multi-module-example/modules/database/main.tf",
    "content": "variable \"vpc_id\" {\n  description = \"VPC ID where the database will be deployed\"\n  type        = string\n}\n\nvariable \"subnet_ids\" {\n  description = \"Subnet IDs for the database\"\n  type        = list(string)\n}\n\nvariable \"environment\" {\n  description = \"Environment name\"\n  type        = string\n}\n\noutput \"database_endpoint\" {\n  description = \"Database endpoint\"\n  value       = \"db-${var.environment}.example.com\"\n}\n\noutput \"database_port\" {\n  description = \"Database port\"\n  value       = 5432\n}\n"
  },
  {
    "path": "examples/terragrunt-multi-module-example/modules/vpc/main.tf",
    "content": "variable \"environment\" {\n  description = \"Environment name\"\n  type        = string\n}\n\noutput \"vpc_id\" {\n  description = \"The VPC ID\"\n  value       = \"vpc-${var.environment}-12345\"\n}\n\noutput \"subnet_ids\" {\n  description = \"List of subnet IDs\"\n  value       = [\"subnet-${var.environment}-a\", \"subnet-${var.environment}-b\"]\n}\n"
  },
  {
    "path": "examples/terragrunt-second-example/main.tf",
    "content": "variable \"input\" {}\nvariable \"other_input\" {}\n\noutput \"output\" {\n  value = \"${var.input} ${var.other_input}\"\n}\n"
  },
  {
    "path": "examples/terragrunt-second-example/terragrunt.hcl",
    "content": "inputs = {\n  input = \"one input\" \n  other_input = \"another input\" \n  extraneous_input = \"an unused input\"\n  more_extraneous_input = \"another unused input\"\n}\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/gruntwork-io/terratest\n\ngo 1.26\n\nrequire (\n\tcloud.google.com/go v0.116.0 // indirect\n\tcloud.google.com/go/storage v1.47.0\n\tgithub.com/Azure/azure-sdk-for-go v51.0.0+incompatible\n\tgithub.com/Azure/go-autorest/autorest v0.11.20\n\tgithub.com/Azure/go-autorest/autorest/azure/auth v0.5.8\n\tgithub.com/Azure/go-autorest/autorest/to v0.4.0 // indirect\n\tgithub.com/Azure/go-autorest/autorest/validation v0.3.1 // indirect\n\tgithub.com/aws/aws-lambda-go v1.47.0\n\tgithub.com/go-errors/errors v1.0.2-0.20180813162953-d98b870cc4e0 // indirect\n\tgithub.com/go-sql-driver/mysql v1.8.1\n\tgithub.com/google/go-containerregistry v0.20.2\n\tgithub.com/google/uuid v1.6.0\n\tgithub.com/gruntwork-io/go-commons v0.8.0\n\tgithub.com/hashicorp/go-multierror v1.1.1\n\tgithub.com/hashicorp/go-version v1.7.0\n\tgithub.com/hashicorp/hcl/v2 v2.22.0\n\tgithub.com/hashicorp/terraform-json v0.23.0\n\tgithub.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a\n\tgithub.com/jstemmer/go-junit-report v1.0.0\n\tgithub.com/magiconair/properties v1.8.7\n\tgithub.com/mattn/go-zglob v0.0.2-0.20190814121620-e3c945676326\n\tgithub.com/miekg/dns v1.1.62\n\tgithub.com/mitchellh/go-homedir v1.1.0\n\tgithub.com/oracle/oci-go-sdk v7.1.0+incompatible\n\tgithub.com/pquerna/otp v1.4.0\n\tgithub.com/sirupsen/logrus v1.9.3\n\tgithub.com/stretchr/testify v1.11.1\n\tgithub.com/tmccombs/hcl2json v0.6.4\n\tgithub.com/urfave/cli v1.22.16\n\tgithub.com/zclconf/go-cty v1.15.0\n\tgolang.org/x/crypto v0.46.0\n\tgolang.org/x/net v0.48.0 // indirect\n\tgolang.org/x/oauth2 v0.34.0\n\tgoogle.golang.org/api v0.206.0\n\tgoogle.golang.org/genproto v0.0.0-20241113202542-65e8d215514f\n\tk8s.io/api v0.35.0\n\tk8s.io/apimachinery v0.35.0\n\tk8s.io/client-go v0.35.0\n)\n\nrequire (\n\tcloud.google.com/go/cloudbuild v1.19.0\n\tgithub.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0\n\tgithub.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1\n\tgithub.com/Azure/azure-sdk-for-go/sdk/resourcemanager/appcontainers/armappcontainers/v3 v3.0.0\n\tgithub.com/Azure/azure-sdk-for-go/sdk/resourcemanager/appservice/armappservice/v2 v2.3.0\n\tgithub.com/Azure/azure-sdk-for-go/sdk/resourcemanager/datafactory/armdatafactory/v9 v9.1.0\n\tgithub.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault v1.5.0\n\tgithub.com/Azure/azure-sdk-for-go/sdk/resourcemanager/mysql/armmysql v1.2.0\n\tgithub.com/Azure/azure-sdk-for-go/sdk/resourcemanager/postgresql/armpostgresql v1.2.0\n\tgithub.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0\n\tgithub.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql v1.2.0\n\tgithub.com/Azure/azure-sdk-for-go/sdk/resourcemanager/synapse/armsynapse v0.8.0\n\tgithub.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azcertificates v1.4.0\n\tgithub.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.4.0\n\tgithub.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.4.0\n\tgithub.com/aws/aws-sdk-go-v2 v1.32.5\n\tgithub.com/aws/aws-sdk-go-v2/config v1.28.5\n\tgithub.com/aws/aws-sdk-go-v2/credentials v1.17.46\n\tgithub.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.41\n\tgithub.com/aws/aws-sdk-go-v2/service/acm v1.30.6\n\tgithub.com/aws/aws-sdk-go-v2/service/autoscaling v1.51.0\n\tgithub.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.44.0\n\tgithub.com/aws/aws-sdk-go-v2/service/dynamodb v1.37.1\n\tgithub.com/aws/aws-sdk-go-v2/service/ec2 v1.193.0\n\tgithub.com/aws/aws-sdk-go-v2/service/ecr v1.36.6\n\tgithub.com/aws/aws-sdk-go-v2/service/ecs v1.52.0\n\tgithub.com/aws/aws-sdk-go-v2/service/iam v1.38.1\n\tgithub.com/aws/aws-sdk-go-v2/service/kms v1.37.6\n\tgithub.com/aws/aws-sdk-go-v2/service/lambda v1.69.0\n\tgithub.com/aws/aws-sdk-go-v2/service/rds v1.91.0\n\tgithub.com/aws/aws-sdk-go-v2/service/route53 v1.46.2\n\tgithub.com/aws/aws-sdk-go-v2/service/s3 v1.69.0\n\tgithub.com/aws/aws-sdk-go-v2/service/secretsmanager v1.34.6\n\tgithub.com/aws/aws-sdk-go-v2/service/sns v1.33.6\n\tgithub.com/aws/aws-sdk-go-v2/service/sqs v1.37.1\n\tgithub.com/aws/aws-sdk-go-v2/service/ssm v1.56.0\n\tgithub.com/aws/aws-sdk-go-v2/service/sts v1.33.1\n\tgithub.com/denisenkom/go-mssqldb v0.12.3\n\tgithub.com/gonvenience/ytbx v1.4.4\n\tgithub.com/hashicorp/go-getter/v2 v2.2.3\n\tgithub.com/homeport/dyff v1.6.0\n\tgithub.com/jackc/pgx/v5 v5.7.1\n\tgithub.com/lib/pq v1.10.9\n\tgithub.com/slack-go/slack v0.15.0\n\tgopkg.in/yaml.v3 v3.0.1\n\tgotest.tools/v3 v3.5.1\n)\n\nrequire (\n\tcel.dev/expr v0.25.1 // indirect\n\tcloud.google.com/go/auth v0.10.2 // indirect\n\tcloud.google.com/go/auth/oauth2adapt v0.2.5 // indirect\n\tcloud.google.com/go/compute/metadata v0.9.0 // indirect\n\tcloud.google.com/go/iam v1.2.2 // indirect\n\tcloud.google.com/go/longrunning v0.6.2 // indirect\n\tcloud.google.com/go/monitoring v1.21.2 // indirect\n\tfilippo.io/edwards25519 v1.1.1 // indirect\n\tgithub.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 // indirect\n\tgithub.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0 // indirect\n\tgithub.com/Azure/go-autorest v14.2.0+incompatible // indirect\n\tgithub.com/Azure/go-autorest/autorest/adal v0.9.13 // indirect\n\tgithub.com/Azure/go-autorest/autorest/azure/cli v0.4.2 // indirect\n\tgithub.com/Azure/go-autorest/autorest/date v0.3.0 // indirect\n\tgithub.com/Azure/go-autorest/logger v0.2.1 // indirect\n\tgithub.com/Azure/go-autorest/tracing v0.6.0 // indirect\n\tgithub.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 // indirect\n\tgithub.com/BurntSushi/toml v1.4.0 // indirect\n\tgithub.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 // indirect\n\tgithub.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.48.1 // indirect\n\tgithub.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.48.1 // indirect\n\tgithub.com/agext/levenshtein v1.2.3 // indirect\n\tgithub.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.7 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.20 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/internal/configsources v1.3.24 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.24 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/internal/v4a v1.3.24 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.5 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.5 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.5 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.5 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/sso v1.24.6 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.5 // indirect\n\tgithub.com/aws/smithy-go v1.22.1 // indirect\n\tgithub.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect\n\tgithub.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect\n\tgithub.com/cespare/xxhash/v2 v2.3.0 // indirect\n\tgithub.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 // indirect\n\tgithub.com/containerd/stargz-snapshotter/estargz v0.14.3 // indirect\n\tgithub.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect\n\tgithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect\n\tgithub.com/dimchansky/utfbom v1.1.1 // indirect\n\tgithub.com/docker/cli v29.2.0+incompatible // indirect\n\tgithub.com/docker/distribution v2.8.2+incompatible // indirect\n\tgithub.com/docker/docker-credential-helpers v0.7.0 // indirect\n\tgithub.com/emicklei/go-restful/v3 v3.12.2 // indirect\n\tgithub.com/envoyproxy/go-control-plane/envoy v1.36.0 // indirect\n\tgithub.com/envoyproxy/protoc-gen-validate v1.3.0 // indirect\n\tgithub.com/felixge/httpsnoop v1.0.4 // indirect\n\tgithub.com/form3tech-oss/jwt-go v3.2.2+incompatible // indirect\n\tgithub.com/fxamacker/cbor/v2 v2.9.0 // indirect\n\tgithub.com/go-jose/go-jose/v4 v4.1.3 // indirect\n\tgithub.com/go-logr/logr v1.4.3 // indirect\n\tgithub.com/go-logr/stdr v1.2.2 // indirect\n\tgithub.com/go-openapi/jsonpointer v0.21.0 // indirect\n\tgithub.com/go-openapi/jsonreference v0.20.2 // indirect\n\tgithub.com/go-openapi/swag v0.23.0 // indirect\n\tgithub.com/golang-jwt/jwt/v5 v5.2.2 // indirect\n\tgithub.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe // indirect\n\tgithub.com/golang-sql/sqlexp v0.1.0 // indirect\n\tgithub.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect\n\tgithub.com/gonvenience/bunt v1.3.5 // indirect\n\tgithub.com/gonvenience/neat v1.3.12 // indirect\n\tgithub.com/gonvenience/term v1.0.2 // indirect\n\tgithub.com/gonvenience/text v1.0.7 // indirect\n\tgithub.com/gonvenience/wrap v1.1.2 // indirect\n\tgithub.com/google/gnostic-models v0.7.0 // indirect\n\tgithub.com/google/go-cmp v0.7.0 // indirect\n\tgithub.com/google/s2a-go v0.1.8 // indirect\n\tgithub.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect\n\tgithub.com/googleapis/gax-go/v2 v2.14.0 // indirect\n\tgithub.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect\n\tgithub.com/hashicorp/errwrap v1.0.0 // indirect\n\tgithub.com/hashicorp/go-cleanhttp v0.5.2 // indirect\n\tgithub.com/hashicorp/go-safetemp v1.0.0 // indirect\n\tgithub.com/jackc/pgpassfile v1.0.0 // indirect\n\tgithub.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect\n\tgithub.com/jackc/puddle/v2 v2.2.2 // indirect\n\tgithub.com/jmespath/go-jmespath v0.4.0 // indirect\n\tgithub.com/josharian/intern v1.0.0 // indirect\n\tgithub.com/json-iterator/go v1.1.12 // indirect\n\tgithub.com/klauspost/compress v1.16.5 // indirect\n\tgithub.com/kylelemons/godebug v1.1.0 // indirect\n\tgithub.com/lucasb-eyer/go-colorful v1.2.0 // indirect\n\tgithub.com/mailru/easyjson v0.7.7 // indirect\n\tgithub.com/mattn/go-ciede2000 v0.0.0-20170301095244-782e8c62fec3 // indirect\n\tgithub.com/mattn/go-isatty v0.0.19 // indirect\n\tgithub.com/mitchellh/go-ps v1.0.0 // indirect\n\tgithub.com/mitchellh/go-testing-interface v1.14.1 // indirect\n\tgithub.com/mitchellh/go-wordwrap v1.0.1 // indirect\n\tgithub.com/mitchellh/hashstructure v1.1.0 // indirect\n\tgithub.com/moby/spdystream v0.5.0 // indirect\n\tgithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect\n\tgithub.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect\n\tgithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect\n\tgithub.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect\n\tgithub.com/opencontainers/go-digest v1.0.0 // indirect\n\tgithub.com/opencontainers/image-spec v1.1.0-rc3 // indirect\n\tgithub.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect\n\tgithub.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect\n\tgithub.com/russross/blackfriday/v2 v2.1.0 // indirect\n\tgithub.com/sergi/go-diff v1.3.1 // indirect\n\tgithub.com/spf13/pflag v1.0.9 // indirect\n\tgithub.com/spiffe/go-spiffe/v2 v2.6.0 // indirect\n\tgithub.com/texttheater/golang-levenshtein v1.0.1 // indirect\n\tgithub.com/ulikunitz/xz v0.5.10 // indirect\n\tgithub.com/vbatts/tar-split v0.11.3 // indirect\n\tgithub.com/virtuald/go-ordered-json v0.0.0-20170621173500-b18e6e673d74 // indirect\n\tgithub.com/x448/float16 v0.8.4 // indirect\n\tgo.opencensus.io v0.24.0 // indirect\n\tgo.opentelemetry.io/auto/sdk v1.2.1 // indirect\n\tgo.opentelemetry.io/contrib/detectors/gcp v1.39.0 // indirect\n\tgo.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 // indirect\n\tgo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect\n\tgo.opentelemetry.io/otel v1.40.0 // indirect\n\tgo.opentelemetry.io/otel/metric v1.40.0 // indirect\n\tgo.opentelemetry.io/otel/sdk v1.40.0 // indirect\n\tgo.opentelemetry.io/otel/sdk/metric v1.40.0 // indirect\n\tgo.opentelemetry.io/otel/trace v1.40.0 // indirect\n\tgo.yaml.in/yaml/v2 v2.4.3 // indirect\n\tgo.yaml.in/yaml/v3 v3.0.4 // indirect\n\tgolang.org/x/mod v0.30.0 // indirect\n\tgolang.org/x/sync v0.19.0 // indirect\n\tgolang.org/x/sys v0.40.0 // indirect\n\tgolang.org/x/term v0.38.0 // indirect\n\tgolang.org/x/text v0.32.0 // indirect\n\tgolang.org/x/time v0.9.0 // indirect\n\tgolang.org/x/tools v0.39.0 // indirect\n\tgoogle.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect\n\tgoogle.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect\n\tgoogle.golang.org/grpc v1.79.3 // indirect\n\tgoogle.golang.org/protobuf v1.36.10 // indirect\n\tgopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect\n\tgopkg.in/inf.v0 v0.9.1 // indirect\n\tgopkg.in/yaml.v2 v2.4.0 // indirect\n\tk8s.io/klog/v2 v2.130.1 // indirect\n\tk8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect\n\tk8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect\n\tsigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect\n\tsigs.k8s.io/randfill v1.0.0 // indirect\n\tsigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect\n\tsigs.k8s.io/yaml v1.6.0 // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=\ncel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=\ncloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.116.0 h1:B3fRrSDkLRt5qSHWe40ERJvhvnQwdZiHu0bJOpldweE=\ncloud.google.com/go v0.116.0/go.mod h1:cEPSRWPzZEswwdr9BxE6ChEn01dWlTaF05LiC2Xs70U=\ncloud.google.com/go/auth v0.10.2 h1:oKF7rgBfSHdp/kuhXtqU/tNDr0mZqhYbEh+6SiqzkKo=\ncloud.google.com/go/auth v0.10.2/go.mod h1:xxA5AqpDrvS+Gkmo9RqrGGRh6WSNKKOXhY3zNOr38tI=\ncloud.google.com/go/auth/oauth2adapt v0.2.5 h1:2p29+dePqsCHPP1bqDJcKj4qxRyYCcbzKpFyKGt3MTk=\ncloud.google.com/go/auth/oauth2adapt v0.2.5/go.mod h1:AlmsELtlEBnaNTL7jCj8VQFLy6mbZv0s4Q7NGBeQ5E8=\ncloud.google.com/go/cloudbuild v1.19.0 h1:Uo0bL251yvyWsNtO3Og9m5Z4S48cgGf3IUX7xzOcl8s=\ncloud.google.com/go/cloudbuild v1.19.0/go.mod h1:ZGRqbNMrVGhknIIjwASa6MqoRTOpXIVMSI+Ew5DMPuY=\ncloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=\ncloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=\ncloud.google.com/go/iam v1.2.2 h1:ozUSofHUGf/F4tCNy/mu9tHLTaxZFLOUiKzjcgWHGIA=\ncloud.google.com/go/iam v1.2.2/go.mod h1:0Ys8ccaZHdI1dEUilwzqng/6ps2YB6vRsjIe00/+6JY=\ncloud.google.com/go/logging v1.12.0 h1:ex1igYcGFd4S/RZWOCU51StlIEuey5bjqwH9ZYjHibk=\ncloud.google.com/go/logging v1.12.0/go.mod h1:wwYBt5HlYP1InnrtYI0wtwttpVU1rifnMT7RejksUAM=\ncloud.google.com/go/longrunning v0.6.2 h1:xjDfh1pQcWPEvnfjZmwjKQEcHnpz6lHjfy7Fo0MK+hc=\ncloud.google.com/go/longrunning v0.6.2/go.mod h1:k/vIs83RN4bE3YCswdXC5PFfWVILjm3hpEUlSko4PiI=\ncloud.google.com/go/monitoring v1.21.2 h1:FChwVtClH19E7pJ+e0xUhJPGksctZNVOk2UhMmblmdU=\ncloud.google.com/go/monitoring v1.21.2/go.mod h1:hS3pXvaG8KgWTSz+dAdyzPrGUYmi2Q+WFX8g2hqVEZU=\ncloud.google.com/go/storage v1.47.0 h1:ajqgt30fnOMmLfWfu1PWcb+V9Dxz6n+9WKjdNg5R4HM=\ncloud.google.com/go/storage v1.47.0/go.mod h1:Ks0vP374w0PW6jOUameJbapbQKXqkjGd/OJRp2fb9IQ=\ncloud.google.com/go/trace v1.11.2 h1:4ZmaBdL8Ng/ajrgKqY5jfvzqMXbrDcBsUGXOT9aqTtI=\ncloud.google.com/go/trace v1.11.2/go.mod h1:bn7OwXd4pd5rFuAnTrzBuoZ4ax2XQeG3qNgYmfCy0Io=\nfilippo.io/edwards25519 v1.1.1 h1:YpjwWWlNmGIDyXOn8zLzqiD+9TyIlPhGFG96P39uBpw=\nfilippo.io/edwards25519 v1.1.1/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=\ngithub.com/Azure/azure-sdk-for-go v51.0.0+incompatible h1:p7blnyJSjJqf5jflHbSGhIhEpXIgIFmYZNg5uwqweso=\ngithub.com/Azure/azure-sdk-for-go v51.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=\ngithub.com/Azure/azure-sdk-for-go/sdk/azcore v0.19.0/go.mod h1:h6H6c8enJmmocHUbLiiGY6sx7f9i+X3m1CHdd5c6Rdw=\ngithub.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 h1:Gt0j3wceWMwPmiazCa8MzMA0MfhmPIz0Qp0FJ6qcM0U=\ngithub.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM=\ngithub.com/Azure/azure-sdk-for-go/sdk/azidentity v0.11.0/go.mod h1:HcM1YX14R7CJcghJGOYCgdezslRSVzqwLf/q+4Y2r/0=\ngithub.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 h1:B+blDbyVIG3WaikNxPnhPiJ1MThR03b3vKGtER95TP4=\ngithub.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1/go.mod h1:JdM5psgjfBf5fo2uWOZhflPWyDBZ/O/CNAH9CtsuZE4=\ngithub.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY=\ngithub.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8=\ngithub.com/Azure/azure-sdk-for-go/sdk/internal v0.7.0/go.mod h1:yqy467j36fJxcRV2TzfVZ1pCb5vxm4BtZPUdYWe/Xo8=\ngithub.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 h1:FPKJS1T+clwv+OLGt13a8UjqeRuh0O4SJ3lUriThc+4=\ngithub.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1/go.mod h1:j2chePtV91HrC22tGoRX3sGY42uF13WzmmV80/OdVAA=\ngithub.com/Azure/azure-sdk-for-go/sdk/resourcemanager/appcontainers/armappcontainers/v3 v3.0.0 h1:NYYoOOPGOqUXw/bGIVd6OY/K8J23a18IAlAx1tOHWNo=\ngithub.com/Azure/azure-sdk-for-go/sdk/resourcemanager/appcontainers/armappcontainers/v3 v3.0.0/go.mod h1:LDN3sr8FJ36sY6ZmMes6Q2vHJ+5r1aFsE3wEo7VbXJg=\ngithub.com/Azure/azure-sdk-for-go/sdk/resourcemanager/appservice/armappservice/v2 v2.3.0 h1:JI8PcWOImyvIUEZ0Bbmfe05FOlWkMi2KhjG+cAKaUms=\ngithub.com/Azure/azure-sdk-for-go/sdk/resourcemanager/appservice/armappservice/v2 v2.3.0/go.mod h1:nJLFPGJkyKfDDyJiPuHIXsCi/gpJkm07EvRgiX7SGlI=\ngithub.com/Azure/azure-sdk-for-go/sdk/resourcemanager/datafactory/armdatafactory/v9 v9.1.0 h1:82oTC4oB/7AjVmPR8KMvlyHZgZ8PGdboh8c0Jol/XWY=\ngithub.com/Azure/azure-sdk-for-go/sdk/resourcemanager/datafactory/armdatafactory/v9 v9.1.0/go.mod h1:nuDWiSqiFv4Bo8LX99dl+Ecl9o1iNSLJDBsrl8iRWr4=\ngithub.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v2 v2.0.0 h1:PTFGRSlMKCQelWwxUyYVEUqseBJVemLyqWJjvMyt0do=\ngithub.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v2 v2.0.0/go.mod h1:LRr2FzBTQlONPPa5HREE5+RjSCTXl7BwOvYOaWTqCaI=\ngithub.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v3 v3.1.0 h1:2qsIIvxVT+uE6yrNldntJKlLRgxGbZ85kgtz5SNBhMw=\ngithub.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v3 v3.1.0/go.mod h1:AW8VEadnhw9xox+VaVd9sP7NjzOAnaZBLRH6Tq3cJ38=\ngithub.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault v1.5.0 h1:nnQ9vXH039UrEFxi08pPuZBE7VfqSJt343uJLw0rhWI=\ngithub.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault v1.5.0/go.mod h1:4YIVtzMFVsPwBvitCDX7J9sqthSj43QD1sP6fYc1egc=\ngithub.com/Azure/azure-sdk-for-go/sdk/resourcemanager/managementgroups/armmanagementgroups v1.0.0 h1:pPvTJ1dY0sA35JOeFq6TsY2xj6Z85Yo23Pj4wCCvu4o=\ngithub.com/Azure/azure-sdk-for-go/sdk/resourcemanager/managementgroups/armmanagementgroups v1.0.0/go.mod h1:mLfWfj8v3jfWKsL9G4eoBoXVcsqcIUTapmdKy7uGOp0=\ngithub.com/Azure/azure-sdk-for-go/sdk/resourcemanager/mysql/armmysql v1.2.0 h1:dhywcZH9yPDIje9aTqwy6psZSPzI6CJLYEprDahIBSQ=\ngithub.com/Azure/azure-sdk-for-go/sdk/resourcemanager/mysql/armmysql v1.2.0/go.mod h1:6z3b+JdBLH0eMzfBex/cvEIoEFVEwXuB0wbgdfN11iM=\ngithub.com/Azure/azure-sdk-for-go/sdk/resourcemanager/postgresql/armpostgresql v1.2.0 h1:0hXKrsbh2M6CQyW0TDC9Bsyd99vQmrOxiBTUfQHZjPA=\ngithub.com/Azure/azure-sdk-for-go/sdk/resourcemanager/postgresql/armpostgresql v1.2.0/go.mod h1:bvZZor36Jg9q9kouuMyfJ+ay77+qK+YUfThXH1FdXjU=\ngithub.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0 h1:Dd+RhdJn0OTtVGaeDLZpcumkIVCtA/3/Fo42+eoYvVM=\ngithub.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0/go.mod h1:5kakwfW5CjC9KK+Q4wjXAg+ShuIm2mBMua0ZFj2C8PE=\ngithub.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql v1.2.0 h1:S087deZ0kP1RUg4pU7w9U9xpUedTCbOtz+mnd0+hrkQ=\ngithub.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql v1.2.0/go.mod h1:B4cEyXrWBmbfMDAPnpJ1di7MAt5DKP57jPEObAvZChg=\ngithub.com/Azure/azure-sdk-for-go/sdk/resourcemanager/synapse/armsynapse v0.8.0 h1:IKCilT2DdxjeCXhiCIZb5hywpA1KDGKwpdA1WL20wT0=\ngithub.com/Azure/azure-sdk-for-go/sdk/resourcemanager/synapse/armsynapse v0.8.0/go.mod h1:IzuvA34YNVnlifc1+KhCouAKEf1VYzV439FOpyfTHzA=\ngithub.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azcertificates v1.4.0 h1:mtvR5ZXH5Ew6PSONd5lO5OXovWP1E3oAlgC8fpxor2Q=\ngithub.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azcertificates v1.4.0/go.mod h1:u560+RFVfG0CBPzkXlDW43slESbBAQjgDGi3r6z+wk8=\ngithub.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.4.0 h1:E4MgwLBGeVB5f2MdcIVD3ELVAWpr+WD6MUe1i+tM/PA=\ngithub.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.4.0/go.mod h1:Y2b/1clN4zsAoUd/pgNAQHjLDnTis/6ROkUfyob6psM=\ngithub.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.4.0 h1:/g8S6wk65vfC6m3FIxJ+i5QDyN9JWwXI8Hb0Img10hU=\ngithub.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.4.0/go.mod h1:gpl+q95AzZlKVI3xSoseF9QPrypk0hQqBiJYeB/cR/I=\ngithub.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0 h1:nCYfgcSyHZXJI8J0IWE5MsCGlb2xp9fJiXyxWgmOFg4=\ngithub.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0/go.mod h1:ucUjca2JtSZboY8IoUqyQyuuXvwbMBVwFOm0vdQPNhA=\ngithub.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs=\ngithub.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=\ngithub.com/Azure/go-autorest/autorest v0.11.17/go.mod h1:eipySxLmqSyC5s5k1CLupqet0PSENBEDP93LQ9a8QYw=\ngithub.com/Azure/go-autorest/autorest v0.11.20 h1:s8H1PbCZSqg/DH7JMlOz6YMig6htWLNPsjDdlLqCx3M=\ngithub.com/Azure/go-autorest/autorest v0.11.20/go.mod h1:o3tqFY+QR40VOlk+pV4d77mORO64jOXSgEnPQgLK6JY=\ngithub.com/Azure/go-autorest/autorest/adal v0.9.5/go.mod h1:B7KF7jKIeC9Mct5spmyCB/A8CG/sEz1vwIRGv/bbw7A=\ngithub.com/Azure/go-autorest/autorest/adal v0.9.11/go.mod h1:nBKAnTomx8gDtl+3ZCJv2v0KACFHWTB2drffI1B68Pk=\ngithub.com/Azure/go-autorest/autorest/adal v0.9.13 h1:Mp5hbtOePIzM8pJVRa3YLrWWmZtoxRXqUEzCfJt3+/Q=\ngithub.com/Azure/go-autorest/autorest/adal v0.9.13/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M=\ngithub.com/Azure/go-autorest/autorest/azure/auth v0.5.8 h1:TzPg6B6fTZ0G1zBf3T54aI7p3cAT6u//TOXGPmFMOXg=\ngithub.com/Azure/go-autorest/autorest/azure/auth v0.5.8/go.mod h1:kxyKZTSfKh8OVFWPAgOgQ/frrJgeYQJPyR5fLFmXko4=\ngithub.com/Azure/go-autorest/autorest/azure/cli v0.4.2 h1:dMOmEJfkLKW/7JsokJqkyoYSgmR08hi9KrhjZb+JALY=\ngithub.com/Azure/go-autorest/autorest/azure/cli v0.4.2/go.mod h1:7qkJkT+j6b+hIpzMOwPChJhTqS8VbsqqgULzMNRugoM=\ngithub.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw=\ngithub.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74=\ngithub.com/Azure/go-autorest/autorest/mocks v0.4.1 h1:K0laFcLE6VLTOwNgSxaGbUcLPuGXlNkbVvq4cW4nIHk=\ngithub.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k=\ngithub.com/Azure/go-autorest/autorest/to v0.4.0 h1:oXVqrxakqqV1UZdSazDOPOLvOIz+XA683u8EctwboHk=\ngithub.com/Azure/go-autorest/autorest/to v0.4.0/go.mod h1:fE8iZBn7LQR7zH/9XU2NcPR4o9jEImooCeWJcYV/zLE=\ngithub.com/Azure/go-autorest/autorest/validation v0.3.1 h1:AgyqjAd94fwNAoTjl/WQXg4VvFeRFpO+UhNyRXqF1ac=\ngithub.com/Azure/go-autorest/autorest/validation v0.3.1/go.mod h1:yhLgjC0Wda5DYXl6JAsWyUe4KVNffhoDhG0zVzUMo3E=\ngithub.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8=\ngithub.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+ZtXWSmf4Tg=\ngithub.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8=\ngithub.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo=\ngithub.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=\ngithub.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM=\ngithub.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE=\ngithub.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs=\ngithub.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=\ngithub.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=\ngithub.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=\ngithub.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=\ngithub.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=\ngithub.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 h1:sBEjpZlNHzK1voKq9695PJSX2o5NEXl7/OL3coiIY0c=\ngithub.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0=\ngithub.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.48.1 h1:UQ0AhxogsIRZDkElkblfnwjc3IaltCm2HUMvezQaL7s=\ngithub.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.48.1/go.mod h1:jyqM3eLpJ3IbIFDTKVz2rF9T/xWGW0rIriGwnz8l9Tk=\ngithub.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.48.1 h1:oTX4vsorBZo/Zdum6OKPA4o7544hm6smoRv1QjpTwGo=\ngithub.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.48.1/go.mod h1:0wEl7vrAD8mehJyohS9HZy+WyEOaQO2mJx86Cvh93kM=\ngithub.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.48.1 h1:8nn+rsCvTq9axyEh382S0PFLBeaFwNsT43IrPWzctRU=\ngithub.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.48.1/go.mod h1:viRWSEhtMZqz1rhwmOVKkWl6SwmVowfL9O2YR5gI2PE=\ngithub.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=\ngithub.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=\ngithub.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo=\ngithub.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=\ngithub.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY=\ngithub.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4=\ngithub.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=\ngithub.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=\ngithub.com/aws/aws-lambda-go v1.47.0 h1:0H8s0vumYx/YKs4sE7YM0ktwL2eWse+kfopsRI1sXVI=\ngithub.com/aws/aws-lambda-go v1.47.0/go.mod h1:dpMpZgvWx5vuQJfBt0zqBha60q7Dd7RfgJv23DymV8A=\ngithub.com/aws/aws-sdk-go-v2 v1.32.5 h1:U8vdWJuY7ruAkzaOdD7guwJjD06YSKmnKCJs7s3IkIo=\ngithub.com/aws/aws-sdk-go-v2 v1.32.5/go.mod h1:P5WJBrYqqbWVaOxgH0X/FYYD47/nooaPOZPlQdmiN2U=\ngithub.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.7 h1:lL7IfaFzngfx0ZwUGOZdsFFnQ5uLvR0hWqqhyE7Q9M8=\ngithub.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.7/go.mod h1:QraP0UcVlQJsmHfioCrveWOC1nbiWUl3ej08h4mXWoc=\ngithub.com/aws/aws-sdk-go-v2/config v1.28.5 h1:Za41twdCXbuyyWv9LndXxZZv3QhTG1DinqlFsSuvtI0=\ngithub.com/aws/aws-sdk-go-v2/config v1.28.5/go.mod h1:4VsPbHP8JdcdUDmbTVgNL/8w9SqOkM5jyY8ljIxLO3o=\ngithub.com/aws/aws-sdk-go-v2/credentials v1.17.46 h1:AU7RcriIo2lXjUfHFnFKYsLCwgbz1E7Mm95ieIRDNUg=\ngithub.com/aws/aws-sdk-go-v2/credentials v1.17.46/go.mod h1:1FmYyLGL08KQXQ6mcTlifyFXfJVCNJTVGuQP4m0d/UA=\ngithub.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.20 h1:sDSXIrlsFSFJtWKLQS4PUWRvrT580rrnuLydJrCQ/yA=\ngithub.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.20/go.mod h1:WZ/c+w0ofps+/OUqMwWgnfrgzZH1DZO1RIkktICsqnY=\ngithub.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.41 h1:hqcxMc2g/MwwnRMod9n6Bd+t+9Nf7d5qRg7RaXKPd6o=\ngithub.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.41/go.mod h1:d1eH0VrttvPmrCraU68LOyNdu26zFxQFjrVSb5vdhog=\ngithub.com/aws/aws-sdk-go-v2/internal/configsources v1.3.24 h1:4usbeaes3yJnCFC7kfeyhkdkPtoRYPa/hTmCqMpKpLI=\ngithub.com/aws/aws-sdk-go-v2/internal/configsources v1.3.24/go.mod h1:5CI1JemjVwde8m2WG3cz23qHKPOxbpkq0HaoreEgLIY=\ngithub.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.24 h1:N1zsICrQglfzaBnrfM0Ys00860C+QFwu6u/5+LomP+o=\ngithub.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.24/go.mod h1:dCn9HbJ8+K31i8IQ8EWmWj0EiIk0+vKiHNMxTTYveAg=\ngithub.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ=\ngithub.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc=\ngithub.com/aws/aws-sdk-go-v2/internal/v4a v1.3.24 h1:JX70yGKLj25+lMC5Yyh8wBtvB01GDilyRuJvXJ4piD0=\ngithub.com/aws/aws-sdk-go-v2/internal/v4a v1.3.24/go.mod h1:+Ln60j9SUTD0LEwnhEB0Xhg61DHqplBrbZpLgyjoEHg=\ngithub.com/aws/aws-sdk-go-v2/service/acm v1.30.6 h1:fDg0RlN30Xf/yYzEUL/WXqhmgFsjVb/I3230oCfyI5w=\ngithub.com/aws/aws-sdk-go-v2/service/acm v1.30.6/go.mod h1:zRR6jE3v/TcbfO8C2P+H0Z+kShiKKVaVyoIl8NQRjyg=\ngithub.com/aws/aws-sdk-go-v2/service/autoscaling v1.51.0 h1:1KzQVZi7OTixxaVJ8fWaJAUBjme+iQ3zBOCZhE4RgxQ=\ngithub.com/aws/aws-sdk-go-v2/service/autoscaling v1.51.0/go.mod h1:I1+/2m+IhnK5qEbhS3CrzjeiVloo9sItE/2K+so0fkU=\ngithub.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.44.0 h1:OREVd94+oXW5a+3SSUAo4K0L5ci8cucCLu+PSiek8OU=\ngithub.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.44.0/go.mod h1:Qbr4yfpNqVNl69l/GEDK+8wxLf/vHi0ChoiSDzD7thU=\ngithub.com/aws/aws-sdk-go-v2/service/dynamodb v1.37.1 h1:vucMirlM6D+RDU8ncKaSZ/5dGrXNajozVwpmWNPn2gQ=\ngithub.com/aws/aws-sdk-go-v2/service/dynamodb v1.37.1/go.mod h1:fceORfs010mNxZbQhfqUjUeHlTwANmIT4mvHamuUaUg=\ngithub.com/aws/aws-sdk-go-v2/service/ec2 v1.193.0 h1:RhSoBFT5/8tTmIseJUXM6INTXTQDF8+0oyxWBnozIms=\ngithub.com/aws/aws-sdk-go-v2/service/ec2 v1.193.0/go.mod h1:mzj8EEjIHSN2oZRXiw1Dd+uB4HZTl7hC8nBzX9IZMWw=\ngithub.com/aws/aws-sdk-go-v2/service/ecr v1.36.6 h1:zg+3FGHA0PBs0KM25qE/rOf2o5zsjNa1g/Qq83+SDI0=\ngithub.com/aws/aws-sdk-go-v2/service/ecr v1.36.6/go.mod h1:ZSq54Z9SIsOTf1Efwgw1msilSs4XVEfVQiP9nYVnKpM=\ngithub.com/aws/aws-sdk-go-v2/service/ecs v1.52.0 h1:7/vgFWplkusJN/m+3QOa+W9FNRqa8ujMPNmdufRaJpg=\ngithub.com/aws/aws-sdk-go-v2/service/ecs v1.52.0/go.mod h1:dPTOvmjJQ1T7Q+2+Xs2KSPrMvx+p0rpyV+HsQVnUK4o=\ngithub.com/aws/aws-sdk-go-v2/service/iam v1.38.1 h1:hfkzDZHBp9jAT4zcd5mtqckpU4E3Ax0LQaEWWk1VgN8=\ngithub.com/aws/aws-sdk-go-v2/service/iam v1.38.1/go.mod h1:u36ahDtZcQHGmVm/r+0L1sfKX4fzLEMdCqiKRKkUMVM=\ngithub.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 h1:iXtILhvDxB6kPvEXgsDhGaZCSC6LQET5ZHSdJozeI0Y=\ngithub.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1/go.mod h1:9nu0fVANtYiAePIBh2/pFUSwtJ402hLnp854CNoDOeE=\ngithub.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.5 h1:gvZOjQKPxFXy1ft3QnEyXmT+IqneM9QAUWlM3r0mfqw=\ngithub.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.5/go.mod h1:DLWnfvIcm9IET/mmjdxeXbBKmTCm0ZB8p1za9BVteM8=\ngithub.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.5 h1:3Y457U2eGukmjYjeHG6kanZpDzJADa2m0ADqnuePYVQ=\ngithub.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.5/go.mod h1:CfwEHGkTjYZpkQ/5PvcbEtT7AJlG68KkEvmtwU8z3/U=\ngithub.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.5 h1:wtpJ4zcwrSbwhECWQoI/g6WM9zqCcSpHDJIWSbMLOu4=\ngithub.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.5/go.mod h1:qu/W9HXQbbQ4+1+JcZp0ZNPV31ym537ZJN+fiS7Ti8E=\ngithub.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.5 h1:P1doBzv5VEg1ONxnJss1Kh5ZG/ewoIE4MQtKKc6Crgg=\ngithub.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.5/go.mod h1:NOP+euMW7W3Ukt28tAxPuoWao4rhhqJD3QEBk7oCg7w=\ngithub.com/aws/aws-sdk-go-v2/service/kms v1.37.6 h1:CZImQdb1QbU9sGgJ9IswhVkxAcjkkD1eQTMA1KHWk+E=\ngithub.com/aws/aws-sdk-go-v2/service/kms v1.37.6/go.mod h1:YJDdlK0zsyxVBxGU48AR/Mi8DMrGdc1E3Yij4fNrONA=\ngithub.com/aws/aws-sdk-go-v2/service/lambda v1.69.0 h1:BXt75frE/FYtAmEDBJRBa2HexOw+oAZWZl6QknZEFgg=\ngithub.com/aws/aws-sdk-go-v2/service/lambda v1.69.0/go.mod h1:guz2K3x4FKSdDaoeB+TPVgJNU9oj2gftbp5cR8ela1A=\ngithub.com/aws/aws-sdk-go-v2/service/rds v1.91.0 h1:eqHz3Uih+gb0vLE5Cc4Xf733vOxsxDp6GFUUVQU4d7w=\ngithub.com/aws/aws-sdk-go-v2/service/rds v1.91.0/go.mod h1:h2jc7IleH3xHY7y+h8FH7WAZcz3IVLOB6/jXotIQ/qU=\ngithub.com/aws/aws-sdk-go-v2/service/route53 v1.46.2 h1:wmt05tPp/CaRZpPV5B4SaJ5TwkHKom07/BzHoLdkY1o=\ngithub.com/aws/aws-sdk-go-v2/service/route53 v1.46.2/go.mod h1:d+K9HESMpGb1EU9/UmmpInbGIUcAkwmcY6ZO/A3zZsw=\ngithub.com/aws/aws-sdk-go-v2/service/s3 v1.69.0 h1:Q2ax8S21clKOnHhhr933xm3JxdJebql+R7aNo7p7GBQ=\ngithub.com/aws/aws-sdk-go-v2/service/s3 v1.69.0/go.mod h1:ralv4XawHjEMaHOWnTFushl0WRqim/gQWesAMF6hTow=\ngithub.com/aws/aws-sdk-go-v2/service/secretsmanager v1.34.6 h1:1KDMKvOKNrpD667ORbZ/+4OgvUoaok1gg/MLzrHF9fw=\ngithub.com/aws/aws-sdk-go-v2/service/secretsmanager v1.34.6/go.mod h1:DmtyfCfONhOyVAJ6ZMTrDSFIeyCBlEO93Qkfhxwbxu0=\ngithub.com/aws/aws-sdk-go-v2/service/sns v1.33.6 h1:lEUtRHICiXsd7VRwRjXaY7MApT2X4Ue0Mrwe6XbyBro=\ngithub.com/aws/aws-sdk-go-v2/service/sns v1.33.6/go.mod h1:SODr0Lu3lFdT0SGsGX1TzFTapwveBrT5wztVoYtppm8=\ngithub.com/aws/aws-sdk-go-v2/service/sqs v1.37.1 h1:39WvSrVq9DD6UHkD+fx5x19P5KpRQfNdtgReDVNbelc=\ngithub.com/aws/aws-sdk-go-v2/service/sqs v1.37.1/go.mod h1:3gwPzC9LER/BTQdQZ3r6dUktb1rSjABF1D3Sr6nS7VU=\ngithub.com/aws/aws-sdk-go-v2/service/ssm v1.56.0 h1:mADKqoZaodipGgiZfuAjtlcr4IVBtXPZKVjkzUZCCYM=\ngithub.com/aws/aws-sdk-go-v2/service/ssm v1.56.0/go.mod h1:l9qF25TzH95FhcIak6e4vt79KE4I7M2Nf59eMUVjj6c=\ngithub.com/aws/aws-sdk-go-v2/service/sso v1.24.6 h1:3zu537oLmsPfDMyjnUS2g+F2vITgy5pB74tHI+JBNoM=\ngithub.com/aws/aws-sdk-go-v2/service/sso v1.24.6/go.mod h1:WJSZH2ZvepM6t6jwu4w/Z45Eoi75lPN7DcydSRtJg6Y=\ngithub.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.5 h1:K0OQAsDywb0ltlFrZm0JHPY3yZp/S9OaoLU33S7vPS8=\ngithub.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.5/go.mod h1:ORITg+fyuMoeiQFiVGoqB3OydVTLkClw/ljbblMq6Cc=\ngithub.com/aws/aws-sdk-go-v2/service/sts v1.33.1 h1:6SZUVRQNvExYlMLbHdlKB48x0fLbc2iVROyaNEwBHbU=\ngithub.com/aws/aws-sdk-go-v2/service/sts v1.33.1/go.mod h1:GqWyYCwLXnlUB1lOAXQyNSPqPLQJvmo8J0DWBzp9mtg=\ngithub.com/aws/smithy-go v1.22.1 h1:/HPHZQ0g7f4eUeK6HKglFz8uwVfZKgoI25rb/J+dnro=\ngithub.com/aws/smithy-go v1.22.1/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=\ngithub.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas=\ngithub.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4=\ngithub.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=\ngithub.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI=\ngithub.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=\ngithub.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=\ngithub.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=\ngithub.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=\ngithub.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=\ngithub.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w=\ngithub.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI=\ngithub.com/containerd/stargz-snapshotter/estargz v0.14.3 h1:OqlDCK3ZVUO6C3B/5FSkDwbkEETK84kQgEeFwDC+62k=\ngithub.com/containerd/stargz-snapshotter/estargz v0.14.3/go.mod h1:KY//uOCIkSuNAHhJogcZtrNHdKrA99/FCCRjE3HD36o=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=\ngithub.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/denisenkom/go-mssqldb v0.12.3 h1:pBSGx9Tq67pBOTLmxNuirNTeB8Vjmf886Kx+8Y+8shw=\ngithub.com/denisenkom/go-mssqldb v0.12.3/go.mod h1:k0mtMFOnU+AihqFxPMiF05rtiDrorD1Vrm1KEz5hxDo=\ngithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=\ngithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=\ngithub.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8=\ngithub.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U=\ngithub.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE=\ngithub.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=\ngithub.com/docker/cli v29.2.0+incompatible h1:9oBd9+YM7rxjZLfyMGxjraKBKE4/nVyvVfN4qNl9XRM=\ngithub.com/docker/cli v29.2.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=\ngithub.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8=\ngithub.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=\ngithub.com/docker/docker-credential-helpers v0.7.0 h1:xtCHsjxogADNZcdv1pKUHXryefjlVRqWqIhk/uXJp0A=\ngithub.com/docker/docker-credential-helpers v0.7.0/go.mod h1:rETQfLdHNT3foU5kuNkFR1R1V12OJRRO5lzt2D1b5X0=\ngithub.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU=\ngithub.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=\ngithub.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=\ngithub.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA=\ngithub.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU=\ngithub.com/envoyproxy/go-control-plane/envoy v1.36.0 h1:yg/JjO5E7ubRyKX3m07GF3reDNEnfOboJ0QySbH736g=\ngithub.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9OtKeEkQLTb+Lkz0k8v9W0Oxsv98=\ngithub.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI=\ngithub.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4=\ngithub.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=\ngithub.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4=\ngithub.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA=\ngithub.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=\ngithub.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=\ngithub.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=\ngithub.com/form3tech-oss/jwt-go v3.2.2+incompatible h1:TcekIExNqud5crz4xD2pavyTgWiPvpYe4Xau31I0PRk=\ngithub.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=\ngithub.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=\ngithub.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=\ngithub.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=\ngithub.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=\ngithub.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=\ngithub.com/go-errors/errors v1.0.2-0.20180813162953-d98b870cc4e0 h1:skJKxRtNmevLqnayafdLe2AsenqRupVmzZSqrvb5caU=\ngithub.com/go-errors/errors v1.0.2-0.20180813162953-d98b870cc4e0/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=\ngithub.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=\ngithub.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=\ngithub.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=\ngithub.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=\ngithub.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=\ngithub.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=\ngithub.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=\ngithub.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=\ngithub.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=\ngithub.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=\ngithub.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=\ngithub.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=\ngithub.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=\ngithub.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=\ngithub.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=\ngithub.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=\ngithub.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=\ngithub.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=\ngithub.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=\ngithub.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=\ngithub.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=\ngithub.com/go-test/deep v1.0.7 h1:/VSMRlnY/JSyqxQUzQLKVMAskpY/NZKFA5j2P+0pP2M=\ngithub.com/go-test/deep v1.0.7/go.mod h1:QV8Hv/iy04NyLBxAdO9njL0iVPN1S4d/A3NVv1V36o8=\ngithub.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=\ngithub.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=\ngithub.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY=\ngithub.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=\ngithub.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=\ngithub.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=\ngithub.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=\ngithub.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=\ngithub.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=\ngithub.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=\ngithub.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=\ngithub.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=\ngithub.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=\ngithub.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=\ngithub.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=\ngithub.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=\ngithub.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=\ngithub.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=\ngithub.com/gonvenience/bunt v1.3.5 h1:wSQquifvwEWtzn27k1ngLfeLaStyt0k1b/K6TrlCNAs=\ngithub.com/gonvenience/bunt v1.3.5/go.mod h1:7ApqkVBEWvX04oJ28Q2WeI/BvJM6VtukaJAU/q/pTs8=\ngithub.com/gonvenience/neat v1.3.12 h1:xwIyRbJcG9LgcDYys+HHLH9DqqHeQsUpS5CfBUeskbs=\ngithub.com/gonvenience/neat v1.3.12/go.mod h1:8OljAIgPelN0uPPO94VBqxK+Kz98d6ZFwHDg5o/PfkE=\ngithub.com/gonvenience/term v1.0.2 h1:qKa2RydbWIrabGjR/fegJwpW5m+JvUwFL8mLhHzDXn0=\ngithub.com/gonvenience/term v1.0.2/go.mod h1:wThTR+3MzWtWn7XGVW6qQ65uaVf8GHED98KmwpuEQeo=\ngithub.com/gonvenience/text v1.0.7 h1:YmIqmgTwxnACYCG59DykgMbomwteYyNhAmEUEJtPl14=\ngithub.com/gonvenience/text v1.0.7/go.mod h1:OAjH+mohRszffLY6OjgQcUXiSkbrIavooFpfIt1ZwAs=\ngithub.com/gonvenience/wrap v1.1.2 h1:xPKxNwL1HCguwyM+HlP/1CIuc9LRd7k8RodLwe9YTZA=\ngithub.com/gonvenience/wrap v1.1.2/go.mod h1:GiryBSXoI3BAAhbWD1cZVj7RZmtiu0ERi/6R6eJfslI=\ngithub.com/gonvenience/ytbx v1.4.4 h1:jQopwyaLsVGuwdxSiN4WkXjsEaFNPJ3V4lUj7eyEpzo=\ngithub.com/gonvenience/ytbx v1.4.4/go.mod h1:w37+MKCPcCMY/jpPNmEklD4xKqrOAVBO6kIWW2+uI6M=\ngithub.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=\ngithub.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=\ngithub.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=\ngithub.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngithub.com/google/go-containerregistry v0.20.2 h1:B1wPJ1SN/S7pB+ZAimcciVD+r+yV/l/DSArMxlbwseo=\ngithub.com/google/go-containerregistry v0.20.2/go.mod h1:z38EKdKh4h7IP2gSfUUqEvalZBqs6AoLeWfUy34nQC8=\ngithub.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=\ngithub.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc=\ngithub.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0=\ngithub.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=\ngithub.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=\ngithub.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM=\ngithub.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA=\ngithub.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw=\ngithub.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA=\ngithub.com/googleapis/gax-go/v2 v2.14.0 h1:f+jMrjBPl+DL9nI4IQzLUxMq7XrAqFYB7hBPqMNIe8o=\ngithub.com/googleapis/gax-go/v2 v2.14.0/go.mod h1:lhBCnjdLrWRaPvLWhmc8IS24m9mr07qSYnHncrgo+zk=\ngithub.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=\ngithub.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=\ngithub.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=\ngithub.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=\ngithub.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=\ngithub.com/gruntwork-io/go-commons v0.8.0 h1:k/yypwrPqSeYHevLlEDmvmgQzcyTwrlZGRaxEM6G0ro=\ngithub.com/gruntwork-io/go-commons v0.8.0/go.mod h1:gtp0yTtIBExIZp7vyIV9I0XQkVwiQZze678hvDXof78=\ngithub.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=\ngithub.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=\ngithub.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=\ngithub.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=\ngithub.com/hashicorp/go-getter/v2 v2.2.3 h1:6CVzhT0KJQHqd9b0pK3xSP0CM/Cv+bVhk+jcaRJ2pGk=\ngithub.com/hashicorp/go-getter/v2 v2.2.3/go.mod h1:hp5Yy0GMQvwWVUmwLs3ygivz1JSLI323hdIE9J9m7TY=\ngithub.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=\ngithub.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=\ngithub.com/hashicorp/go-safetemp v1.0.0 h1:2HR189eFNrjHQyENnQMMpCiBAsRxzbTMIgBhEyExpmo=\ngithub.com/hashicorp/go-safetemp v1.0.0/go.mod h1:oaerMy3BhqiTbVye6QuFhFtIceqFoDHxNAB65b+Rj1I=\ngithub.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=\ngithub.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=\ngithub.com/hashicorp/hcl/v2 v2.22.0 h1:hkZ3nCtqeJsDhPRFz5EA9iwcG1hNWGePOTw6oyul12M=\ngithub.com/hashicorp/hcl/v2 v2.22.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA=\ngithub.com/hashicorp/terraform-json v0.23.0 h1:sniCkExU4iKtTADReHzACkk8fnpQXrdD2xoR+lppBkI=\ngithub.com/hashicorp/terraform-json v0.23.0/go.mod h1:MHdXbBAbSg0GvzuWazEGKAn/cyNfIB7mN6y7KJN6y2c=\ngithub.com/homeport/dyff v1.6.0 h1:AN+ikld0Fy+qx34YE7655b/bpWuxS6cL9k852pE2GUc=\ngithub.com/homeport/dyff v1.6.0/go.mod h1:FlAOFYzeKvxmU5nTrnG+qrlJVWpsFew7pt8L99p5q8k=\ngithub.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=\ngithub.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=\ngithub.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=\ngithub.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=\ngithub.com/jackc/pgx/v5 v5.7.1 h1:x7SYsPBYDkHDksogeSmZZ5xzThcTgRz++I5E+ePFUcs=\ngithub.com/jackc/pgx/v5 v5.7.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA=\ngithub.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=\ngithub.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=\ngithub.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a h1:zPPuIq2jAWWPTrGt70eK/BSch+gFAGrNzecsoENgu2o=\ngithub.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a/go.mod h1:yL958EeXv8Ylng6IfnvG4oflryUi3vgA3xPs9hmII1s=\ngithub.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=\ngithub.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=\ngithub.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=\ngithub.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=\ngithub.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=\ngithub.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=\ngithub.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=\ngithub.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=\ngithub.com/jstemmer/go-junit-report v1.0.0 h1:8X1gzZpR+nVQLAht+L/foqOeX2l9DTZoaIPbEQHxsds=\ngithub.com/jstemmer/go-junit-report v1.0.0/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=\ngithub.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU=\ngithub.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k=\ngithub.com/klauspost/compress v1.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI=\ngithub.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=\ngithub.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=\ngithub.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=\ngithub.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=\ngithub.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=\ngithub.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=\ngithub.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=\ngithub.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=\ngithub.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=\ngithub.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=\ngithub.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=\ngithub.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=\ngithub.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=\ngithub.com/mattn/go-ciede2000 v0.0.0-20170301095244-782e8c62fec3 h1:BXxTozrOU8zgC5dkpn3J6NTRdoP+hjok/e+ACr4Hibk=\ngithub.com/mattn/go-ciede2000 v0.0.0-20170301095244-782e8c62fec3/go.mod h1:x1uk6vxTiVuNt6S5R2UYgdhpj3oKojXvOXauHZ7dEnI=\ngithub.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=\ngithub.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=\ngithub.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=\ngithub.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=\ngithub.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/mattn/go-zglob v0.0.1/go.mod h1:9fxibJccNxU2cnpIKLRRFA7zX7qhkJIQWBb449FYHOo=\ngithub.com/mattn/go-zglob v0.0.2-0.20190814121620-e3c945676326 h1:ofNAzWCcyTALn2Zv40+8XitdzCgXY6e9qvXwN9W0YXg=\ngithub.com/mattn/go-zglob v0.0.2-0.20190814121620-e3c945676326/go.mod h1:9fxibJccNxU2cnpIKLRRFA7zX7qhkJIQWBb449FYHOo=\ngithub.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=\ngithub.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=\ngithub.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=\ngithub.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=\ngithub.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=\ngithub.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=\ngithub.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU=\ngithub.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8=\ngithub.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=\ngithub.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=\ngithub.com/mitchellh/hashstructure v1.1.0 h1:P6P1hdjqAAknpY/M1CGipelZgp+4y9ja9kmUZPXP+H0=\ngithub.com/mitchellh/hashstructure v1.1.0/go.mod h1:xUDAozZz0Wmdiufv0uyhnHkUTN6/6d8ulp4AwfLKrmA=\ngithub.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU=\ngithub.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI=\ngithub.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=\ngithub.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=\ngithub.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=\ngithub.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8=\ngithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=\ngithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=\ngithub.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus=\ngithub.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=\ngithub.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=\ngithub.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=\ngithub.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=\ngithub.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=\ngithub.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns=\ngithub.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=\ngithub.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A=\ngithub.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k=\ngithub.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=\ngithub.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=\ngithub.com/opencontainers/image-spec v1.1.0-rc3 h1:fzg1mXZFj8YdPeNkRXMg+zb88BFV0Ys52cJydRwBkb8=\ngithub.com/opencontainers/image-spec v1.1.0-rc3/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8=\ngithub.com/oracle/oci-go-sdk v7.1.0+incompatible h1:ul/J6rOlLTuVgAB9oSBMwse0U9q8tZj3xx/NjmjRM2g=\ngithub.com/oracle/oci-go-sdk v7.1.0+incompatible/go.mod h1:VQb79nF8Z2cwLkLS35ukwStZIg5F66tcBccjip/j888=\ngithub.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA=\ngithub.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=\ngithub.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=\ngithub.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg=\ngithub.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=\ngithub.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/redis/go-redis/v9 v9.8.0 h1:q3nRvjrlge/6UD7eTu/DSg2uYiU2mCL0G/uzBWqhicI=\ngithub.com/redis/go-redis/v9 v9.8.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=\ngithub.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=\ngithub.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=\ngithub.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=\ngithub.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=\ngithub.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=\ngithub.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=\ngithub.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=\ngithub.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=\ngithub.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=\ngithub.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=\ngithub.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=\ngithub.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=\ngithub.com/slack-go/slack v0.15.0 h1:LE2lj2y9vqqiOf+qIIy0GvEoxgF1N5yLGZffmEZykt0=\ngithub.com/slack-go/slack v0.15.0/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw=\ngithub.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=\ngithub.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo=\ngithub.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=\ngithub.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=\ngithub.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=\ngithub.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=\ngithub.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=\ngithub.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=\ngithub.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=\ngithub.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=\ngithub.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=\ngithub.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngithub.com/texttheater/golang-levenshtein v1.0.1 h1:+cRNoVrfiwufQPhoMzB6N0Yf/Mqajr6t1lOv8GyGE2U=\ngithub.com/texttheater/golang-levenshtein v1.0.1/go.mod h1:PYAKrbF5sAiq9wd+H82hs7gNaen0CplQ9uvm6+enD/8=\ngithub.com/tmccombs/hcl2json v0.6.4 h1:/FWnzS9JCuyZ4MNwrG4vMrFrzRgsWEOVi+1AyYUVLGw=\ngithub.com/tmccombs/hcl2json v0.6.4/go.mod h1:+ppKlIW3H5nsAsZddXPy2iMyvld3SHxyjswOZhavRDk=\ngithub.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8=\ngithub.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=\ngithub.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=\ngithub.com/urfave/cli v1.22.12/go.mod h1:sSBEIC79qR6OvcmsD4U3KABeOTxDqQtdDnaFuUN30b8=\ngithub.com/urfave/cli v1.22.16 h1:MH0k6uJxdwdeWQTwhSO42Pwr4YLrNLwBtg1MRgTqPdQ=\ngithub.com/urfave/cli v1.22.16/go.mod h1:EeJR6BKodywf4zciqrdw6hpCPk68JO9z5LazXZMn5Po=\ngithub.com/vbatts/tar-split v0.11.3 h1:hLFqsOLQ1SsppQNTMpkpPXClLDfC2A3Zgy9OUU+RVck=\ngithub.com/vbatts/tar-split v0.11.3/go.mod h1:9QlHN18E+fEH7RdG+QAJJcuya3rqT7eXSTY7wGrAokY=\ngithub.com/virtuald/go-ordered-json v0.0.0-20170621173500-b18e6e673d74 h1:JwtAtbp7r/7QSyGz8mKUbYJBg2+6Cd7OjM8o/GNOcVo=\ngithub.com/virtuald/go-ordered-json v0.0.0-20170621173500-b18e6e673d74/go.mod h1:RmMWU37GKR2s6pgrIEB4ixgpVCt/cf7dnJv3fuH1J1c=\ngithub.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=\ngithub.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=\ngithub.com/zclconf/go-cty v1.15.0 h1:tTCRWxsexYUmtt/wVxgDClUe+uQusuI443uL6e+5sXQ=\ngithub.com/zclconf/go-cty v1.15.0/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE=\ngithub.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo=\ngithub.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM=\ngo.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=\ngo.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=\ngo.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=\ngo.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=\ngo.opentelemetry.io/contrib/detectors/gcp v1.39.0 h1:kWRNZMsfBHZ+uHjiH4y7Etn2FK26LAGkNFw7RHv1DhE=\ngo.opentelemetry.io/contrib/detectors/gcp v1.39.0/go.mod h1:t/OGqzHBa5v6RHZwrDBJ2OirWc+4q/w2fTbLZwAKjTk=\ngo.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 h1:r6I7RJCN86bpD/FQwedZ0vSixDpwuWREjW9oRMsmqDc=\ngo.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0/go.mod h1:B9yO6b04uB80CzjedvewuqDhxJxi11s7/GtiGa8bAjI=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8=\ngo.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=\ngo.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=\ngo.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.29.0 h1:WDdP9acbMYjbKIyJUhTvtzj601sVJOqgWdUxSdR/Ysc=\ngo.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.29.0/go.mod h1:BLbf7zbNIONBLPwvFnwNHGj4zge8uTCM/UPIVW1Mq2I=\ngo.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=\ngo.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=\ngo.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=\ngo.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE=\ngo.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw=\ngo.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=\ngo.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=\ngo.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=\ngo.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=\ngo.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=\ngo.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=\ngo.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=\ngolang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=\ngolang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=\ngolang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=\ngolang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=\ngolang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=\ngolang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=\ngolang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=\ngolang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=\ngolang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=\ngolang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=\ngolang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=\ngolang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=\ngolang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=\ngolang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220906165534-d0df966e6959/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=\ngolang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=\ngolang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=\ngolang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=\ngolang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=\ngolang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=\ngolang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=\ngolang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=\ngonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=\ngoogle.golang.org/api v0.206.0 h1:A27GClesCSheW5P2BymVHjpEeQ2XHH8DI8Srs2HI2L8=\ngoogle.golang.org/api v0.206.0/go.mod h1:BtB8bfjTYIrai3d8UyvPmV9REGgox7coh+ZRwm0b+W8=\ngoogle.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=\ngoogle.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=\ngoogle.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=\ngoogle.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=\ngoogle.golang.org/genproto v0.0.0-20241113202542-65e8d215514f h1:zDoHYmMzMacIdjNe+P2XiTmPsLawi/pCbSPfxt6lTfw=\ngoogle.golang.org/genproto v0.0.0-20241113202542-65e8d215514f/go.mod h1:Q5m6g8b5KaFFzsQFIGdJkSJDGeJiybVenoYFMMa3ohI=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=\ngoogle.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=\ngoogle.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=\ngoogle.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=\ngoogle.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=\ngoogle.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=\ngoogle.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=\ngoogle.golang.org/grpc/stats/opentelemetry v0.0.0-20240907200651-3ffb98b2c93a h1:UIpYSuWdWHSzjwcAFRLjKcPXFZVVLXGEM23W+NWqipw=\ngoogle.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=\ngoogle.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=\ngoogle.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=\ngoogle.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=\ngoogle.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=\ngoogle.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=\ngoogle.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=\ngoogle.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\ngopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo=\ngopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=\ngopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=\ngopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=\ngopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=\ngopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=\ngopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=\ngopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=\ngotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=\nhonnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nk8s.io/api v0.35.0 h1:iBAU5LTyBI9vw3L5glmat1njFK34srdLmktWwLTprlY=\nk8s.io/api v0.35.0/go.mod h1:AQ0SNTzm4ZAczM03QH42c7l3bih1TbAXYo0DkF8ktnA=\nk8s.io/apimachinery v0.35.0 h1:Z2L3IHvPVv/MJ7xRxHEtk6GoJElaAqDCCU0S6ncYok8=\nk8s.io/apimachinery v0.35.0/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns=\nk8s.io/client-go v0.35.0 h1:IAW0ifFbfQQwQmga0UdoH0yvdqrbwMdq9vIFEhRpxBE=\nk8s.io/client-go v0.35.0/go.mod h1:q2E5AAyqcbeLGPdoRB+Nxe3KYTfPce1Dnu1myQdqz9o=\nk8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=\nk8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=\nk8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE=\nk8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ=\nk8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck=\nk8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=\nsigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=\nsigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=\nsigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=\nsigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=\nsigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco=\nsigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=\nsigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=\nsigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=\n"
  },
  {
    "path": "internal/lib/formatting/format.go",
    "content": "// Package formatting provides internal utilities for formatting Terraform/Terragrunt CLI arguments.\npackage formatting\n\nimport (\n\t\"fmt\"\n\t\"reflect\"\n\t\"strconv\"\n\t\"strings\"\n)\n\n// FormatBackendConfigAsArgs formats backend configuration as Terraform CLI args.\n// Example: {\"bucket\": \"my-bucket\"} -> [\"-backend-config=bucket=my-bucket\"]\nfunc FormatBackendConfigAsArgs(vars map[string]interface{}) []string {\n\treturn formatTerraformArgs(vars, \"-backend-config\", false, true)\n}\n\n// FormatPluginDirAsArgs formats plugin directory as a Terraform CLI arg.\n// Returns nil if pluginDir is empty.\nfunc FormatPluginDirAsArgs(pluginDir string) []string {\n\tif pluginDir == \"\" {\n\t\treturn nil\n\t}\n\treturn []string{fmt.Sprintf(\"-plugin-dir=%v\", pluginDir)}\n}\n\n// formatTerraformArgs formats vars as CLI args with the given prefix.\nfunc formatTerraformArgs(vars map[string]interface{}, prefix string, useSpaceAsSeparator bool, omitNil bool) []string {\n\tvar args []string\n\n\tfor key, value := range vars {\n\t\tvar argValue string\n\t\tif omitNil && value == nil {\n\t\t\targValue = key\n\t\t} else {\n\t\t\thclString := toHclString(value, false)\n\t\t\targValue = fmt.Sprintf(\"%s=%s\", key, hclString)\n\t\t}\n\t\tif useSpaceAsSeparator {\n\t\t\targs = append(args, prefix, argValue)\n\t\t} else {\n\t\t\targs = append(args, fmt.Sprintf(\"%s=%s\", prefix, argValue))\n\t\t}\n\t}\n\n\treturn args\n}\n\n// toHclString converts Go values to HCL-formatted strings for Terraform CLI arguments.\n// Handles primitives, slices, and maps. Example: []int{1,2,3} -> \"[1, 2, 3]\"\nfunc toHclString(value interface{}, isNested bool) string {\n\tif slice, isSlice := tryToConvertToGenericSlice(value); isSlice {\n\t\treturn sliceToHclString(slice)\n\t} else if m, isMap := tryToConvertToGenericMap(value); isMap {\n\t\treturn mapToHclString(m)\n\t} else {\n\t\treturn primitiveToHclString(value, isNested)\n\t}\n}\n\n// tryToConvertToGenericSlice converts any slice type to []interface{} using reflection.\nfunc tryToConvertToGenericSlice(value interface{}) ([]interface{}, bool) {\n\treflectValue := reflect.ValueOf(value)\n\tif reflectValue.Kind() != reflect.Slice {\n\t\treturn []interface{}{}, false\n\t}\n\n\tgenericSlice := make([]interface{}, reflectValue.Len())\n\n\tfor i := 0; i < reflectValue.Len(); i++ {\n\t\tgenericSlice[i] = reflectValue.Index(i).Interface()\n\t}\n\n\treturn genericSlice, true\n}\n\n// tryToConvertToGenericMap converts any map[string]T to map[string]interface{} using reflection.\nfunc tryToConvertToGenericMap(value interface{}) (map[string]interface{}, bool) {\n\treflectValue := reflect.ValueOf(value)\n\tif reflectValue.Kind() != reflect.Map {\n\t\treturn map[string]interface{}{}, false\n\t}\n\n\treflectType := reflect.TypeOf(value)\n\tif reflectType.Key().Kind() != reflect.String {\n\t\treturn map[string]interface{}{}, false\n\t}\n\n\tgenericMap := make(map[string]interface{}, reflectValue.Len())\n\n\tmapKeys := reflectValue.MapKeys()\n\tfor _, key := range mapKeys {\n\t\tgenericMap[key.String()] = reflectValue.MapIndex(key).Interface()\n\t}\n\n\treturn genericMap, true\n}\n\nfunc sliceToHclString(slice []interface{}) string {\n\thclValues := []string{}\n\n\tfor _, value := range slice {\n\t\thclValue := toHclString(value, true)\n\t\thclValues = append(hclValues, hclValue)\n\t}\n\n\treturn fmt.Sprintf(\"[%s]\", strings.Join(hclValues, \", \"))\n}\n\nfunc mapToHclString(m map[string]interface{}) string {\n\tkeyValuePairs := []string{}\n\n\tfor key, value := range m {\n\t\tkeyValuePair := fmt.Sprintf(`\"%s\" = %s`, key, toHclString(value, true))\n\t\tkeyValuePairs = append(keyValuePairs, keyValuePair)\n\t}\n\n\treturn fmt.Sprintf(\"{%s}\", strings.Join(keyValuePairs, \", \"))\n}\n\nfunc primitiveToHclString(value interface{}, isNested bool) string {\n\tif value == nil {\n\t\treturn \"null\"\n\t}\n\n\tswitch v := value.(type) {\n\n\tcase bool:\n\t\treturn strconv.FormatBool(v)\n\n\tcase string:\n\t\t// If string is nested in a larger data structure (e.g. list of string, map of string), ensure value is quoted\n\t\tif isNested {\n\t\t\treturn fmt.Sprintf(\"\\\"%v\\\"\", v)\n\t\t}\n\n\t\treturn fmt.Sprintf(\"%v\", v)\n\n\tdefault:\n\t\treturn fmt.Sprintf(\"%v\", v)\n\t}\n}\n"
  },
  {
    "path": "internal/lib/formatting/format_test.go",
    "content": "package formatting\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestFormatBackendConfigAsArgs(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname   string\n\t\tinput  map[string]interface{}\n\t\texpect []string\n\t}{\n\t\t{\n\t\t\tname:   \"empty config\",\n\t\t\tinput:  map[string]interface{}{},\n\t\t\texpect: []string{},\n\t\t},\n\t\t{\n\t\t\tname:   \"string value\",\n\t\t\tinput:  map[string]interface{}{\"bucket\": \"my-bucket\"},\n\t\t\texpect: []string{\"-backend-config=bucket=my-bucket\"},\n\t\t},\n\t\t{\n\t\t\tname:   \"nil value omitted\",\n\t\t\tinput:  map[string]interface{}{\"key\": nil},\n\t\t\texpect: []string{\"-backend-config=key\"},\n\t\t},\n\t\t{\n\t\t\tname:   \"multiple values\",\n\t\t\tinput:  map[string]interface{}{\"region\": \"us-east-1\", \"bucket\": \"state\"},\n\t\t\texpect: []string{\"-backend-config=bucket=state\", \"-backend-config=region=us-east-1\"},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := FormatBackendConfigAsArgs(tt.input)\n\t\t\tassert.ElementsMatch(t, tt.expect, result)\n\t\t})\n\t}\n}\n\nfunc TestFormatPluginDirAsArgs(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname   string\n\t\tinput  string\n\t\texpect []string\n\t}{\n\t\t{\n\t\t\tname:   \"empty path\",\n\t\t\tinput:  \"\",\n\t\t\texpect: nil,\n\t\t},\n\t\t{\n\t\t\tname:   \"valid path\",\n\t\t\tinput:  \"/path/to/plugins\",\n\t\t\texpect: []string{\"-plugin-dir=/path/to/plugins\"},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := FormatPluginDirAsArgs(tt.input)\n\t\t\tassert.Equal(t, tt.expect, result)\n\t\t})\n\t}\n}\n\nfunc TestToHclString(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname   string\n\t\tinput  interface{}\n\t\texpect string\n\t}{\n\t\t{\"nil\", nil, \"null\"},\n\t\t{\"bool true\", true, \"true\"},\n\t\t{\"bool false\", false, \"false\"},\n\t\t{\"string\", \"hello\", \"hello\"},\n\t\t{\"int\", 42, \"42\"},\n\t\t{\"list of strings\", []string{\"a\", \"b\"}, `[\"a\", \"b\"]`},\n\t\t{\"list of ints\", []int{1, 2, 3}, \"[1, 2, 3]\"},\n\t\t{\"map\", map[string]string{\"key\": \"value\"}, `{\"key\" = \"value\"}`},\n\t\t{\"nested list\", []interface{}{[]int{1, 2}}, \"[[1, 2]]\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := toHclString(tt.input, false)\n\t\t\tassert.Equal(t, tt.expect, result)\n\t\t})\n\t}\n}\n\nfunc TestToHclStringNested(t *testing.T) {\n\tt.Parallel()\n\n\t// Nested strings should be quoted\n\tresult := toHclString(\"nested\", true)\n\tassert.Equal(t, `\"nested\"`, result)\n\n\t// Non-nested strings should not be quoted\n\tresult = toHclString(\"not-nested\", false)\n\tassert.Equal(t, \"not-nested\", result)\n}\n"
  },
  {
    "path": "mise.toml",
    "content": "[tools]\ngo = \"1.26.1\"\ngolangci-lint = \"2.11.3\"\n"
  },
  {
    "path": "modules/aws/account.go",
    "content": "package aws\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"strings\"\n\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\t\"github.com/aws/aws-sdk-go-v2/service/sts\"\n\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n)\n\n// GetAccountId gets the Account ID for the currently logged in IAM User.\nfunc GetAccountId(t testing.TestingT) string {\n\tid, err := GetAccountIdE(t)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn id\n}\n\n// GetAccountIdE gets the Account ID for the currently logged in IAM User.\nfunc GetAccountIdE(t testing.TestingT) (string, error) {\n\tstsClient, err := NewStsClientE(t, defaultRegion)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tidentity, err := stsClient.GetCallerIdentity(context.Background(), &sts.GetCallerIdentityInput{})\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn aws.ToString(identity.Account), nil\n}\n\n// An IAM arn is of the format arn:aws:iam::123456789012:user/test. The account id is the number after arn:aws:iam::,\n// so we split on a colon and return the 5th item.\nfunc extractAccountIDFromARN(arn string) (string, error) {\n\tarnParts := strings.Split(arn, \":\")\n\n\tif len(arnParts) < 5 {\n\t\treturn \"\", errors.New(\"Unrecognized format for IAM ARN: \" + arn)\n\t}\n\n\treturn arnParts[4], nil\n}\n\n// NewStsClientE creates a new STS client.\nfunc NewStsClientE(t testing.TestingT, region string) (*sts.Client, error) {\n\tsess, err := NewAuthenticatedSession(region)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn sts.NewFromConfig(*sess), nil\n}\n"
  },
  {
    "path": "modules/aws/account_test.go",
    "content": "package aws\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestGetAccountId(t *testing.T) {\n\taccountID := GetAccountId(t)\n\tassert.Regexp(t, \"^[0-9]{12}$\", accountID)\n}\n\nfunc TestExtractAccountIdFromValidArn(t *testing.T) {\n\tt.Parallel()\n\n\texpectedAccountID := \"123456789012\"\n\tarn := \"arn:aws:iam::\" + expectedAccountID + \":user/test\"\n\n\tactualAccountID, err := extractAccountIDFromARN(arn)\n\tif err != nil {\n\t\tt.Fatalf(\"Unexpected error while extracting account id from arn %s: %s\", arn, err)\n\t}\n\n\tif actualAccountID != expectedAccountID {\n\t\tt.Fatalf(\"Did not get expected account id. Expected: %s. Actual: %s.\", expectedAccountID, actualAccountID)\n\t}\n}\n\nfunc TestExtractAccountIdFromInvalidArn(t *testing.T) {\n\tt.Parallel()\n\n\t_, err := extractAccountIDFromARN(\"invalidArn\")\n\tif err == nil {\n\t\tt.Fatalf(\"Expected an error when extracting an account id from an invalid ARN, but got nil\")\n\t}\n}\n"
  },
  {
    "path": "modules/aws/acm.go",
    "content": "package aws\n\nimport (\n\t\"context\"\n\n\t\"github.com/aws/aws-sdk-go-v2/service/acm\"\n\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n)\n\n// GetAcmCertificateArn gets the ACM certificate for the given domain name in the given region.\nfunc GetAcmCertificateArn(t testing.TestingT, awsRegion string, certDomainName string) string {\n\tarn, err := GetAcmCertificateArnE(t, awsRegion, certDomainName)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn arn\n}\n\n// GetAcmCertificateArnE gets the ACM certificate for the given domain name in the given region.\nfunc GetAcmCertificateArnE(t testing.TestingT, awsRegion string, certDomainName string) (string, error) {\n\tacmClient, err := NewAcmClientE(t, awsRegion)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tresult, err := acmClient.ListCertificates(context.Background(), &acm.ListCertificatesInput{})\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tfor _, summary := range result.CertificateSummaryList {\n\t\tif *summary.DomainName == certDomainName {\n\t\t\treturn *summary.CertificateArn, nil\n\t\t}\n\t}\n\n\treturn \"\", nil\n}\n\n// NewAcmClient create a new ACM client.\nfunc NewAcmClient(t testing.TestingT, region string) *acm.Client {\n\tclient, err := NewAcmClientE(t, region)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn client\n}\n\n// NewAcmClientE creates a new ACM client.\nfunc NewAcmClientE(t testing.TestingT, awsRegion string) (*acm.Client, error) {\n\tsess, err := NewAuthenticatedSession(awsRegion)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn acm.NewFromConfig(*sess), nil\n}\n"
  },
  {
    "path": "modules/aws/ami.go",
    "content": "package aws\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"sort\"\n\t\"time\"\n\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\t\"github.com/aws/aws-sdk-go-v2/service/ec2\"\n\t\"github.com/aws/aws-sdk-go-v2/service/ec2/types\"\n\t\"github.com/gruntwork-io/terratest/modules/logger\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n)\n\n// These are commonly used AMI account IDs.\nconst (\n\tCanonicalAccountId = \"099720109477\"\n\tCentOsAccountId    = \"679593333241\"\n\tAmazonAccountId    = \"amazon\"\n)\n\n// DeleteAmiAndAllSnapshots will delete the given AMI along with all EBS snapshots that backed that AMI\nfunc DeleteAmiAndAllSnapshots(t testing.TestingT, region string, ami string) {\n\terr := DeleteAmiAndAllSnapshotsE(t, region, ami)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n\n// DeleteAmiAndAllSnapshotsE will delete the given AMI along with all EBS snapshots that backed that AMI\nfunc DeleteAmiAndAllSnapshotsE(t testing.TestingT, region string, ami string) error {\n\tsnapshots, err := GetEbsSnapshotsForAmiE(t, region, ami)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = DeleteAmiE(t, region, ami)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfor _, snapshot := range snapshots {\n\t\terr = DeleteEbsSnapshotE(t, region, snapshot)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// GetEbsSnapshotsForAmi retrieves the EBS snapshots which back the given AMI\nfunc GetEbsSnapshotsForAmi(t testing.TestingT, region string, ami string) []string {\n\tsnapshots, err := GetEbsSnapshotsForAmiE(t, region, ami)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn snapshots\n}\n\n// GetEbsSnapshotsForAmiE retrieves the EBS snapshots which back the given AMI\nfunc GetEbsSnapshotsForAmiE(t testing.TestingT, region string, ami string) ([]string, error) {\n\tlogger.Default.Logf(t, \"Retrieving EBS snapshots backing AMI %s\", ami)\n\tec2Client, err := NewEc2ClientE(t, region)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\timages, err := ec2Client.DescribeImages(context.Background(), &ec2.DescribeImagesInput{\n\t\tImageIds: []string{\n\t\t\tami,\n\t\t},\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar snapshots []string\n\tfor _, image := range images.Images {\n\t\tfor _, mapping := range image.BlockDeviceMappings {\n\t\t\tif mapping.Ebs != nil && mapping.Ebs.SnapshotId != nil {\n\t\t\t\tsnapshots = append(snapshots, aws.ToString(mapping.Ebs.SnapshotId))\n\t\t\t}\n\t\t}\n\t}\n\n\treturn snapshots, err\n}\n\n// GetMostRecentAmiId gets the ID of the most recent AMI in the given region that has the given owner and matches the given filters. Each\n// filter should correspond to the name and values of a filter supported by DescribeImagesInput:\n// https://docs.aws.amazon.com/sdk-for-go/api/service/ec2/#DescribeImagesInput\nfunc GetMostRecentAmiId(t testing.TestingT, region string, ownerId string, filters map[string][]string) string {\n\tamiID, err := GetMostRecentAmiIdE(t, region, ownerId, filters)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn amiID\n}\n\n// GetMostRecentAmiIdE gets the ID of the most recent AMI in the given region that has the given owner and matches the given filters. Each\n// filter should correspond to the name and values of a filter supported by DescribeImagesInput:\n// https://docs.aws.amazon.com/sdk-for-go/api/service/ec2/#DescribeImagesInput\nfunc GetMostRecentAmiIdE(t testing.TestingT, region string, ownerId string, filters map[string][]string) (string, error) {\n\tec2Client, err := NewEc2ClientE(t, region)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tvar ec2Filters []types.Filter\n\tfor name, values := range filters {\n\t\tec2Filters = append(ec2Filters, types.Filter{Name: aws.String(name), Values: values})\n\t}\n\n\tinput := ec2.DescribeImagesInput{\n\t\tFilters:           ec2Filters,\n\t\tIncludeDeprecated: aws.Bool(true),\n\t\tOwners:            []string{ownerId},\n\t}\n\n\tout, err := ec2Client.DescribeImages(context.Background(), &input)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif len(out.Images) == 0 {\n\t\treturn \"\", NoImagesFound{Region: region, OwnerId: ownerId, Filters: filters}\n\t}\n\n\tmostRecentImage := mostRecentAMI(out.Images)\n\treturn aws.ToString(mostRecentImage.ImageId), nil\n}\n\n// Image sorting code borrowed from: https://github.com/hashicorp/packer/blob/7f4112ba229309cfc0ebaa10ded2abdfaf1b22c8/builder/amazon/common/step_source_ami_info.go\ntype imageSort []types.Image\n\nfunc (a imageSort) Len() int      { return len(a) }\nfunc (a imageSort) Swap(i, j int) { a[i], a[j] = a[j], a[i] }\nfunc (a imageSort) Less(i, j int) bool {\n\tiTime, _ := time.Parse(time.RFC3339, *a[i].CreationDate)\n\tjTime, _ := time.Parse(time.RFC3339, *a[j].CreationDate)\n\treturn iTime.Unix() < jTime.Unix()\n}\n\n// mostRecentAMI returns the most recent AMI out of a slice of images.\nfunc mostRecentAMI(images []types.Image) types.Image {\n\tsortedImages := images\n\tsort.Sort(imageSort(sortedImages))\n\treturn sortedImages[len(sortedImages)-1]\n}\n\n// GetUbuntu1404Ami gets the ID of the most recent Ubuntu 14.04 HVM x86_64 EBS GP2 AMI in the given region.\nfunc GetUbuntu1404Ami(t testing.TestingT, region string) string {\n\tamiID, err := GetUbuntu1404AmiE(t, region)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn amiID\n}\n\n// GetUbuntu1404AmiE gets the ID of the most recent Ubuntu 14.04 HVM x86_64 EBS GP2 AMI in the given region.\nfunc GetUbuntu1404AmiE(t testing.TestingT, region string) (string, error) {\n\tfilters := map[string][]string{\n\t\t\"name\":                             {\"*ubuntu-trusty-14.04-amd64-server-*\"},\n\t\t\"virtualization-type\":              {\"hvm\"},\n\t\t\"architecture\":                     {\"x86_64\"},\n\t\t\"root-device-type\":                 {\"ebs\"},\n\t\t\"block-device-mapping.volume-type\": {\"gp2\"},\n\t}\n\n\treturn GetMostRecentAmiIdE(t, region, CanonicalAccountId, filters)\n}\n\n// GetUbuntu1604Ami gets the ID of the most recent Ubuntu 16.04 HVM x86_64 EBS GP2 AMI in the given region.\nfunc GetUbuntu1604Ami(t testing.TestingT, region string) string {\n\tamiID, err := GetUbuntu1604AmiE(t, region)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn amiID\n}\n\n// GetUbuntu1604AmiE gets the ID of the most recent Ubuntu 16.04 HVM x86_64 EBS GP2 AMI in the given region.\nfunc GetUbuntu1604AmiE(t testing.TestingT, region string) (string, error) {\n\tfilters := map[string][]string{\n\t\t\"name\":                             {\"*ubuntu-xenial-16.04-amd64-server-*\"},\n\t\t\"virtualization-type\":              {\"hvm\"},\n\t\t\"architecture\":                     {\"x86_64\"},\n\t\t\"root-device-type\":                 {\"ebs\"},\n\t\t\"block-device-mapping.volume-type\": {\"gp2\"},\n\t}\n\n\treturn GetMostRecentAmiIdE(t, region, CanonicalAccountId, filters)\n}\n\n// GetUbuntu2004Ami gets the ID of the most recent Ubuntu 20.04 HVM x86_64 EBS GP2 AMI in the given region.\nfunc GetUbuntu2004Ami(t testing.TestingT, region string) string {\n\tamiID, err := GetUbuntu2004AmiE(t, region)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn amiID\n}\n\n// GetUbuntu2004AmiE gets the ID of the most recent Ubuntu 20.04 HVM x86_64 EBS GP2 AMI in the given region.\nfunc GetUbuntu2004AmiE(t testing.TestingT, region string) (string, error) {\n\tfilters := map[string][]string{\n\t\t\"name\":                             {\"*ubuntu-focal-20.04-amd64-server-*\"},\n\t\t\"virtualization-type\":              {\"hvm\"},\n\t\t\"architecture\":                     {\"x86_64\"},\n\t\t\"root-device-type\":                 {\"ebs\"},\n\t\t\"block-device-mapping.volume-type\": {\"gp2\"},\n\t}\n\n\treturn GetMostRecentAmiIdE(t, region, CanonicalAccountId, filters)\n}\n\n// GetUbuntu2204Ami gets the ID of the most recent Ubuntu 22.04 HVM x86_64 EBS GP2 AMI in the given region.\nfunc GetUbuntu2204Ami(t testing.TestingT, region string) string {\n\tamiID, err := GetUbuntu2204AmiE(t, region)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn amiID\n}\n\n// GetUbuntu2204AmiE gets the ID of the most recent Ubuntu 22.04 HVM x86_64 EBS GP2 AMI in the given region.\nfunc GetUbuntu2204AmiE(t testing.TestingT, region string) (string, error) {\n\tfilters := map[string][]string{\n\t\t\"name\":                             {\"*ubuntu-jammy-22.04-amd64-server-*\"},\n\t\t\"virtualization-type\":              {\"hvm\"},\n\t\t\"architecture\":                     {\"x86_64\"},\n\t\t\"root-device-type\":                 {\"ebs\"},\n\t\t\"block-device-mapping.volume-type\": {\"gp2\"},\n\t}\n\n\treturn GetMostRecentAmiIdE(t, region, CanonicalAccountId, filters)\n}\n\n// GetCentos7Ami returns a CentOS 7 public AMI from the given region.\n// WARNING: you may have to accept the terms & conditions of this AMI in AWS MarketPlace for your AWS Account before\n// you can successfully launch the AMI.\nfunc GetCentos7Ami(t testing.TestingT, region string) string {\n\tamiID, err := GetCentos7AmiE(t, region)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn amiID\n}\n\n// GetCentos7AmiE returns a CentOS 7 public AMI from the given region.\n// WARNING: you may have to accept the terms & conditions of this AMI in AWS MarketPlace for your AWS Account before\n// you can successfully launch the AMI.\nfunc GetCentos7AmiE(t testing.TestingT, region string) (string, error) {\n\tfilters := map[string][]string{\n\t\t\"name\":                             {\"*CentOS Linux 7 x86_64 HVM EBS*\"},\n\t\t\"virtualization-type\":              {\"hvm\"},\n\t\t\"architecture\":                     {\"x86_64\"},\n\t\t\"root-device-type\":                 {\"ebs\"},\n\t\t\"block-device-mapping.volume-type\": {\"gp2\"},\n\t}\n\n\treturn GetMostRecentAmiIdE(t, region, CentOsAccountId, filters)\n}\n\n// GetAmazonLinuxAmi returns an Amazon Linux AMI HVM, SSD Volume Type public AMI for the given region.\nfunc GetAmazonLinuxAmi(t testing.TestingT, region string) string {\n\tamiID, err := GetAmazonLinuxAmiE(t, region)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn amiID\n}\n\n// GetAmazonLinuxAmiE returns an Amazon Linux AMI HVM, SSD Volume Type public AMI for the given region.\nfunc GetAmazonLinuxAmiE(t testing.TestingT, region string) (string, error) {\n\tfilters := map[string][]string{\n\t\t\"name\":                             {\"*amzn2-ami-hvm-*-x86_64*\"},\n\t\t\"virtualization-type\":              {\"hvm\"},\n\t\t\"architecture\":                     {\"x86_64\"},\n\t\t\"root-device-type\":                 {\"ebs\"},\n\t\t\"block-device-mapping.volume-type\": {\"gp2\"},\n\t}\n\n\treturn GetMostRecentAmiIdE(t, region, AmazonAccountId, filters)\n}\n\n// GetEcsOptimizedAmazonLinuxAmi returns an Amazon ECS-Optimized Amazon Linux AMI for the given region. This AMI is useful for running an ECS cluster.\nfunc GetEcsOptimizedAmazonLinuxAmi(t testing.TestingT, region string) string {\n\tamiID, err := GetEcsOptimizedAmazonLinuxAmiE(t, region)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn amiID\n}\n\n// GetEcsOptimizedAmazonLinuxAmiE returns an Amazon ECS-Optimized Amazon Linux AMI for the given region. This AMI is useful for running an ECS cluster.\nfunc GetEcsOptimizedAmazonLinuxAmiE(t testing.TestingT, region string) (string, error) {\n\tfilters := map[string][]string{\n\t\t\"name\":                             {\"*amzn-ami*amazon-ecs-optimized*\"},\n\t\t\"virtualization-type\":              {\"hvm\"},\n\t\t\"architecture\":                     {\"x86_64\"},\n\t\t\"root-device-type\":                 {\"ebs\"},\n\t\t\"block-device-mapping.volume-type\": {\"gp2\"},\n\t}\n\n\treturn GetMostRecentAmiIdE(t, region, AmazonAccountId, filters)\n}\n\n// NoImagesFound is an error that occurs if no images were found.\ntype NoImagesFound struct {\n\tRegion  string\n\tOwnerId string\n\tFilters map[string][]string\n}\n\nfunc (err NoImagesFound) Error() string {\n\treturn fmt.Sprintf(\"No AMIs found in %s for owner ID %s and filters: %v\", err.Region, err.OwnerId, err.Filters)\n}\n"
  },
  {
    "path": "modules/aws/ami_test.go",
    "content": "package aws\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestGetUbuntu1404AmiReturnsSomeAmi(t *testing.T) {\n\tt.Parallel()\n\n\tamiID := GetUbuntu1404Ami(t, \"us-east-1\")\n\tassert.Regexp(t, \"^ami-[[:alnum:]]+$\", amiID)\n}\n\nfunc TestGetUbuntu1604AmiReturnsSomeAmi(t *testing.T) {\n\tt.Parallel()\n\n\tamiID := GetUbuntu1604Ami(t, \"us-west-1\")\n\tassert.Regexp(t, \"^ami-[[:alnum:]]+$\", amiID)\n}\n\nfunc TestGetUbuntu2004AmiReturnsSomeAmi(t *testing.T) {\n\tt.Parallel()\n\n\tamiID := GetUbuntu2004Ami(t, \"us-west-1\")\n\tassert.Regexp(t, \"^ami-[[:alnum:]]+$\", amiID)\n}\n\nfunc TestGetUbuntu2204AmiReturnsSomeAmi(t *testing.T) {\n\tt.Parallel()\n\n\tamiID := GetUbuntu2204Ami(t, \"us-west-1\")\n\tassert.Regexp(t, \"^ami-[[:alnum:]]+$\", amiID)\n}\n\nfunc TestGetCentos7AmiReturnsSomeAmi(t *testing.T) {\n\tt.Parallel()\n\n\tamiID := GetCentos7Ami(t, \"eu-west-1\")\n\tassert.Regexp(t, \"^ami-[[:alnum:]]+$\", amiID)\n}\n\nfunc TestGetAmazonLinuxAmiReturnsSomeAmi(t *testing.T) {\n\tt.Parallel()\n\n\tamiID := GetAmazonLinuxAmi(t, \"ap-southeast-1\")\n\tassert.Regexp(t, \"^ami-[[:alnum:]]+$\", amiID)\n}\n\nfunc TestGetEcsOptimizedAmazonLinuxAmiEReturnsSomeAmi(t *testing.T) {\n\tt.Parallel()\n\n\tamiID := GetEcsOptimizedAmazonLinuxAmi(t, \"us-east-2\")\n\tassert.Regexp(t, \"^ami-[[:alnum:]]+$\", amiID)\n}\n"
  },
  {
    "path": "modules/aws/asg.go",
    "content": "package aws\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\t\"github.com/aws/aws-sdk-go-v2/service/autoscaling\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/gruntwork-io/terratest/modules/logger\"\n\t\"github.com/gruntwork-io/terratest/modules/retry\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n)\n\ntype AsgCapacityInfo struct {\n\tMinCapacity     int64\n\tMaxCapacity     int64\n\tCurrentCapacity int64\n\tDesiredCapacity int64\n}\n\n// GetCapacityInfoForAsg returns the capacity info for the queried asg as a struct, AsgCapacityInfo.\nfunc GetCapacityInfoForAsg(t testing.TestingT, asgName string, awsRegion string) AsgCapacityInfo {\n\tcapacityInfo, err := GetCapacityInfoForAsgE(t, asgName, awsRegion)\n\trequire.NoError(t, err)\n\treturn capacityInfo\n}\n\n// GetCapacityInfoForAsgE returns the capacity info for the queried asg as a struct, AsgCapacityInfo.\nfunc GetCapacityInfoForAsgE(t testing.TestingT, asgName string, awsRegion string) (AsgCapacityInfo, error) {\n\tasgClient, err := NewAsgClientE(t, awsRegion)\n\tif err != nil {\n\t\treturn AsgCapacityInfo{}, err\n\t}\n\n\tinput := autoscaling.DescribeAutoScalingGroupsInput{AutoScalingGroupNames: []string{asgName}}\n\toutput, err := asgClient.DescribeAutoScalingGroups(context.Background(), &input)\n\tif err != nil {\n\t\treturn AsgCapacityInfo{}, err\n\t}\n\tgroups := output.AutoScalingGroups\n\tif len(groups) == 0 {\n\t\treturn AsgCapacityInfo{}, NewNotFoundError(\"ASG\", asgName, awsRegion)\n\t}\n\tcapacityInfo := AsgCapacityInfo{\n\t\tMinCapacity:     int64(*groups[0].MinSize),\n\t\tMaxCapacity:     int64(*groups[0].MaxSize),\n\t\tDesiredCapacity: int64(*groups[0].DesiredCapacity),\n\t\tCurrentCapacity: int64(len(groups[0].Instances)),\n\t}\n\treturn capacityInfo, nil\n}\n\n// GetInstanceIdsForAsg gets the IDs of EC2 Instances in the given ASG.\nfunc GetInstanceIdsForAsg(t testing.TestingT, asgName string, awsRegion string) []string {\n\tids, err := GetInstanceIdsForAsgE(t, asgName, awsRegion)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn ids\n}\n\n// GetInstanceIdsForAsgE gets the IDs of EC2 Instances in the given ASG.\nfunc GetInstanceIdsForAsgE(t testing.TestingT, asgName string, awsRegion string) ([]string, error) {\n\tasgClient, err := NewAsgClientE(t, awsRegion)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tinput := autoscaling.DescribeAutoScalingGroupsInput{AutoScalingGroupNames: []string{asgName}}\n\toutput, err := asgClient.DescribeAutoScalingGroups(context.Background(), &input)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar instanceIDs []string\n\tfor _, asg := range output.AutoScalingGroups {\n\t\tfor _, instance := range asg.Instances {\n\t\t\tinstanceIDs = append(instanceIDs, aws.ToString(instance.InstanceId))\n\t\t}\n\t}\n\n\treturn instanceIDs, nil\n}\n\n// WaitForCapacity waits for the currently set desired capacity to be reached on the ASG\nfunc WaitForCapacity(\n\tt testing.TestingT,\n\tasgName string,\n\tregion string,\n\tmaxRetries int,\n\tsleepBetweenRetries time.Duration,\n) {\n\terr := WaitForCapacityE(t, asgName, region, maxRetries, sleepBetweenRetries)\n\trequire.NoError(t, err)\n}\n\n// WaitForCapacityE waits for the currently set desired capacity to be reached on the ASG\nfunc WaitForCapacityE(\n\tt testing.TestingT,\n\tasgName string,\n\tregion string,\n\tmaxRetries int,\n\tsleepBetweenRetries time.Duration,\n) error {\n\tmsg, err := retry.DoWithRetryE(\n\t\tt,\n\t\tfmt.Sprintf(\"Waiting for ASG %s to reach desired capacity.\", asgName),\n\t\tmaxRetries,\n\t\tsleepBetweenRetries,\n\t\tfunc() (string, error) {\n\t\t\tcapacityInfo, err := GetCapacityInfoForAsgE(t, asgName, region)\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", err\n\t\t\t}\n\t\t\tif capacityInfo.CurrentCapacity != capacityInfo.DesiredCapacity {\n\t\t\t\treturn \"\", NewAsgCapacityNotMetError(asgName, capacityInfo.DesiredCapacity, capacityInfo.CurrentCapacity)\n\t\t\t}\n\t\t\treturn fmt.Sprintf(\"ASG %s is now at desired capacity %d\", asgName, capacityInfo.DesiredCapacity), nil\n\t\t},\n\t)\n\tlogger.Default.Logf(t, \"%s\", msg)\n\treturn err\n}\n\n// NewAsgClient creates an Auto Scaling Group client.\nfunc NewAsgClient(t testing.TestingT, region string) *autoscaling.Client {\n\tclient, err := NewAsgClientE(t, region)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn client\n}\n\n// NewAsgClientE creates an Auto Scaling Group client.\nfunc NewAsgClientE(t testing.TestingT, region string) (*autoscaling.Client, error) {\n\tsess, err := NewAuthenticatedSession(region)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn autoscaling.NewFromConfig(*sess), nil\n}\n"
  },
  {
    "path": "modules/aws/asg_test.go",
    "content": "package aws\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\t\"github.com/aws/aws-sdk-go-v2/service/autoscaling\"\n\tautoscalingTypes \"github.com/aws/aws-sdk-go-v2/service/autoscaling/types\"\n\t\"github.com/aws/aws-sdk-go-v2/service/ec2\"\n\t\"github.com/aws/aws-sdk-go-v2/service/ec2/types\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n)\n\nfunc TestGetCapacityInfoForAsg(t *testing.T) {\n\tt.Parallel()\n\n\tuniqueID := random.UniqueId()\n\tasgName := fmt.Sprintf(\"%s-%s\", t.Name(), uniqueID)\n\tregion := GetRandomStableRegion(t, []string{}, []string{})\n\n\tdefer deleteAutoScalingGroup(t, asgName, region)\n\tcreateTestAutoScalingGroup(t, asgName, region, 2)\n\tWaitForCapacity(t, asgName, region, 40, 15*time.Second)\n\n\tcapacityInfo := GetCapacityInfoForAsg(t, asgName, region)\n\tassert.Equal(t, capacityInfo.DesiredCapacity, int64(2))\n\tassert.Equal(t, capacityInfo.CurrentCapacity, int64(2))\n\tassert.Equal(t, capacityInfo.MinCapacity, int64(1))\n\tassert.Equal(t, capacityInfo.MaxCapacity, int64(3))\n}\n\nfunc TestGetInstanceIdsForAsg(t *testing.T) {\n\tt.Parallel()\n\n\tuniqueID := random.UniqueId()\n\tasgName := fmt.Sprintf(\"%s-%s\", t.Name(), uniqueID)\n\tregion := GetRandomStableRegion(t, []string{}, []string{})\n\n\tdefer deleteAutoScalingGroup(t, asgName, region)\n\tcreateTestAutoScalingGroup(t, asgName, region, 1)\n\tWaitForCapacity(t, asgName, region, 40, 15*time.Second)\n\n\tinstanceIds := GetInstanceIdsForAsg(t, asgName, region)\n\tassert.Equal(t, len(instanceIds), 1)\n}\n\nfunc createTestAutoScalingGroup(t *testing.T, name string, region string, desiredCount int32) {\n\tazs := GetAvailabilityZones(t, region)\n\tec2Client := NewEc2Client(t, region)\n\timageID := GetAmazonLinuxAmi(t, region)\n\ttemplate, err := ec2Client.CreateLaunchTemplate(context.Background(), &ec2.CreateLaunchTemplateInput{\n\t\tLaunchTemplateData: &types.RequestLaunchTemplateData{\n\t\t\tImageId:      aws.String(imageID),\n\t\t\tInstanceType: types.InstanceType(GetRecommendedInstanceType(t, region, []string{\"t2.micro, t3.micro\", \"t2.small\", \"t3.small\"})),\n\t\t},\n\t\tLaunchTemplateName: aws.String(name),\n\t})\n\trequire.NoError(t, err)\n\n\tasgClient := NewAsgClient(t, region)\n\tparam := &autoscaling.CreateAutoScalingGroupInput{\n\t\tAutoScalingGroupName: &name,\n\t\tLaunchTemplate: &autoscalingTypes.LaunchTemplateSpecification{\n\t\t\tLaunchTemplateId: template.LaunchTemplate.LaunchTemplateId,\n\t\t\tVersion:          aws.String(\"$Latest\"),\n\t\t},\n\t\tAvailabilityZones: azs,\n\t\tDesiredCapacity:   aws.Int32(desiredCount),\n\t\tMinSize:           aws.Int32(1),\n\t\tMaxSize:           aws.Int32(3),\n\t}\n\t_, err = asgClient.CreateAutoScalingGroup(context.Background(), param)\n\trequire.NoError(t, err)\n\n\twaiter := autoscaling.NewGroupExistsWaiter(asgClient)\n\terr = waiter.Wait(context.Background(), &autoscaling.DescribeAutoScalingGroupsInput{\n\t\tAutoScalingGroupNames: []string{name},\n\t}, 42*time.Minute)\n\trequire.NoError(t, err)\n}\n\nfunc createTestEC2Instance(t *testing.T, region string, name string) types.Instance {\n\tec2Client := NewEc2Client(t, region)\n\timageID := GetAmazonLinuxAmi(t, region)\n\tparams := &ec2.RunInstancesInput{\n\t\tImageId:      aws.String(imageID),\n\t\tInstanceType: types.InstanceType(GetRecommendedInstanceType(t, region, []string{\"t2.micro, t3.micro\", \"t2.small\", \"t3.small\"})),\n\t\tMinCount:     aws.Int32(1),\n\t\tMaxCount:     aws.Int32(1),\n\t}\n\trunResult, err := ec2Client.RunInstances(context.Background(), params)\n\trequire.NoError(t, err)\n\n\trequire.NotEqual(t, len(runResult.Instances), 0)\n\n\twaiter := ec2.NewInstanceExistsWaiter(ec2Client)\n\terr = waiter.Wait(\n\t\tcontext.Background(),\n\t\t&ec2.DescribeInstancesInput{\n\t\t\tFilters: []types.Filter{\n\t\t\t\t{\n\t\t\t\t\tName:   aws.String(\"instance-id\"),\n\t\t\t\t\tValues: []string{*runResult.Instances[0].InstanceId},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t42*time.Minute,\n\t)\n\trequire.NoError(t, err)\n\n\t// Add test tag to the created instance\n\t_, err = ec2Client.CreateTags(context.Background(), &ec2.CreateTagsInput{\n\t\tResources: []string{*runResult.Instances[0].InstanceId},\n\t\tTags: []types.Tag{\n\t\t\t{\n\t\t\t\tKey:   aws.String(\"Name\"),\n\t\t\t\tValue: aws.String(name),\n\t\t\t},\n\t\t},\n\t})\n\trequire.NoError(t, err)\n\n\t// EC2 Instance must be in a running before this function returns\n\trunningWaiter := ec2.NewInstanceRunningWaiter(ec2Client)\n\terr = runningWaiter.Wait(context.Background(), &ec2.DescribeInstancesInput{\n\t\tFilters: []types.Filter{\n\t\t\t{\n\t\t\t\tName:   aws.String(\"instance-id\"),\n\t\t\t\tValues: []string{*runResult.Instances[0].InstanceId},\n\t\t\t},\n\t\t},\n\t}, 42*time.Minute)\n\trequire.NoError(t, err)\n\n\treturn runResult.Instances[0]\n}\n\nfunc terminateEc2InstancesByName(t *testing.T, region string, names []string) {\n\tfor _, name := range names {\n\t\tinstanceIds := GetEc2InstanceIdsByTag(t, region, \"Name\", name)\n\t\tfor _, instanceId := range instanceIds {\n\t\t\tTerminateInstance(t, region, instanceId)\n\t\t}\n\t}\n}\n\nfunc deleteAutoScalingGroup(t *testing.T, name string, region string) {\n\t// We have to scale ASG down to 0 before we can delete it\n\tscaleAsgToZero(t, name, region)\n\n\tasgClient := NewAsgClient(t, region)\n\tinput := &autoscaling.DeleteAutoScalingGroupInput{AutoScalingGroupName: aws.String(name)}\n\t_, err := asgClient.DeleteAutoScalingGroup(context.Background(), input)\n\trequire.NoError(t, err)\n\n\twaiter := autoscaling.NewGroupNotExistsWaiter(asgClient)\n\terr = waiter.Wait(context.Background(), &autoscaling.DescribeAutoScalingGroupsInput{\n\t\tAutoScalingGroupNames: []string{name},\n\t}, 40*time.Minute)\n\trequire.NoError(t, err)\n\n\tec2Client := NewEc2Client(t, region)\n\t_, err = ec2Client.DeleteLaunchTemplate(context.Background(), &ec2.DeleteLaunchTemplateInput{\n\t\tLaunchTemplateName: aws.String(name),\n\t})\n\trequire.NoError(t, err)\n}\n\nfunc scaleAsgToZero(t *testing.T, name string, region string) {\n\tasgClient := NewAsgClient(t, region)\n\tinput := &autoscaling.UpdateAutoScalingGroupInput{\n\t\tAutoScalingGroupName: aws.String(name),\n\t\tDesiredCapacity:      aws.Int32(0),\n\t\tMinSize:              aws.Int32(0),\n\t\tMaxSize:              aws.Int32(0),\n\t}\n\t_, err := asgClient.UpdateAutoScalingGroup(context.Background(), input)\n\trequire.NoError(t, err)\n\tWaitForCapacity(t, name, region, 40, 15*time.Second)\n\n\t// There is an eventual consistency bug where even though the ASG is scaled down, AWS sometimes still views a\n\t// scaling activity so we add a 5-second pause here to work around it.\n\ttime.Sleep(5 * time.Second)\n}\n"
  },
  {
    "path": "modules/aws/auth.go",
    "content": "package aws\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\t\"github.com/aws/aws-sdk-go-v2/config\"\n\t\"github.com/aws/aws-sdk-go-v2/credentials\"\n\t\"github.com/aws/aws-sdk-go-v2/credentials/stscreds\"\n\t\"github.com/aws/aws-sdk-go-v2/service/iam\"\n\t\"github.com/aws/aws-sdk-go-v2/service/iam/types\"\n\t\"github.com/aws/aws-sdk-go-v2/service/sts\"\n\t\"github.com/pquerna/otp/totp\"\n)\n\nconst (\n\tAuthAssumeRoleEnvVar = \"TERRATEST_IAM_ROLE\" // OS environment variable name through which Assume Role ARN may be passed for authentication\n)\n\n// NewAuthenticatedSession creates an AWS Config following to standard AWS authentication workflow.\n// If AuthAssumeIamRoleEnvVar environment variable is set, assumes IAM role specified in it.\nfunc NewAuthenticatedSession(region string) (*aws.Config, error) {\n\tif assumeRoleArn, ok := os.LookupEnv(AuthAssumeRoleEnvVar); ok {\n\t\treturn NewAuthenticatedSessionFromRole(region, assumeRoleArn)\n\t} else {\n\t\treturn NewAuthenticatedSessionFromDefaultCredentials(region)\n\t}\n}\n\n// NewAuthenticatedSessionFromDefaultCredentials gets an AWS Config, checking that the user has credentials properly configured in their environment.\nfunc NewAuthenticatedSessionFromDefaultCredentials(region string) (*aws.Config, error) {\n\tcfg, err := config.LoadDefaultConfig(context.Background(), config.WithRegion(region))\n\tif err != nil {\n\t\treturn nil, CredentialsError{UnderlyingErr: err}\n\t}\n\n\treturn &cfg, nil\n}\n\n// NewAuthenticatedSessionFromRole returns a new AWS Config after assuming the\n// role whose ARN is provided in roleARN. If the credentials are not properly\n// configured in the underlying environment, an error is returned.\nfunc NewAuthenticatedSessionFromRole(region string, roleARN string) (*aws.Config, error) {\n\tcfg, err := NewAuthenticatedSessionFromDefaultCredentials(region)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tclient := sts.NewFromConfig(*cfg)\n\n\troleProvider := stscreds.NewAssumeRoleProvider(client, roleARN)\n\tretrieve, err := roleProvider.Retrieve(context.Background())\n\tif err != nil {\n\t\treturn nil, CredentialsError{UnderlyingErr: err}\n\t}\n\n\treturn &aws.Config{\n\t\tRegion: region,\n\t\tCredentials: aws.NewCredentialsCache(credentials.StaticCredentialsProvider{\n\t\t\tValue: retrieve,\n\t\t}),\n\t}, nil\n}\n\n// CreateAwsSessionWithCreds creates a new AWS Config using explicit credentials. This is useful if you want to create an IAM User dynamically and\n// create an AWS Config authenticated as the new IAM User.\nfunc CreateAwsSessionWithCreds(region string, accessKeyID string, secretAccessKey string) (*aws.Config, error) {\n\treturn &aws.Config{\n\t\tRegion:      region,\n\t\tCredentials: aws.NewCredentialsCache(credentials.NewStaticCredentialsProvider(accessKeyID, secretAccessKey, \"\")),\n\t}, nil\n}\n\n// CreateAwsSessionWithMfa creates a new AWS Config authenticated using an MFA token retrieved using the given STS client and MFA Device.\nfunc CreateAwsSessionWithMfa(region string, stsClient *sts.Client, mfaDevice *types.VirtualMFADevice) (*aws.Config, error) {\n\ttokenCode, err := GetTimeBasedOneTimePassword(mfaDevice)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\toutput, err := stsClient.GetSessionToken(context.Background(), &sts.GetSessionTokenInput{\n\t\tSerialNumber: mfaDevice.SerialNumber,\n\t\tTokenCode:    aws.String(tokenCode),\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\taccessKeyID := *output.Credentials.AccessKeyId\n\tsecretAccessKey := *output.Credentials.SecretAccessKey\n\tsessionToken := *output.Credentials.SessionToken\n\n\treturn &aws.Config{\n\t\tRegion:      region,\n\t\tCredentials: aws.NewCredentialsCache(credentials.NewStaticCredentialsProvider(accessKeyID, secretAccessKey, sessionToken)),\n\t}, nil\n}\n\n// GetTimeBasedOneTimePassword gets a One-Time Password from the given mfaDevice. Per the RFC 6238 standard, this value will be different every 30 seconds.\nfunc GetTimeBasedOneTimePassword(mfaDevice *types.VirtualMFADevice) (string, error) {\n\tbase32StringSeed := string(mfaDevice.Base32StringSeed)\n\n\totp, err := totp.GenerateCode(base32StringSeed, time.Now())\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn otp, nil\n}\n\n// ReadPasswordPolicyMinPasswordLength returns the minimal password length.\nfunc ReadPasswordPolicyMinPasswordLength(iamClient *iam.Client) (int, error) {\n\toutput, err := iamClient.GetAccountPasswordPolicy(context.Background(), &iam.GetAccountPasswordPolicyInput{})\n\tif err != nil {\n\t\treturn -1, err\n\t}\n\n\treturn int(*output.PasswordPolicy.MinimumPasswordLength), nil\n}\n\n// CredentialsError is an error that occurs because AWS credentials can't be found.\ntype CredentialsError struct {\n\tUnderlyingErr error\n}\n\nfunc (err CredentialsError) Error() string {\n\treturn fmt.Sprintf(\"Error finding AWS credentials. Did you set the AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables or configure an AWS profile? Underlying error: %v\", err.UnderlyingErr)\n}\n"
  },
  {
    "path": "modules/aws/aws.go",
    "content": "// Package aws allows to interact with resources on Amazon Web Services.\npackage aws\n"
  },
  {
    "path": "modules/aws/cloudwatch.go",
    "content": "package aws\n\nimport (\n\t\"context\"\n\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\t\"github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n)\n\n// GetCloudWatchLogEntries returns the CloudWatch log messages in the given region for the given log stream and log group.\nfunc GetCloudWatchLogEntries(t testing.TestingT, awsRegion string, logStreamName string, logGroupName string) []string {\n\tout, err := GetCloudWatchLogEntriesE(t, awsRegion, logStreamName, logGroupName)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn out\n}\n\n// GetCloudWatchLogEntriesE returns the CloudWatch log messages in the given region for the given log stream and log group.\nfunc GetCloudWatchLogEntriesE(t testing.TestingT, awsRegion string, logStreamName string, logGroupName string) ([]string, error) {\n\tclient, err := NewCloudWatchLogsClientE(t, awsRegion)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\toutput, err := client.GetLogEvents(context.Background(), &cloudwatchlogs.GetLogEventsInput{\n\t\tLogGroupName:  aws.String(logGroupName),\n\t\tLogStreamName: aws.String(logStreamName),\n\t})\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar entries []string\n\tfor _, event := range output.Events {\n\t\tentries = append(entries, *event.Message)\n\t}\n\n\treturn entries, nil\n}\n\n// NewCloudWatchLogsClient creates a new CloudWatch Logs client.\nfunc NewCloudWatchLogsClient(t testing.TestingT, region string) *cloudwatchlogs.Client {\n\tclient, err := NewCloudWatchLogsClientE(t, region)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn client\n}\n\n// NewCloudWatchLogsClientE creates a new CloudWatch Logs client.\nfunc NewCloudWatchLogsClientE(t testing.TestingT, region string) (*cloudwatchlogs.Client, error) {\n\tsess, err := NewAuthenticatedSession(region)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn cloudwatchlogs.NewFromConfig(*sess), nil\n}\n"
  },
  {
    "path": "modules/aws/dynamodb.go",
    "content": "package aws\n\nimport (\n\t\"context\"\n\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\t\"github.com/aws/aws-sdk-go-v2/service/dynamodb\"\n\t\"github.com/aws/aws-sdk-go-v2/service/dynamodb/types\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// GetDynamoDbTableTags fetches resource tags of a specified dynamoDB table. This will fail the test if there are any errors\nfunc GetDynamoDbTableTags(t testing.TestingT, region string, tableName string) []types.Tag {\n\ttags, err := GetDynamoDbTableTagsE(t, region, tableName)\n\trequire.NoError(t, err)\n\treturn tags\n}\n\n// GetDynamoDbTableTagsE fetches resource tags of a specified dynamoDB table.\nfunc GetDynamoDbTableTagsE(t testing.TestingT, region string, tableName string) ([]types.Tag, error) {\n\ttable, err := GetDynamoDBTableE(t, region, tableName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tclient, err := NewDynamoDBClientE(t, region)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tout, err := client.ListTagsOfResource(context.Background(), &dynamodb.ListTagsOfResourceInput{\n\t\tResourceArn: table.TableArn,\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out.Tags, err\n}\n\n// GetDynamoDBTableTimeToLive fetches information about the TTL configuration of a specified dynamoDB table. This will fail the test if there are any errors.\nfunc GetDynamoDBTableTimeToLive(t testing.TestingT, region string, tableName string) *types.TimeToLiveDescription {\n\tttl, err := GetDynamoDBTableTimeToLiveE(t, region, tableName)\n\trequire.NoError(t, err)\n\treturn ttl\n}\n\n// GetDynamoDBTableTimeToLiveE fetches information about the TTL configuration of a specified dynamoDB table.\nfunc GetDynamoDBTableTimeToLiveE(t testing.TestingT, region string, tableName string) (*types.TimeToLiveDescription, error) {\n\tclient, err := NewDynamoDBClientE(t, region)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tout, err := client.DescribeTimeToLive(context.Background(), &dynamodb.DescribeTimeToLiveInput{\n\t\tTableName: aws.String(tableName),\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out.TimeToLiveDescription, err\n}\n\n// GetDynamoDBTable fetches information about the specified dynamoDB table. This will fail the test if there are any errors.\nfunc GetDynamoDBTable(t testing.TestingT, region string, tableName string) *types.TableDescription {\n\ttable, err := GetDynamoDBTableE(t, region, tableName)\n\trequire.NoError(t, err)\n\treturn table\n}\n\n// GetDynamoDBTableE fetches information about the specified dynamoDB table.\nfunc GetDynamoDBTableE(t testing.TestingT, region string, tableName string) (*types.TableDescription, error) {\n\tclient, err := NewDynamoDBClientE(t, region)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tout, err := client.DescribeTable(context.Background(), &dynamodb.DescribeTableInput{\n\t\tTableName: aws.String(tableName),\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out.Table, err\n}\n\n// NewDynamoDBClient creates a DynamoDB client.\nfunc NewDynamoDBClient(t testing.TestingT, region string) *dynamodb.Client {\n\tclient, err := NewDynamoDBClientE(t, region)\n\trequire.NoError(t, err)\n\treturn client\n}\n\n// NewDynamoDBClientE creates a DynamoDB client.\nfunc NewDynamoDBClientE(t testing.TestingT, region string) (*dynamodb.Client, error) {\n\tsess, err := NewAuthenticatedSession(region)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn dynamodb.NewFromConfig(*sess), nil\n}\n"
  },
  {
    "path": "modules/aws/ebs.go",
    "content": "package aws\n\nimport (\n\t\"context\"\n\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\t\"github.com/aws/aws-sdk-go-v2/service/ec2\"\n\t\"github.com/gruntwork-io/terratest/modules/logger\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n)\n\n// DeleteEbsSnapshot deletes the given EBS snapshot\nfunc DeleteEbsSnapshot(t testing.TestingT, region string, snapshot string) {\n\terr := DeleteEbsSnapshotE(t, region, snapshot)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n\n// DeleteEbsSnapshotE deletes the given EBS snapshot\nfunc DeleteEbsSnapshotE(t testing.TestingT, region string, snapshot string) error {\n\tlogger.Default.Logf(t, \"Deleting EBS snapshot %s\", snapshot)\n\tec2Client, err := NewEc2ClientE(t, region)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t_, err = ec2Client.DeleteSnapshot(context.Background(), &ec2.DeleteSnapshotInput{\n\t\tSnapshotId: aws.String(snapshot),\n\t})\n\treturn err\n}\n"
  },
  {
    "path": "modules/aws/ec2-files.go",
    "content": "package aws\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/gruntwork-io/terratest/modules/files\"\n\t\"github.com/gruntwork-io/terratest/modules/ssh\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/hashicorp/go-multierror\"\n)\n\n// RemoteFileSpecification describes which files you want to copy from your instances\ntype RemoteFileSpecification struct {\n\tAsgNames               []string            //ASGs where our instances will be\n\tRemotePathToFileFilter map[string][]string //A map of the files to fetch, where the keys are directories on the remote host and the values are filters for what files to fetch from the directory. The filters support bash-style wildcards.\n\tUseSudo                bool\n\tSshUser                string\n\tKeyPair                *Ec2Keypair\n\tLocalDestinationDir    string //base path where to store downloaded artifacts locally. The final path of each resource will include the ip of the host and the name of the immediate parent folder.\n}\n\n// FetchContentsOfFileFromInstance looks up the public IP address of the EC2 Instance with the given ID, connects to\n// the Instance via SSH using the given username and Key Pair, fetches the contents of the file at the given path\n// (using sudo if useSudo is true), and returns the contents of that file as a string.\nfunc FetchContentsOfFileFromInstance(t testing.TestingT, awsRegion string, sshUserName string, keyPair *Ec2Keypair, instanceID string, useSudo bool, filePath string) string {\n\tout, err := FetchContentsOfFileFromInstanceE(t, awsRegion, sshUserName, keyPair, instanceID, useSudo, filePath)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn out\n}\n\n// FetchContentsOfFileFromInstanceE looks up the public IP address of the EC2 Instance with the given ID, connects to\n// the Instance via SSH using the given username and Key Pair, fetches the contents of the file at the given path\n// (using sudo if useSudo is true), and returns the contents of that file as a string.\nfunc FetchContentsOfFileFromInstanceE(t testing.TestingT, awsRegion string, sshUserName string, keyPair *Ec2Keypair, instanceID string, useSudo bool, filePath string) (string, error) {\n\tpublicIp, err := GetPublicIpOfEc2InstanceE(t, instanceID, awsRegion)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\thost := ssh.Host{\n\t\tSshUserName: sshUserName,\n\t\tSshKeyPair:  keyPair.KeyPair,\n\t\tHostname:    publicIp,\n\t}\n\n\treturn ssh.FetchContentsOfFileE(t, host, useSudo, filePath)\n}\n\n// FetchContentsOfFilesFromInstance looks up the public IP address of the EC2 Instance with the given ID, connects to\n// the Instance via SSH using the given username and Key Pair, fetches the contents of the files at the given paths\n// (using sudo if useSudo is true), and returns a map from file path to the contents of that file as a string.\nfunc FetchContentsOfFilesFromInstance(t testing.TestingT, awsRegion string, sshUserName string, keyPair *Ec2Keypair, instanceID string, useSudo bool, filePaths ...string) map[string]string {\n\tout, err := FetchContentsOfFilesFromInstanceE(t, awsRegion, sshUserName, keyPair, instanceID, useSudo, filePaths...)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn out\n}\n\n// FetchContentsOfFilesFromInstanceE looks up the public IP address of the EC2 Instance with the given ID, connects to\n// the Instance via SSH using the given username and Key Pair, fetches the contents of the files at the given paths\n// (using sudo if useSudo is true), and returns a map from file path to the contents of that file as a string.\nfunc FetchContentsOfFilesFromInstanceE(t testing.TestingT, awsRegion string, sshUserName string, keyPair *Ec2Keypair, instanceID string, useSudo bool, filePaths ...string) (map[string]string, error) {\n\tpublicIp, err := GetPublicIpOfEc2InstanceE(t, instanceID, awsRegion)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\thost := ssh.Host{\n\t\tSshUserName: sshUserName,\n\t\tSshKeyPair:  keyPair.KeyPair,\n\t\tHostname:    publicIp,\n\t}\n\n\treturn ssh.FetchContentsOfFilesE(t, host, useSudo, filePaths...)\n}\n\n// FetchContentsOfFileFromAsg looks up the EC2 Instances in the given ASG, looks up the public IPs of those EC2\n// Instances, connects to each Instance via SSH using the given username and Key Pair, fetches the contents of the file\n// at the given path (using sudo if useSudo is true), and returns a map from Instance ID to the contents of that file\n// as a string.\nfunc FetchContentsOfFileFromAsg(t testing.TestingT, awsRegion string, sshUserName string, keyPair *Ec2Keypair, asgName string, useSudo bool, filePath string) map[string]string {\n\tout, err := FetchContentsOfFileFromAsgE(t, awsRegion, sshUserName, keyPair, asgName, useSudo, filePath)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn out\n}\n\n// FetchContentsOfFileFromAsgE looks up the EC2 Instances in the given ASG, looks up the public IPs of those EC2\n// Instances, connects to each Instance via SSH using the given username and Key Pair, fetches the contents of the file\n// at the given path (using sudo if useSudo is true), and returns a map from Instance ID to the contents of that file\n// as a string.\nfunc FetchContentsOfFileFromAsgE(t testing.TestingT, awsRegion string, sshUserName string, keyPair *Ec2Keypair, asgName string, useSudo bool, filePath string) (map[string]string, error) {\n\tinstanceIDs, err := GetInstanceIdsForAsgE(t, asgName, awsRegion)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tinstanceIdToContents := map[string]string{}\n\n\tfor _, instanceID := range instanceIDs {\n\t\tcontents, err := FetchContentsOfFileFromInstanceE(t, awsRegion, sshUserName, keyPair, instanceID, useSudo, filePath)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tinstanceIdToContents[instanceID] = contents\n\t}\n\n\treturn instanceIdToContents, err\n}\n\n// FetchContentsOfFilesFromAsg looks up the EC2 Instances in the given ASG, looks up the public IPs of those EC2\n// Instances, connects to each Instance via SSH using the given username and Key Pair, fetches the contents of the files\n// at the given paths (using sudo if useSudo is true), and returns a map from Instance ID to a map of file path to the\n// contents of that file as a string.\nfunc FetchContentsOfFilesFromAsg(t testing.TestingT, awsRegion string, sshUserName string, keyPair *Ec2Keypair, asgName string, useSudo bool, filePaths ...string) map[string]map[string]string {\n\tout, err := FetchContentsOfFilesFromAsgE(t, awsRegion, sshUserName, keyPair, asgName, useSudo, filePaths...)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn out\n}\n\n// FetchContentsOfFilesFromAsgE looks up the EC2 Instances in the given ASG, looks up the public IPs of those EC2\n// Instances, connects to each Instance via SSH using the given username and Key Pair, fetches the contents of the files\n// at the given paths (using sudo if useSudo is true), and returns a map from Instance ID to a map of file path to the\n// contents of that file as a string.\nfunc FetchContentsOfFilesFromAsgE(t testing.TestingT, awsRegion string, sshUserName string, keyPair *Ec2Keypair, asgName string, useSudo bool, filePaths ...string) (map[string]map[string]string, error) {\n\tinstanceIDs, err := GetInstanceIdsForAsgE(t, asgName, awsRegion)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tinstanceIdToFilePathToContents := map[string]map[string]string{}\n\n\tfor _, instanceID := range instanceIDs {\n\t\tcontents, err := FetchContentsOfFilesFromInstanceE(t, awsRegion, sshUserName, keyPair, instanceID, useSudo, filePaths...)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tinstanceIdToFilePathToContents[instanceID] = contents\n\t}\n\n\treturn instanceIdToFilePathToContents, err\n}\n\n// FetchFilesFromInstance looks up the EC2 Instances in the given ASG, looks up the public IPs of those EC2\n// Instances, connects to each Instance via SSH using the given username and Key Pair, downloads the files\n// matching filenameFilters at the given remoteDirectory (using sudo if useSudo is true), and stores the files locally\n// at localDirectory/<publicip>/<remoteFolderName>\nfunc FetchFilesFromInstance(t testing.TestingT, awsRegion string, sshUserName string, keyPair *Ec2Keypair, instanceID string, useSudo bool, remoteDirectory string, localDirectory string, filenameFilters []string) {\n\terr := FetchFilesFromInstanceE(t, awsRegion, sshUserName, keyPair, instanceID, useSudo, remoteDirectory, localDirectory, filenameFilters)\n\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n\n// FetchFilesFromInstanceE looks up the EC2 Instances in the given ASG, looks up the public IPs of those EC2\n// Instances, connects to each Instance via SSH using the given username and Key Pair, downloads the files\n// matching filenameFilters at the given remoteDirectory (using sudo if useSudo is true), and stores the files locally\n// at localDirectory/<publicip>/<remoteFolderName>\nfunc FetchFilesFromInstanceE(t testing.TestingT, awsRegion string, sshUserName string, keyPair *Ec2Keypair, instanceID string, useSudo bool, remoteDirectory string, localDirectory string, filenameFilters []string) error {\n\tpublicIp, err := GetPublicIpOfEc2InstanceE(t, instanceID, awsRegion)\n\n\tif err != nil {\n\t\treturn err\n\t}\n\n\thost := ssh.Host{\n\t\tHostname:    publicIp,\n\t\tSshUserName: sshUserName,\n\t\tSshKeyPair:  keyPair.KeyPair,\n\t}\n\n\tfinalLocalDestDir := filepath.Join(localDirectory, publicIp, filepath.Base(remoteDirectory))\n\n\tif !files.FileExists(finalLocalDestDir) {\n\t\tos.MkdirAll(finalLocalDestDir, 0755)\n\t}\n\n\tscpOptions := ssh.ScpDownloadOptions{\n\t\tRemoteHost:      host,\n\t\tRemoteDir:       remoteDirectory,\n\t\tLocalDir:        finalLocalDestDir,\n\t\tFileNameFilters: filenameFilters,\n\t}\n\n\treturn ssh.ScpDirFromE(t, scpOptions, useSudo)\n}\n\n// FetchFilesFromAsgs looks up the EC2 Instances in all the ASGs given in the RemoteFileSpecification,\n// looks up the public IPs of those EC2 Instances, connects to each Instance via SSH using the given\n// username and Key Pair, downloads the files matching filenameFilters at the given\n// remoteDirectory (using sudo if useSudo is true), and stores the files locally at\n// localDirectory/<publicip>/<remoteFolderName>\nfunc FetchFilesFromAsgs(t testing.TestingT, awsRegion string, spec RemoteFileSpecification) {\n\terr := FetchFilesFromAsgsE(t, awsRegion, spec)\n\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n\n// FetchFilesFromAsgsE looks up the EC2 Instances in all the ASGs given in the RemoteFileSpecification,\n// looks up the public IPs of those EC2 Instances, connects to each Instance via SSH using the given\n// username and Key Pair, downloads the files matching filenameFilters at the given\n// remoteDirectory (using sudo if useSudo is true), and stores the files locally at\n// localDirectory/<publicip>/<remoteFolderName>\nfunc FetchFilesFromAsgsE(t testing.TestingT, awsRegion string, spec RemoteFileSpecification) error {\n\tvar errorsOccurred = new(multierror.Error)\n\n\tfor _, curAsg := range spec.AsgNames {\n\t\tfor curRemoteDir, fileFilters := range spec.RemotePathToFileFilter {\n\n\t\t\tinstanceIDs, err := GetInstanceIdsForAsgE(t, curAsg, awsRegion)\n\t\t\tif err != nil {\n\t\t\t\terrorsOccurred = multierror.Append(errorsOccurred, err)\n\t\t\t} else {\n\t\t\t\tfor _, instanceID := range instanceIDs {\n\t\t\t\t\terr = FetchFilesFromInstanceE(t, awsRegion, spec.SshUser, spec.KeyPair, instanceID, spec.UseSudo, curRemoteDir, spec.LocalDestinationDir, fileFilters)\n\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\terrorsOccurred = multierror.Append(errorsOccurred, err)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn errorsOccurred.ErrorOrNil()\n}\n"
  },
  {
    "path": "modules/aws/ec2-syslog.go",
    "content": "package aws\n\nimport (\n\t\"context\"\n\t\"encoding/base64\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\t\"github.com/aws/aws-sdk-go-v2/service/ec2\"\n\t\"github.com/gruntwork-io/terratest/modules/logger\"\n\t\"github.com/gruntwork-io/terratest/modules/retry\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n)\n\n// GetSyslogForInstance (Deprecated) See the FetchContentsOfFileFromInstance method for a more powerful solution.\n//\n// GetSyslogForInstance gets the syslog for the Instance with the given ID in the given region. This should be available ~1 minute after an\n// Instance boots and is very useful for debugging boot-time issues, such as an error in User Data.\nfunc GetSyslogForInstance(t testing.TestingT, instanceID string, awsRegion string) string {\n\tout, err := GetSyslogForInstanceE(t, instanceID, awsRegion)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn out\n}\n\n// GetSyslogForInstanceE (Deprecated) See the FetchContentsOfFileFromInstanceE method for a more powerful solution.\n//\n// GetSyslogForInstanceE gets the syslog for the Instance with the given ID in the given region. This should be available ~1 minute after an\n// Instance boots and is very useful for debugging boot-time issues, such as an error in User Data.\nfunc GetSyslogForInstanceE(t testing.TestingT, instanceID string, region string) (string, error) {\n\tdescription := fmt.Sprintf(\"Fetching syslog for Instance %s in %s\", instanceID, region)\n\tmaxRetries := 120\n\ttimeBetweenRetries := 5 * time.Second\n\n\tlogger.Default.Logf(t, \"%s\", description)\n\n\tclient, err := NewEc2ClientE(t, region)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tinput := ec2.GetConsoleOutputInput{\n\t\tInstanceId: aws.String(instanceID),\n\t}\n\n\tsyslogB64, err := retry.DoWithRetryE(t, description, maxRetries, timeBetweenRetries, func() (string, error) {\n\t\tout, err := client.GetConsoleOutput(context.Background(), &input)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\tsyslog := aws.ToString(out.Output)\n\t\tif syslog == \"\" {\n\t\t\treturn \"\", fmt.Errorf(\"syslog is not yet available for instance %s in %s\", instanceID, region)\n\t\t}\n\n\t\treturn syslog, nil\n\t})\n\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tsyslogBytes, err := base64.StdEncoding.DecodeString(syslogB64)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn string(syslogBytes), nil\n}\n\n// GetSyslogForInstancesInAsg (Deprecated) See the FetchContentsOfFilesFromAsg method for a more powerful solution.\n//\n// GetSyslogForInstancesInAsg gets the syslog for each of the Instances in the given ASG in the given region. These logs should be available ~1\n// minute after the Instance boots and are very useful for debugging boot-time issues, such as an error in User Data.\n// Returns a map of Instance ID -> Syslog for that Instance.\nfunc GetSyslogForInstancesInAsg(t testing.TestingT, asgName string, awsRegion string) map[string]string {\n\tout, err := GetSyslogForInstancesInAsgE(t, asgName, awsRegion)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn out\n}\n\n// GetSyslogForInstancesInAsgE (Deprecated) See the FetchContentsOfFilesFromAsgE method for a more powerful solution.\n//\n// GetSyslogForInstancesInAsgE gets the syslog for each of the Instances in the given ASG in the given region. These logs should be available ~1\n// minute after the Instance boots and are very useful for debugging boot-time issues, such as an error in User Data.\n// Returns a map of Instance ID -> Syslog for that Instance.\nfunc GetSyslogForInstancesInAsgE(t testing.TestingT, asgName string, awsRegion string) (map[string]string, error) {\n\tlogger.Default.Logf(t, \"Fetching syslog for each Instance in ASG %s in %s\", asgName, awsRegion)\n\n\tinstanceIDs, err := GetEc2InstanceIdsByTagE(t, awsRegion, \"aws:autoscaling:groupName\", asgName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tlogs := map[string]string{}\n\tfor _, id := range instanceIDs {\n\t\tsyslog, err := GetSyslogForInstanceE(t, id, awsRegion)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tlogs[id] = syslog\n\t}\n\n\treturn logs, nil\n}\n"
  },
  {
    "path": "modules/aws/ec2.go",
    "content": "package aws\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\t\"github.com/aws/aws-sdk-go-v2/service/ec2\"\n\t\"github.com/aws/aws-sdk-go-v2/service/ec2/types\"\n\t\"github.com/gruntwork-io/terratest/modules/logger\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// GetPrivateIpOfEc2Instance gets the private IP address of the given EC2 Instance in the given region.\nfunc GetPrivateIpOfEc2Instance(t testing.TestingT, instanceID string, awsRegion string) string {\n\tip, err := GetPrivateIpOfEc2InstanceE(t, instanceID, awsRegion)\n\trequire.NoError(t, err)\n\treturn ip\n}\n\n// GetPrivateIpOfEc2InstanceE gets the private IP address of the given EC2 Instance in the given region.\nfunc GetPrivateIpOfEc2InstanceE(t testing.TestingT, instanceID string, awsRegion string) (string, error) {\n\tips, err := GetPrivateIpsOfEc2InstancesE(t, []string{instanceID}, awsRegion)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tip, containsIP := ips[instanceID]\n\n\tif !containsIP {\n\t\treturn \"\", IpForEc2InstanceNotFound{InstanceId: instanceID, AwsRegion: awsRegion, Type: \"private\"}\n\t}\n\n\treturn ip, nil\n}\n\n// GetPrivateIpsOfEc2Instances gets the private IP address of the given EC2 Instance in the given region. Returns a map of instance ID to IP address.\nfunc GetPrivateIpsOfEc2Instances(t testing.TestingT, instanceIDs []string, awsRegion string) map[string]string {\n\tips, err := GetPrivateIpsOfEc2InstancesE(t, instanceIDs, awsRegion)\n\trequire.NoError(t, err)\n\treturn ips\n}\n\n// GetPrivateIpsOfEc2InstancesE gets the private IP address of the given EC2 Instance in the given region. Returns a map of instance ID to IP address.\nfunc GetPrivateIpsOfEc2InstancesE(t testing.TestingT, instanceIDs []string, awsRegion string) (map[string]string, error) {\n\tec2Client := NewEc2Client(t, awsRegion)\n\t// TODO: implement pagination for cases that extend beyond limit (1000 instances)\n\tinput := ec2.DescribeInstancesInput{InstanceIds: instanceIDs}\n\toutput, err := ec2Client.DescribeInstances(context.Background(), &input)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tips := map[string]string{}\n\n\tfor _, reservation := range output.Reservations {\n\t\tfor _, instance := range reservation.Instances {\n\t\t\tips[aws.ToString(instance.InstanceId)] = aws.ToString(instance.PrivateIpAddress)\n\t\t}\n\t}\n\n\treturn ips, nil\n}\n\n// GetPrivateHostnameOfEc2Instance gets the private IP address of the given EC2 Instance in the given region.\nfunc GetPrivateHostnameOfEc2Instance(t testing.TestingT, instanceID string, awsRegion string) string {\n\tip, err := GetPrivateHostnameOfEc2InstanceE(t, instanceID, awsRegion)\n\trequire.NoError(t, err)\n\treturn ip\n}\n\n// GetPrivateHostnameOfEc2InstanceE gets the private IP address of the given EC2 Instance in the given region.\nfunc GetPrivateHostnameOfEc2InstanceE(t testing.TestingT, instanceID string, awsRegion string) (string, error) {\n\thostnames, err := GetPrivateHostnamesOfEc2InstancesE(t, []string{instanceID}, awsRegion)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\thostname, containsHostname := hostnames[instanceID]\n\n\tif !containsHostname {\n\t\treturn \"\", HostnameForEc2InstanceNotFound{InstanceId: instanceID, AwsRegion: awsRegion, Type: \"private\"}\n\t}\n\n\treturn hostname, nil\n}\n\n// GetPrivateHostnamesOfEc2Instances gets the private IP address of the given EC2 Instance in the given region. Returns a map of instance ID to IP address.\nfunc GetPrivateHostnamesOfEc2Instances(t testing.TestingT, instanceIDs []string, awsRegion string) map[string]string {\n\tips, err := GetPrivateHostnamesOfEc2InstancesE(t, instanceIDs, awsRegion)\n\trequire.NoError(t, err)\n\treturn ips\n}\n\n// GetPrivateHostnamesOfEc2InstancesE gets the private IP address of the given EC2 Instance in the given region. Returns a map of instance ID to IP address.\nfunc GetPrivateHostnamesOfEc2InstancesE(t testing.TestingT, instanceIDs []string, awsRegion string) (map[string]string, error) {\n\tec2Client, err := NewEc2ClientE(t, awsRegion)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\t// TODO: implement pagination for cases that extend beyond limit (1000 instances)\n\tinput := ec2.DescribeInstancesInput{InstanceIds: instanceIDs}\n\toutput, err := ec2Client.DescribeInstances(context.Background(), &input)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\thostnames := map[string]string{}\n\n\tfor _, reservation := range output.Reservations {\n\t\tfor _, instance := range reservation.Instances {\n\t\t\thostnames[aws.ToString(instance.InstanceId)] = aws.ToString(instance.PrivateDnsName)\n\t\t}\n\t}\n\n\treturn hostnames, nil\n}\n\n// GetPublicIpOfEc2Instance gets the public IP address of the given EC2 Instance in the given region.\nfunc GetPublicIpOfEc2Instance(t testing.TestingT, instanceID string, awsRegion string) string {\n\tip, err := GetPublicIpOfEc2InstanceE(t, instanceID, awsRegion)\n\trequire.NoError(t, err)\n\treturn ip\n}\n\n// GetPublicIpOfEc2InstanceE gets the public IP address of the given EC2 Instance in the given region.\nfunc GetPublicIpOfEc2InstanceE(t testing.TestingT, instanceID string, awsRegion string) (string, error) {\n\tips, err := GetPublicIpsOfEc2InstancesE(t, []string{instanceID}, awsRegion)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tip, containsIP := ips[instanceID]\n\n\tif !containsIP {\n\t\treturn \"\", IpForEc2InstanceNotFound{InstanceId: instanceID, AwsRegion: awsRegion, Type: \"public\"}\n\t}\n\n\treturn ip, nil\n}\n\n// GetPublicIpsOfEc2Instances gets the public IP address of the given EC2 Instance in the given region. Returns a map of instance ID to IP address.\nfunc GetPublicIpsOfEc2Instances(t testing.TestingT, instanceIDs []string, awsRegion string) map[string]string {\n\tips, err := GetPublicIpsOfEc2InstancesE(t, instanceIDs, awsRegion)\n\trequire.NoError(t, err)\n\treturn ips\n}\n\n// GetPublicIpsOfEc2InstancesE gets the public IP address of the given EC2 Instance in the given region. Returns a map of instance ID to IP address.\nfunc GetPublicIpsOfEc2InstancesE(t testing.TestingT, instanceIDs []string, awsRegion string) (map[string]string, error) {\n\tec2Client := NewEc2Client(t, awsRegion)\n\t// TODO: implement pagination for cases that extend beyond limit (1000 instances)\n\tinput := ec2.DescribeInstancesInput{InstanceIds: instanceIDs}\n\toutput, err := ec2Client.DescribeInstances(context.Background(), &input)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tips := map[string]string{}\n\n\tfor _, reservation := range output.Reservations {\n\t\tfor _, instance := range reservation.Instances {\n\t\t\tips[aws.ToString(instance.InstanceId)] = aws.ToString(instance.PublicIpAddress)\n\t\t}\n\t}\n\n\treturn ips, nil\n}\n\n// GetEc2InstanceIdsByTag returns all the IDs of EC2 instances in the given region with the given tag.\nfunc GetEc2InstanceIdsByTag(t testing.TestingT, region string, tagName string, tagValue string) []string {\n\tout, err := GetEc2InstanceIdsByTagE(t, region, tagName, tagValue)\n\trequire.NoError(t, err)\n\treturn out\n}\n\n// GetEc2InstanceIdsByTagE returns all the IDs of EC2 instances in the given region with the given tag.\nfunc GetEc2InstanceIdsByTagE(t testing.TestingT, region string, tagName string, tagValue string) ([]string, error) {\n\tec2Filters := map[string][]string{\n\t\tfmt.Sprintf(\"tag:%s\", tagName): {tagValue},\n\t}\n\treturn GetEc2InstanceIdsByFiltersE(t, region, ec2Filters)\n}\n\n// GetEc2InstanceIdsByFilters returns all the IDs of EC2 instances in the given region which match to EC2 filter list\n// as per https://docs.aws.amazon.com/sdk-for-go/api/service/ec2/#DescribeInstancesInput.\nfunc GetEc2InstanceIdsByFilters(t testing.TestingT, region string, ec2Filters map[string][]string) []string {\n\tout, err := GetEc2InstanceIdsByFiltersE(t, region, ec2Filters)\n\trequire.NoError(t, err)\n\treturn out\n}\n\n// GetEc2InstanceIdsByFiltersE returns all the IDs of EC2 instances in the given region which match to EC2 filter list\n// as per https://docs.aws.amazon.com/sdk-for-go/api/service/ec2/#DescribeInstancesInput.\nfunc GetEc2InstanceIdsByFiltersE(t testing.TestingT, region string, ec2Filters map[string][]string) ([]string, error) {\n\tclient, err := NewEc2ClientE(t, region)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar ec2FilterList []types.Filter\n\n\tfor name, values := range ec2Filters {\n\t\tec2FilterList = append(ec2FilterList, types.Filter{Name: aws.String(name), Values: values})\n\t}\n\n\t// TODO: implement pagination for cases that extend beyond limit (1000 instances)\n\toutput, err := client.DescribeInstances(context.Background(), &ec2.DescribeInstancesInput{Filters: ec2FilterList})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar instanceIDs []string\n\n\tfor _, reservation := range output.Reservations {\n\t\tfor _, instance := range reservation.Instances {\n\t\t\tinstanceIDs = append(instanceIDs, *instance.InstanceId)\n\t\t}\n\t}\n\n\treturn instanceIDs, err\n}\n\n// GetTagsForEc2Instance returns all the tags for the given EC2 Instance.\nfunc GetTagsForEc2Instance(t testing.TestingT, region string, instanceID string) map[string]string {\n\ttags, err := GetTagsForEc2InstanceE(t, region, instanceID)\n\trequire.NoError(t, err)\n\treturn tags\n}\n\n// GetTagsForEc2InstanceE returns all the tags for the given EC2 Instance.\nfunc GetTagsForEc2InstanceE(t testing.TestingT, region string, instanceID string) (map[string]string, error) {\n\tclient, err := NewEc2ClientE(t, region)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tinput := ec2.DescribeTagsInput{\n\t\tFilters: []types.Filter{\n\t\t\t{\n\t\t\t\tName:   aws.String(\"resource-type\"),\n\t\t\t\tValues: []string{\"instance\"},\n\t\t\t},\n\t\t\t{\n\t\t\t\tName:   aws.String(\"resource-id\"),\n\t\t\t\tValues: []string{instanceID},\n\t\t\t},\n\t\t},\n\t}\n\n\tout, err := client.DescribeTags(context.Background(), &input)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\ttags := map[string]string{}\n\n\tfor _, tag := range out.Tags {\n\t\ttags[aws.ToString(tag.Key)] = aws.ToString(tag.Value)\n\t}\n\n\treturn tags, nil\n}\n\n// DeleteAmi deletes the given AMI in the given region.\nfunc DeleteAmi(t testing.TestingT, region string, imageID string) {\n\trequire.NoError(t, DeleteAmiE(t, region, imageID))\n}\n\n// DeleteAmiE deletes the given AMI in the given region.\nfunc DeleteAmiE(t testing.TestingT, region string, imageID string) error {\n\tlogger.Default.Logf(t, \"Deregistering AMI %s\", imageID)\n\n\tclient, err := NewEc2ClientE(t, region)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t_, err = client.DeregisterImage(context.Background(), &ec2.DeregisterImageInput{ImageId: aws.String(imageID)})\n\treturn err\n}\n\n// AddTagsToResource adds the tags to the given taggable AWS resource such as EC2, AMI or VPC.\nfunc AddTagsToResource(t testing.TestingT, region string, resource string, tags map[string]string) {\n\trequire.NoError(t, AddTagsToResourceE(t, region, resource, tags))\n}\n\n// AddTagsToResourceE adds the tags to the given taggable AWS resource such as EC2, AMI or VPC.\nfunc AddTagsToResourceE(t testing.TestingT, region string, resource string, tags map[string]string) error {\n\tclient, err := NewEc2ClientE(t, region)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar awsTags []types.Tag\n\tfor key, value := range tags {\n\t\tawsTags = append(awsTags, types.Tag{\n\t\t\tKey:   aws.String(key),\n\t\t\tValue: aws.String(value),\n\t\t})\n\t}\n\n\t_, err = client.CreateTags(context.Background(), &ec2.CreateTagsInput{\n\t\tResources: []string{resource},\n\t\tTags:      awsTags,\n\t})\n\n\treturn err\n}\n\n// TerminateInstance terminates the EC2 instance with the given ID in the given region.\nfunc TerminateInstance(t testing.TestingT, region string, instanceID string) {\n\trequire.NoError(t, TerminateInstanceE(t, region, instanceID))\n}\n\n// TerminateInstanceE terminates the EC2 instance with the given ID in the given region.\nfunc TerminateInstanceE(t testing.TestingT, region string, instanceID string) error {\n\tlogger.Default.Logf(t, \"Terminating Instance %s\", instanceID)\n\n\tclient, err := NewEc2ClientE(t, region)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t_, err = client.TerminateInstances(context.Background(), &ec2.TerminateInstancesInput{\n\t\tInstanceIds: []string{\n\t\t\tinstanceID,\n\t\t},\n\t})\n\n\treturn err\n}\n\n// GetAmiPubliclyAccessible returns whether the AMI is publicly accessible or not\nfunc GetAmiPubliclyAccessible(t testing.TestingT, awsRegion string, amiID string) bool {\n\toutput, err := GetAmiPubliclyAccessibleE(t, awsRegion, amiID)\n\trequire.NoError(t, err)\n\treturn output\n}\n\n// GetAmiPubliclyAccessibleE returns whether the AMI is publicly accessible or not\nfunc GetAmiPubliclyAccessibleE(t testing.TestingT, awsRegion string, amiID string) (bool, error) {\n\tlaunchPermissions, err := GetLaunchPermissionsForAmiE(t, awsRegion, amiID)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tfor _, launchPermission := range launchPermissions {\n\t\tif string(launchPermission.Group) == \"all\" {\n\t\t\treturn true, nil\n\t\t}\n\t}\n\treturn false, nil\n}\n\n// GetAccountsWithLaunchPermissionsForAmi returns list of accounts that the AMI is shared with\nfunc GetAccountsWithLaunchPermissionsForAmi(t testing.TestingT, awsRegion string, amiID string) []string {\n\toutput, err := GetAccountsWithLaunchPermissionsForAmiE(t, awsRegion, amiID)\n\trequire.NoError(t, err)\n\treturn output\n}\n\n// GetAccountsWithLaunchPermissionsForAmiE returns list of accounts that the AMI is shared with\nfunc GetAccountsWithLaunchPermissionsForAmiE(t testing.TestingT, awsRegion string, amiID string) ([]string, error) {\n\tvar accountIDs []string\n\tlaunchPermissions, err := GetLaunchPermissionsForAmiE(t, awsRegion, amiID)\n\tif err != nil {\n\t\treturn accountIDs, err\n\t}\n\tfor _, launchPermission := range launchPermissions {\n\t\tif aws.ToString(launchPermission.UserId) != \"\" {\n\t\t\taccountIDs = append(accountIDs, aws.ToString(launchPermission.UserId))\n\t\t}\n\t}\n\treturn accountIDs, nil\n}\n\n// GetLaunchPermissionsForAmiE returns launchPermissions as configured in AWS\nfunc GetLaunchPermissionsForAmiE(t testing.TestingT, awsRegion string, amiID string) ([]types.LaunchPermission, error) {\n\tclient := NewEc2Client(t, awsRegion)\n\tinput := &ec2.DescribeImageAttributeInput{\n\t\tAttribute: types.ImageAttributeNameLaunchPermission,\n\t\tImageId:   aws.String(amiID),\n\t}\n\n\toutput, err := client.DescribeImageAttribute(context.Background(), input)\n\tif err != nil {\n\t\treturn []types.LaunchPermission{}, err\n\t}\n\treturn output.LaunchPermissions, nil\n}\n\n// GetRecommendedInstanceType takes in a list of EC2 instance types (e.g., \"t2.micro\", \"t3.micro\") and returns the\n// first instance type in the list that is available in all Availability Zones (AZs) in the given region. If there's no\n// instance available in all AZs, this function exits with an error. This is useful because certain instance types,\n// such as t2.micro, are not available in some of the newer AZs, while t3.micro is not available in some of the older\n// AZs, and if you have code that needs to run on a \"small\" instance across all AZs in many different regions, you can\n// use this function to automatically figure out which instance type you should use.\n// This function will fail the test if there is an error.\nfunc GetRecommendedInstanceType(t testing.TestingT, region string, instanceTypeOptions []string) string {\n\tout, err := GetRecommendedInstanceTypeE(t, region, instanceTypeOptions)\n\trequire.NoError(t, err)\n\treturn out\n}\n\n// GetRecommendedInstanceTypeE takes in a list of EC2 instance types (e.g., \"t2.micro\", \"t3.micro\") and returns the\n// first instance type in the list that is available in all Availability Zones (AZs) in the given region. If there's no\n// instance available in all AZs, this function exits with an error. This is useful because certain instance types,\n// such as t2.micro, are not available in some of the newer AZs, while t3.micro is not available in some of the older\n// AZs. If you have code that needs to run on a \"small\" instance across all AZs in many different regions, you can\n// use this function to automatically figure out which instance type you should use.\nfunc GetRecommendedInstanceTypeE(t testing.TestingT, region string, instanceTypeOptions []string) (string, error) {\n\tclient, err := NewEc2ClientE(t, region)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn GetRecommendedInstanceTypeWithClientE(t, client, instanceTypeOptions)\n}\n\n// GetRecommendedInstanceTypeWithClientE takes in a list of EC2 instance types (e.g., \"t2.micro\", \"t3.micro\") and returns the\n// first instance type in the list that is available in all Availability Zones (AZs) in the given region. If there's no\n// instance available in all AZs, this function exits with an error. This is useful because certain instance types,\n// such as t2.micro, are not available in some of the newer AZs, while t3.micro is not available in some of the older\n// AZs. If you have code that needs to run on a \"small\" instance across all AZs in many different regions, you can\n// use this function to automatically figure out which instance type you should use.\n// This function expects an authenticated EC2 client from the AWS SDK Go library.\nfunc GetRecommendedInstanceTypeWithClientE(t testing.TestingT, ec2Client *ec2.Client, instanceTypeOptions []string) (string, error) {\n\tavailabilityZones, err := getAllAvailabilityZonesE(ec2Client)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tinstanceTypeOfferings, err := getInstanceTypeOfferingsE(ec2Client, instanceTypeOptions)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn pickRecommendedInstanceTypeE(availabilityZones, instanceTypeOfferings, instanceTypeOptions)\n}\n\n// pickRecommendedInstanceTypeE returns the first instance type from instanceTypeOptions that is available in all the\n// AZs in availabilityZones based on the availability data in instanceTypeOfferings. If none of the instance types are\n// available in all AZs, this function returns an error.\nfunc pickRecommendedInstanceTypeE(availabilityZones []string, instanceTypeOfferings []types.InstanceTypeOffering, instanceTypeOptions []string) (string, error) {\n\t// O(n^3) for the win!\n\tfor _, instanceType := range instanceTypeOptions {\n\t\tif instanceTypeExistsInAllAzs(instanceType, availabilityZones, instanceTypeOfferings) {\n\t\t\treturn instanceType, nil\n\t\t}\n\t}\n\n\treturn \"\", NoInstanceTypeError{InstanceTypeOptions: instanceTypeOptions, Azs: availabilityZones}\n}\n\n// instanceTypeExistsInAllAzs returns true if the given instance type exists in all the given availabilityZones based\n// on the availability data in instanceTypeOfferings\nfunc instanceTypeExistsInAllAzs(instanceType string, availabilityZones []string, instanceTypeOfferings []types.InstanceTypeOffering) bool {\n\tif len(availabilityZones) == 0 || len(instanceTypeOfferings) == 0 {\n\t\treturn false\n\t}\n\n\tfor _, az := range availabilityZones {\n\t\tif !hasOffering(instanceTypeOfferings, az, instanceType) {\n\t\t\treturn false\n\t\t}\n\t}\n\n\treturn true\n}\n\n// hasOffering returns true if the given availability zone and instance type are one of the offerings in\n// instanceTypeOfferings\nfunc hasOffering(instanceTypeOfferings []types.InstanceTypeOffering, availabilityZone string, instanceType string) bool {\n\tfor _, offering := range instanceTypeOfferings {\n\t\tif string(offering.InstanceType) == instanceType && aws.ToString(offering.Location) == availabilityZone {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// getInstanceTypeOfferingsE returns the instance types from the given list that are available in the region configured\n// in the given EC2 client\nfunc getInstanceTypeOfferingsE(client *ec2.Client, instanceTypeOptions []string) ([]types.InstanceTypeOffering, error) {\n\tinput := ec2.DescribeInstanceTypeOfferingsInput{\n\t\tLocationType: types.LocationTypeAvailabilityZone,\n\t\tFilters: []types.Filter{\n\t\t\t{\n\t\t\t\tName:   aws.String(\"instance-type\"),\n\t\t\t\tValues: instanceTypeOptions,\n\t\t\t},\n\t\t},\n\t}\n\n\tout, err := client.DescribeInstanceTypeOfferings(context.Background(), &input)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn out.InstanceTypeOfferings, nil\n}\n\n// getAllAvailabilityZonesE returns all the available AZs in the region configured in the given EC2 client\nfunc getAllAvailabilityZonesE(client *ec2.Client) ([]string, error) {\n\tinput := ec2.DescribeAvailabilityZonesInput{\n\t\tFilters: []types.Filter{\n\t\t\t{\n\t\t\t\tName:   aws.String(\"state\"),\n\t\t\t\tValues: []string{\"available\"},\n\t\t\t},\n\t\t},\n\t}\n\n\tout, err := client.DescribeAvailabilityZones(context.Background(), &input)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar azs []string\n\n\tfor _, az := range out.AvailabilityZones {\n\t\tazs = append(azs, aws.ToString(az.ZoneName))\n\t}\n\n\treturn azs, nil\n}\n\n// NewEc2Client creates an EC2 client.\nfunc NewEc2Client(t testing.TestingT, region string) *ec2.Client {\n\tclient, err := NewEc2ClientE(t, region)\n\trequire.NoError(t, err)\n\treturn client\n}\n\n// NewEc2ClientE creates an EC2 client.\nfunc NewEc2ClientE(t testing.TestingT, region string) (*ec2.Client, error) {\n\tsess, err := NewAuthenticatedSession(region)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn ec2.NewFromConfig(*sess), nil\n}\n"
  },
  {
    "path": "modules/aws/ec2_test.go",
    "content": "package aws\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\t\"github.com/aws/aws-sdk-go-v2/service/ec2/types\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestGetEc2InstanceIdsByTag(t *testing.T) {\n\tt.Parallel()\n\n\tregion := GetRandomStableRegion(t, nil, nil)\n\tids, err := GetEc2InstanceIdsByTagE(t, region, \"Name\", fmt.Sprintf(\"nonexistent-%s\", random.UniqueId()))\n\trequire.NoError(t, err)\n\tassert.Equal(t, 0, len(ids))\n}\n\nfunc TestGetEc2InstanceIdsByFilters(t *testing.T) {\n\tt.Parallel()\n\n\tregion := GetRandomStableRegion(t, nil, nil)\n\tfilters := map[string][]string{\n\t\t\"instance-state-name\": {\"running\", \"shutting-down\"},\n\t\t\"tag:Name\":            {fmt.Sprintf(\"nonexistent-%s\", random.UniqueId())},\n\t}\n\n\tids, err := GetEc2InstanceIdsByFiltersE(t, region, filters)\n\trequire.NoError(t, err)\n\tassert.Equal(t, 0, len(ids))\n}\n\nfunc TestGetRecommendedInstanceType(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tregion              string\n\t\tinstanceTypeOptions []string\n\t}{\n\t\t{\"eu-west-1\", []string{\"t2.micro\", \"t3.micro\"}},\n\t\t{\"ap-northeast-2\", []string{\"t2.micro\", \"t3.micro\"}},\n\t\t{\"us-east-1\", []string{\"t2.large\", \"t3.large\"}},\n\t}\n\n\tfor _, testCase := range testCases {\n\t\t// The following is necessary to make sure testCase's values don't get updated due to concurrency within the\n\t\t// scope of t.Run(..) below. https://golang.org/doc/faq#closures_and_goroutines\n\t\ttestCase := testCase\n\n\t\tt.Run(fmt.Sprintf(\"%s-%s\", testCase.region, strings.Join(testCase.instanceTypeOptions, \"-\")), func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tinstanceType := GetRecommendedInstanceType(t, testCase.region, testCase.instanceTypeOptions)\n\t\t\t// We could hard-code the expected result (e.g., as of July 2020, we expect eu-west-1 to return t2.micro\n\t\t\t// and ap-northeast-2 to return t3.micro), but the result will likely change over time, so to avoid a\n\t\t\t// brittle test, we simply check that we get _one_ result. Combined with the unit test below, this hopefully\n\t\t\t// is enough to be confident this function works correctly.\n\t\t\tassert.Contains(t, testCase.instanceTypeOptions, instanceType)\n\t\t})\n\t}\n}\n\nfunc TestPickRecommendedInstanceTypeHappyPath(t *testing.T) {\n\ttestCases := []struct {\n\t\tname                  string\n\t\tavailabilityZones     []string\n\t\tinstanceTypeOfferings []types.InstanceTypeOffering\n\t\tinstanceTypeOptions   []string\n\t\texpected              string\n\t}{\n\t\t{\n\t\t\t\"One AZ, one instance type, available in one offering\",\n\t\t\t[]string{\"us-east-1a\"},\n\t\t\tofferings(map[string][]string{\"us-east-1a\": {\"t2.micro\"}}),\n\t\t\t[]string{\"t2.micro\"},\n\t\t\t\"t2.micro\",\n\t\t},\n\t\t{\n\t\t\t\"Three AZs, one instance type, available in all three offerings\",\n\t\t\t[]string{\"us-east-1a\", \"us-east-1b\", \"us-east-1c\"},\n\t\t\tofferings(map[string][]string{\"us-east-1a\": {\"t2.micro\"}, \"us-east-1b\": {\"t2.micro\"}, \"us-east-1c\": {\"t2.micro\"}}),\n\t\t\t[]string{\"t2.micro\"},\n\t\t\t\"t2.micro\",\n\t\t},\n\t\t{\n\t\t\t\"Three AZs, two instance types, first one available in all three offerings, the other not available at all\",\n\t\t\t[]string{\"us-east-1a\", \"us-east-1b\", \"us-east-1c\"},\n\t\t\tofferings(map[string][]string{\"us-east-1a\": {\"t2.micro\"}, \"us-east-1b\": {\"t2.micro\"}, \"us-east-1c\": {\"t2.micro\"}}),\n\t\t\t[]string{\"t2.micro\", \"t3.micro\"},\n\t\t\t\"t2.micro\",\n\t\t},\n\t\t{\n\t\t\t\"Three AZs, two instance types, first one available in all three offerings, the other only available in one offering in an unrequested AZ\",\n\t\t\t[]string{\"us-east-1a\", \"us-east-1b\", \"us-east-1c\"},\n\t\t\tofferings(map[string][]string{\"us-east-1a\": {\"t2.micro\"}, \"us-east-1b\": {\"t2.micro\"}, \"us-east-1c\": {\"t2.micro\"}, \"us-east-1d\": {\"t3.micro\"}}),\n\t\t\t[]string{\"t2.micro\", \"t3.micro\"},\n\t\t\t\"t2.micro\",\n\t\t},\n\t\t{\n\t\t\t\"Three AZs, two instance types, first one available in all three offerings, the other one available in only two offerings\",\n\t\t\t[]string{\"us-east-1a\", \"us-east-1b\", \"us-east-1c\"},\n\t\t\tofferings(map[string][]string{\"us-east-1a\": {\"t2.micro\", \"t3.micro\"}, \"us-east-1b\": {\"t2.micro\"}, \"us-east-1c\": {\"t2.micro\"}}),\n\t\t\t[]string{\"t2.micro\", \"t3.micro\"},\n\t\t\t\"t2.micro\",\n\t\t},\n\t\t{\n\t\t\t\"Three AZs, three instance types, first one available in two offerings, second in all three offerings, third in two offerings\",\n\t\t\t[]string{\"us-east-1a\", \"us-east-1b\", \"us-east-1c\"},\n\t\t\tofferings(map[string][]string{\"us-east-1a\": {\"t2.micro\", \"t3.micro\", \"t3.small\"}, \"us-east-1b\": {\"t3.micro\"}, \"us-east-1c\": {\"t2.micro\", \"t3.micro\", \"t3.small\"}}),\n\t\t\t[]string{\"t2.micro\", \"t3.micro\", \"t3.small\"},\n\t\t\t\"t3.micro\",\n\t\t},\n\t}\n\n\tfor _, testCase := range testCases {\n\t\t// The following is necessary to make sure testCase's values don't get updated due to concurrency within the\n\t\t// scope of t.Run(..) below. https://golang.org/doc/faq#closures_and_goroutines\n\t\ttestCase := testCase\n\n\t\tt.Run(testCase.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tactual, err := pickRecommendedInstanceTypeE(testCase.availabilityZones, testCase.instanceTypeOfferings, testCase.instanceTypeOptions)\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, testCase.expected, actual)\n\t\t})\n\t}\n}\n\nfunc TestPickRecommendedInstanceTypeErrors(t *testing.T) {\n\ttestCases := []struct {\n\t\tname                  string\n\t\tavailabilityZones     []string\n\t\tinstanceTypeOfferings []types.InstanceTypeOffering\n\t\tinstanceTypeOptions   []string\n\t}{\n\t\t{\n\t\t\t\"All params nil\",\n\t\t\tnil,\n\t\t\tnil,\n\t\t\tnil,\n\t\t},\n\t\t{\n\t\t\t\"No AZs, one instance type, no offerings\",\n\t\t\tnil,\n\t\t\tnil,\n\t\t\t[]string{\"t2.micro\"},\n\t\t},\n\t\t{\n\t\t\t\"One AZ, one instance type, no offerings\",\n\t\t\t[]string{\"us-east-1a\"},\n\t\t\tnil,\n\t\t\t[]string{\"t2.micro\"},\n\t\t},\n\t\t{\n\t\t\t\"Two AZs, one instance type, available in only one offering\",\n\t\t\t[]string{\"us-east-1a\", \"us-east-1b\"},\n\t\t\tofferings(map[string][]string{\"us-east-1a\": {\"t2.micro\"}}),\n\t\t\t[]string{\"t2.micro\"},\n\t\t},\n\t\t{\n\t\t\t\"Three AZs, two instance types, each available in only two of the three offerings\",\n\t\t\t[]string{\"us-east-1a\", \"us-east-1b\", \"us-east-1c\"},\n\t\t\tofferings(map[string][]string{\"us-east-1a\": {\"t2.micro\"}, \"us-east-1b\": {\"t2.micro\", \"t3.micro\"}, \"us-east-1c\": {\"t3.micro\"}}),\n\t\t\t[]string{\"t2.micro\", \"t3.micro\"},\n\t\t},\n\t}\n\n\tfor _, testCase := range testCases {\n\t\t// The following is necessary to make sure testCase's values don't\n\t\t// get updated due to concurrency within the scope of t.Run(..) below\n\t\ttestCase := testCase\n\t\tt.Run(testCase.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\t_, err := pickRecommendedInstanceTypeE(testCase.availabilityZones, testCase.instanceTypeOfferings, testCase.instanceTypeOptions)\n\t\t\tassert.EqualError(t, err, NoInstanceTypeError{Azs: testCase.availabilityZones, InstanceTypeOptions: testCase.instanceTypeOptions}.Error())\n\t\t})\n\t}\n}\n\nfunc offerings(offerings map[string][]string) []types.InstanceTypeOffering {\n\tvar out []types.InstanceTypeOffering\n\n\tfor az, instanceTypes := range offerings {\n\t\tfor _, instanceType := range instanceTypes {\n\t\t\toffering := types.InstanceTypeOffering{\n\t\t\t\tInstanceType: types.InstanceType(instanceType),\n\t\t\t\tLocation:     aws.String(az),\n\t\t\t\tLocationType: types.LocationTypeAvailabilityZone,\n\t\t\t}\n\t\t\tout = append(out, offering)\n\t\t}\n\t}\n\n\treturn out\n}\n"
  },
  {
    "path": "modules/aws/ecr.go",
    "content": "package aws\n\nimport (\n\t\"context\"\n\tgoerrors \"errors\"\n\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\t\"github.com/aws/aws-sdk-go-v2/service/ecr\"\n\t\"github.com/aws/aws-sdk-go-v2/service/ecr/types\"\n\t\"github.com/gruntwork-io/go-commons/errors\"\n\t\"github.com/gruntwork-io/terratest/modules/logger\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// CreateECRRepo creates a new ECR Repository. This will fail the test and stop execution if there is an error.\nfunc CreateECRRepo(t testing.TestingT, region string, name string) *types.Repository {\n\trepo, err := CreateECRRepoE(t, region, name)\n\trequire.NoError(t, err)\n\treturn repo\n}\n\n// CreateECRRepoE creates a new ECR Repository.\nfunc CreateECRRepoE(t testing.TestingT, region string, name string) (*types.Repository, error) {\n\tclient := NewECRClient(t, region)\n\tresp, err := client.CreateRepository(context.Background(), &ecr.CreateRepositoryInput{RepositoryName: aws.String(name)})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn resp.Repository, nil\n}\n\n// GetECRRepo gets an ECR repository by name. This will fail the test and stop execution if there is an error.\n// An error occurs if a repository with the given name does not exist in the given region.\nfunc GetECRRepo(t testing.TestingT, region string, name string) *types.Repository {\n\trepo, err := GetECRRepoE(t, region, name)\n\trequire.NoError(t, err)\n\treturn repo\n}\n\n// GetECRRepoE gets an ECR Repository by name.\n// An error occurs if a repository with the given name does not exist in the given region.\nfunc GetECRRepoE(t testing.TestingT, region string, name string) (*types.Repository, error) {\n\tclient := NewECRClient(t, region)\n\trepositoryNames := []string{name}\n\tresp, err := client.DescribeRepositories(context.Background(), &ecr.DescribeRepositoriesInput{RepositoryNames: repositoryNames})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif len(resp.Repositories) != 1 {\n\t\treturn nil, errors.WithStackTrace(goerrors.New(\"an unexpected condition occurred. Please file an issue at github.com/gruntwork-io/terratest\"))\n\t}\n\treturn &resp.Repositories[0], nil\n}\n\n// DeleteECRRepo will force delete the ECR repo by deleting all images prior to deleting the ECR repository.\n// This will fail the test and stop execution if there is an error.\nfunc DeleteECRRepo(t testing.TestingT, region string, repo *types.Repository) {\n\terr := DeleteECRRepoE(t, region, repo)\n\trequire.NoError(t, err)\n}\n\n// DeleteECRRepoE will force delete the ECR repo by deleting all images prior to deleting the ECR repository.\nfunc DeleteECRRepoE(t testing.TestingT, region string, repo *types.Repository) error {\n\tclient := NewECRClient(t, region)\n\tresp, err := client.ListImages(context.Background(), &ecr.ListImagesInput{RepositoryName: repo.RepositoryName})\n\tif err != nil {\n\t\treturn err\n\t}\n\tif len(resp.ImageIds) > 0 {\n\t\t_, err = client.BatchDeleteImage(context.Background(), &ecr.BatchDeleteImageInput{\n\t\t\tRepositoryName: repo.RepositoryName,\n\t\t\tImageIds:       resp.ImageIds,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\t_, err = client.DeleteRepository(context.Background(), &ecr.DeleteRepositoryInput{RepositoryName: repo.RepositoryName})\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\n// NewECRClient returns a client for the Elastic Container Registry. This will fail the test and\n// stop execution if there is an error.\nfunc NewECRClient(t testing.TestingT, region string) *ecr.Client {\n\tsess, err := NewECRClientE(t, region)\n\trequire.NoError(t, err)\n\treturn sess\n}\n\n// NewECRClientE returns a client for the Elastic Container Registry.\nfunc NewECRClientE(t testing.TestingT, region string) (*ecr.Client, error) {\n\tsess, err := NewAuthenticatedSession(region)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn ecr.NewFromConfig(*sess), nil\n}\n\n// GetECRRepoLifecyclePolicy gets the policies for the given ECR repository.\n// This will fail the test and stop execution if there is an error.\nfunc GetECRRepoLifecyclePolicy(t testing.TestingT, region string, repo *types.Repository) string {\n\tpolicy, err := GetECRRepoLifecyclePolicyE(t, region, repo)\n\trequire.NoError(t, err)\n\treturn policy\n}\n\n// GetECRRepoLifecyclePolicyE gets the policies for the given ECR repository.\nfunc GetECRRepoLifecyclePolicyE(t testing.TestingT, region string, repo *types.Repository) (string, error) {\n\tclient := NewECRClient(t, region)\n\tresp, err := client.GetLifecyclePolicy(context.Background(), &ecr.GetLifecyclePolicyInput{RepositoryName: repo.RepositoryName})\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn *resp.LifecyclePolicyText, nil\n}\n\n// PutECRRepoLifecyclePolicy puts the given policy for the given ECR repository.\n// This will fail the test and stop execution if there is an error.\nfunc PutECRRepoLifecyclePolicy(t testing.TestingT, region string, repo *types.Repository, policy string) {\n\terr := PutECRRepoLifecyclePolicyE(t, region, repo, policy)\n\trequire.NoError(t, err)\n}\n\n// PutECRRepoLifecyclePolicyE puts the given policy for the given ECR repository.\nfunc PutECRRepoLifecyclePolicyE(t testing.TestingT, region string, repo *types.Repository, policy string) error {\n\tlogger.Default.Logf(t, \"Applying policy for repository %s in %s\", *repo.RepositoryName, region)\n\n\tclient, err := NewECRClientE(t, region)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tinput := &ecr.PutLifecyclePolicyInput{\n\t\tRepositoryName:      repo.RepositoryName,\n\t\tLifecyclePolicyText: aws.String(policy),\n\t}\n\n\t_, err = client.PutLifecyclePolicy(context.Background(), input)\n\treturn err\n}\n\n// GetECRRepoPolicy gets the permissions for the given ECR repository.\n// This will fail the test and stop execution if there is an error.\nfunc GetECRRepoPolicy(t testing.TestingT, region string, repo *types.Repository) string {\n\tpolicy, err := GetECRRepoPolicyE(t, region, repo)\n\trequire.NoError(t, err)\n\treturn policy\n}\n\n// GetECRRepoPolicyE gets the policies for the given ECR repository.\nfunc GetECRRepoPolicyE(t testing.TestingT, region string, repo *types.Repository) (string, error) {\n\tclient := NewECRClient(t, region)\n\tresp, err := client.GetRepositoryPolicy(context.Background(), &ecr.GetRepositoryPolicyInput{RepositoryName: repo.RepositoryName})\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn *resp.PolicyText, nil\n}\n\n// PutECRRepoPolicy puts the given policy for the given ECR repository.\n// This will fail the test and stop execution if there is an error.\nfunc PutECRRepoPolicy(t testing.TestingT, region string, repo *types.Repository, policy string) {\n\terr := PutECRRepoPolicyE(t, region, repo, policy)\n\trequire.NoError(t, err)\n}\n\n// PutECRRepoPolicyE puts the given policy for the given ECR repository.\nfunc PutECRRepoPolicyE(t testing.TestingT, region string, repo *types.Repository, policy string) error {\n\tlogger.Default.Logf(t, \"Applying repo policy for repository %s in %s\", *repo.RepositoryName, region)\n\n\tclient, err := NewECRClientE(t, region)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tinput := &ecr.SetRepositoryPolicyInput{\n\t\tPolicyText:     &policy,\n\t\tRepositoryName: repo.RepositoryName,\n\t}\n\n\t_, err = client.SetRepositoryPolicy(context.Background(), input)\n\treturn err\n}\n"
  },
  {
    "path": "modules/aws/ecr_test.go",
    "content": "package aws\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestEcrRepo(t *testing.T) {\n\tt.Parallel()\n\n\tregion := GetRandomStableRegion(t, nil, nil)\n\tecrRepoName := fmt.Sprintf(\"terratest%s\", strings.ToLower(random.UniqueId()))\n\trepo1, err := CreateECRRepoE(t, region, ecrRepoName)\n\tdefer DeleteECRRepo(t, region, repo1)\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, ecrRepoName, aws.ToString(repo1.RepositoryName))\n\n\trepo2, err := GetECRRepoE(t, region, ecrRepoName)\n\trequire.NoError(t, err)\n\tassert.Equal(t, ecrRepoName, aws.ToString(repo2.RepositoryName))\n}\n\nfunc TestGetEcrRepoLifecyclePolicyError(t *testing.T) {\n\tt.Parallel()\n\n\tregion := GetRandomStableRegion(t, nil, nil)\n\tecrRepoName := fmt.Sprintf(\"terratest%s\", strings.ToLower(random.UniqueId()))\n\trepo1, err := CreateECRRepoE(t, region, ecrRepoName)\n\tdefer DeleteECRRepo(t, region, repo1)\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, ecrRepoName, aws.ToString(repo1.RepositoryName))\n\n\t_, err = GetECRRepoLifecyclePolicyE(t, region, repo1)\n\trequire.Error(t, err)\n}\n\nfunc TestCanSetECRRepoLifecyclePolicyWithSingleRule(t *testing.T) {\n\tt.Parallel()\n\n\tregion := GetRandomStableRegion(t, nil, nil)\n\tecrRepoName := fmt.Sprintf(\"terratest%s\", strings.ToLower(random.UniqueId()))\n\trepo1, err := CreateECRRepoE(t, region, ecrRepoName)\n\tdefer DeleteECRRepo(t, region, repo1)\n\trequire.NoError(t, err)\n\n\tlifecyclePolicy := `{\n\t\t\"rules\": [\n\t\t\t{\n\t\t\t\t\"rulePriority\": 1,\n\t\t\t\t\"description\": \"Expire images older than 14 days\",\n\t\t\t\t\"selection\": {\n\t\t\t\t\t\"tagStatus\": \"untagged\",\n\t\t\t\t\t\"countType\": \"sinceImagePushed\",\n\t\t\t\t\t\"countUnit\": \"days\",\n\t\t\t\t\t\"countNumber\": 14\n\t\t\t\t},\n\t\t\t\t\"action\": {\n\t\t\t\t\t\"type\": \"expire\"\n\t\t\t\t}\n\t\t\t}\n\t\t]\n\t}`\n\n\terr = PutECRRepoLifecyclePolicyE(t, region, repo1, lifecyclePolicy)\n\trequire.NoError(t, err)\n\n\tpolicy := GetECRRepoLifecyclePolicy(t, region, repo1)\n\tassert.JSONEq(t, lifecyclePolicy, policy)\n}\n\nfunc TestCanSetRepositoryPolicyWithSimplePolicy(t *testing.T) {\n\tt.Parallel()\n\n\tregion := GetRandomStableRegion(t, nil, nil)\n\tecrRepoName := fmt.Sprintf(\"terratest%s\", strings.ToLower(random.UniqueId()))\n\trepo, err := CreateECRRepoE(t, region, ecrRepoName)\n\tdefer DeleteECRRepo(t, region, repo)\n\trequire.NoError(t, err)\n\n\trepositoryPolicy := `\n\t\t{\n\t\t\"Version\": \"2012-10-17\",\n\t\t\"Statement\": [\n\t\t\t{\n\t\t\t\t\"Sid\": \"AllowPushPull\",\n\t\t\t\t\"Effect\": \"Allow\",\n\t\t\t\t\"Principal\": {\n\t\t\t\t\t\"AWS\": \"*\"\n\t\t\t\t},\n\t\t\t\t\"Action\": \"ecr:*\"\n\t\t\t}\n\t\t]\n\t}`\n\n\terr = PutECRRepoPolicyE(t, region, repo, repositoryPolicy)\n\trequire.NoError(t, err)\n\n\tpolicy := GetECRRepoPolicy(t, region, repo)\n\tassert.JSONEq(t, repositoryPolicy, policy)\n}\n"
  },
  {
    "path": "modules/aws/ecs.go",
    "content": "package aws\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\t\"github.com/aws/aws-sdk-go-v2/service/ecs\"\n\t\"github.com/aws/aws-sdk-go-v2/service/ecs/types\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// GetEcsCluster fetches information about specified ECS cluster.\nfunc GetEcsCluster(t testing.TestingT, region string, name string) *types.Cluster {\n\tcluster, err := GetEcsClusterE(t, region, name)\n\trequire.NoError(t, err)\n\treturn cluster\n}\n\n// GetEcsClusterE fetches information about specified ECS cluster.\nfunc GetEcsClusterE(t testing.TestingT, region string, name string) (*types.Cluster, error) {\n\treturn GetEcsClusterWithIncludeE(t, region, name, []types.ClusterField{})\n}\n\n// GetEcsClusterWithInclude fetches extended information about specified ECS cluster.\n// The `include` parameter specifies a list of `ecs.ClusterField*` constants, such as `ecs.ClusterFieldTags`.\nfunc GetEcsClusterWithInclude(t testing.TestingT, region string, name string, include []types.ClusterField) *types.Cluster {\n\tclusterInfo, err := GetEcsClusterWithIncludeE(t, region, name, include)\n\trequire.NoError(t, err)\n\treturn clusterInfo\n}\n\n// GetEcsClusterWithIncludeE fetches extended information about specified ECS cluster.\n// The `include` parameter specifies a list of `ecs.ClusterField*` constants, such as `ecs.ClusterFieldTags`.\nfunc GetEcsClusterWithIncludeE(t testing.TestingT, region string, name string, include []types.ClusterField) (*types.Cluster, error) {\n\tclient, err := NewEcsClientE(t, region)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tinput := &ecs.DescribeClustersInput{\n\t\tClusters: []string{\n\t\t\tname,\n\t\t},\n\t\tInclude: include,\n\t}\n\toutput, err := client.DescribeClusters(context.Background(), input)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tnumClusters := len(output.Clusters)\n\tif numClusters != 1 {\n\t\treturn nil, fmt.Errorf(\"expected to find 1 ECS cluster named '%s' in region '%v', but found '%d'\",\n\t\t\tname, region, numClusters)\n\t}\n\n\treturn &output.Clusters[0], nil\n}\n\n// GetDefaultEcsClusterE fetches information about default ECS cluster.\nfunc GetDefaultEcsClusterE(t testing.TestingT, region string) (*types.Cluster, error) {\n\treturn GetEcsClusterE(t, region, \"default\")\n}\n\n// GetDefaultEcsCluster fetches information about default ECS cluster.\nfunc GetDefaultEcsCluster(t testing.TestingT, region string) *types.Cluster {\n\treturn GetEcsCluster(t, region, \"default\")\n}\n\n// CreateEcsCluster creates ECS cluster in the given region under the given name.\nfunc CreateEcsCluster(t testing.TestingT, region string, name string) *types.Cluster {\n\tcluster, err := CreateEcsClusterE(t, region, name)\n\trequire.NoError(t, err)\n\treturn cluster\n}\n\n// CreateEcsClusterE creates ECS cluster in the given region under the given name.\nfunc CreateEcsClusterE(t testing.TestingT, region string, name string) (*types.Cluster, error) {\n\tclient := NewEcsClient(t, region)\n\tcluster, err := client.CreateCluster(context.Background(), &ecs.CreateClusterInput{\n\t\tClusterName: aws.String(name),\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn cluster.Cluster, nil\n}\n\nfunc DeleteEcsCluster(t testing.TestingT, region string, cluster *types.Cluster) {\n\terr := DeleteEcsClusterE(t, region, cluster)\n\trequire.NoError(t, err)\n}\n\n// DeleteEcsClusterE deletes existing ECS cluster in the given region.\nfunc DeleteEcsClusterE(t testing.TestingT, region string, cluster *types.Cluster) error {\n\tclient := NewEcsClient(t, region)\n\t_, err := client.DeleteCluster(context.Background(), &ecs.DeleteClusterInput{\n\t\tCluster: aws.String(*cluster.ClusterName),\n\t})\n\treturn err\n}\n\n// GetEcsService fetches information about specified ECS service.\nfunc GetEcsService(t testing.TestingT, region string, clusterName string, serviceName string) *types.Service {\n\tservice, err := GetEcsServiceE(t, region, clusterName, serviceName)\n\trequire.NoError(t, err)\n\treturn service\n}\n\n// GetEcsServiceE fetches information about specified ECS service.\nfunc GetEcsServiceE(t testing.TestingT, region string, clusterName string, serviceName string) (*types.Service, error) {\n\toutput, err := NewEcsClient(t, region).DescribeServices(context.Background(), &ecs.DescribeServicesInput{\n\t\tCluster: aws.String(clusterName),\n\t\tServices: []string{\n\t\t\tserviceName,\n\t\t},\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tnumServices := len(output.Services)\n\tif numServices != 1 {\n\t\treturn nil, fmt.Errorf(\n\t\t\t\"expected to find 1 ECS service named '%s' in cluster '%s' in region '%v', but found '%d'\",\n\t\t\tserviceName, clusterName, region, numServices)\n\t}\n\treturn &output.Services[0], nil\n}\n\n// GetEcsTaskDefinition fetches information about specified ECS task definition.\nfunc GetEcsTaskDefinition(t testing.TestingT, region string, taskDefinition string) *types.TaskDefinition {\n\ttask, err := GetEcsTaskDefinitionE(t, region, taskDefinition)\n\trequire.NoError(t, err)\n\treturn task\n}\n\n// GetEcsTaskDefinitionE fetches information about specified ECS task definition.\nfunc GetEcsTaskDefinitionE(t testing.TestingT, region string, taskDefinition string) (*types.TaskDefinition, error) {\n\toutput, err := NewEcsClient(t, region).DescribeTaskDefinition(context.Background(), &ecs.DescribeTaskDefinitionInput{\n\t\tTaskDefinition: aws.String(taskDefinition),\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn output.TaskDefinition, nil\n}\n\n// NewEcsClient creates en ECS client.\nfunc NewEcsClient(t testing.TestingT, region string) *ecs.Client {\n\tclient, err := NewEcsClientE(t, region)\n\trequire.NoError(t, err)\n\treturn client\n}\n\n// NewEcsClientE creates an ECS client.\nfunc NewEcsClientE(t testing.TestingT, region string) (*ecs.Client, error) {\n\tsess, err := NewAuthenticatedSession(region)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn ecs.NewFromConfig(*sess), nil\n}\n"
  },
  {
    "path": "modules/aws/ecs_test.go",
    "content": "package aws\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\t\"github.com/aws/aws-sdk-go-v2/service/ecs\"\n\t\"github.com/aws/aws-sdk-go-v2/service/ecs/types\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestEcsCluster(t *testing.T) {\n\tt.Parallel()\n\n\tregion := GetRandomStableRegion(t, nil, nil)\n\tc1, err := CreateEcsClusterE(t, region, \"terratest\")\n\tdefer DeleteEcsCluster(t, region, c1)\n\n\tassert.Nil(t, err)\n\tassert.Equal(t, \"terratest\", *c1.ClusterName)\n\n\tc2, err := GetEcsClusterE(t, region, *c1.ClusterName)\n\n\tassert.Nil(t, err)\n\tassert.Equal(t, \"terratest\", *c2.ClusterName)\n}\n\nfunc TestEcsClusterWithInclude(t *testing.T) {\n\tt.Parallel()\n\n\tregion := GetRandomStableRegion(t, nil, nil)\n\tclusterName := \"terratest-\" + random.UniqueId()\n\ttags := []types.Tag{{\n\t\tKey:   aws.String(\"test-tag\"),\n\t\tValue: aws.String(\"hello-world\"),\n\t}}\n\n\tclient := NewEcsClient(t, region)\n\tc1, err := client.CreateCluster(context.Background(), &ecs.CreateClusterInput{\n\t\tClusterName: aws.String(clusterName),\n\t\tTags:        tags,\n\t})\n\tassert.NoError(t, err)\n\n\tdefer DeleteEcsCluster(t, region, c1.Cluster)\n\n\tassert.Equal(t, clusterName, aws.ToString(c1.Cluster.ClusterName))\n\n\tc2, err := GetEcsClusterWithIncludeE(t, region, clusterName, []types.ClusterField{types.ClusterFieldTags})\n\tassert.NoError(t, err)\n\n\tassert.Equal(t, clusterName, aws.ToString(c2.ClusterName))\n\tassert.Equal(t, tags, c2.Tags)\n\tassert.Empty(t, c2.Statistics)\n\n\tc3, err := GetEcsClusterWithIncludeE(t, region, clusterName, []types.ClusterField{types.ClusterFieldStatistics})\n\tassert.NoError(t, err)\n\n\tassert.Equal(t, clusterName, aws.ToString(c3.ClusterName))\n\tassert.NotEmpty(t, c3.Statistics)\n\tassert.Empty(t, c3.Tags)\n}\n"
  },
  {
    "path": "modules/aws/errors.go",
    "content": "package aws\n\nimport (\n\t\"fmt\"\n)\n\n// IpForEc2InstanceNotFound is an error that occurs when the IP for an EC2 instance is not found.\ntype IpForEc2InstanceNotFound struct {\n\tInstanceId string\n\tAwsRegion  string\n\tType       string\n}\n\nfunc (err IpForEc2InstanceNotFound) Error() string {\n\treturn fmt.Sprintf(\"Could not find a %s IP address for EC2 Instance %s in %s\", err.Type, err.InstanceId, err.AwsRegion)\n}\n\n// HostnameForEc2InstanceNotFound is an error that occurs when the IP for an EC2 instance is not found.\ntype HostnameForEc2InstanceNotFound struct {\n\tInstanceId string\n\tAwsRegion  string\n\tType       string\n}\n\nfunc (err HostnameForEc2InstanceNotFound) Error() string {\n\treturn fmt.Sprintf(\"Could not find a %s hostname for EC2 Instance %s in %s\", err.Type, err.InstanceId, err.AwsRegion)\n}\n\n// NotFoundError is returned when an expected object is not found\ntype NotFoundError struct {\n\tobjectType string\n\tobjectID   string\n\tregion     string\n}\n\nfunc (err NotFoundError) Error() string {\n\treturn fmt.Sprintf(\"Object of type %s with id %s not found in region %s\", err.objectType, err.objectID, err.region)\n}\n\nfunc NewNotFoundError(objectType string, objectID string, region string) NotFoundError {\n\treturn NotFoundError{objectType, objectID, region}\n}\n\n// AsgCapacityNotMetError is returned when the ASG capacity is not yet at the desired capacity.\ntype AsgCapacityNotMetError struct {\n\tasgName         string\n\tdesiredCapacity int64\n\tcurrentCapacity int64\n}\n\nfunc (err AsgCapacityNotMetError) Error() string {\n\treturn fmt.Sprintf(\n\t\t\"ASG %s not yet at desired capacity %d (current %d)\",\n\t\terr.asgName,\n\t\terr.desiredCapacity,\n\t\terr.currentCapacity,\n\t)\n}\n\nfunc NewAsgCapacityNotMetError(asgName string, desiredCapacity int64, currentCapacity int64) AsgCapacityNotMetError {\n\treturn AsgCapacityNotMetError{asgName, desiredCapacity, currentCapacity}\n}\n\n// BucketVersioningNotEnabledError is returned when an S3 bucket that should have versioning does not have it applied\ntype BucketVersioningNotEnabledError struct {\n\ts3BucketName     string\n\tawsRegion        string\n\tversioningStatus string\n}\n\nfunc (err BucketVersioningNotEnabledError) Error() string {\n\treturn fmt.Sprintf(\n\t\t\"Versioning status for bucket %s in the %s region is %s\",\n\t\terr.s3BucketName,\n\t\terr.awsRegion,\n\t\terr.versioningStatus,\n\t)\n}\n\nfunc NewBucketVersioningNotEnabledError(s3BucketName string, awsRegion string, versioningStatus string) BucketVersioningNotEnabledError {\n\treturn BucketVersioningNotEnabledError{s3BucketName: s3BucketName, awsRegion: awsRegion, versioningStatus: versioningStatus}\n}\n\n// NoBucketPolicyError is returned when an S3 bucket that should have a policy applied does not\ntype NoBucketPolicyError struct {\n\ts3BucketName string\n\tawsRegion    string\n\tbucketPolicy string\n}\n\nfunc (err NoBucketPolicyError) Error() string {\n\treturn fmt.Sprintf(\n\t\t\"The policy for bucket %s in the %s region does not have a policy attached.\",\n\t\terr.s3BucketName,\n\t\terr.awsRegion,\n\t)\n}\n\nfunc NewNoBucketPolicyError(s3BucketName string, awsRegion string, bucketPolicy string) NoBucketPolicyError {\n\treturn NoBucketPolicyError{s3BucketName: s3BucketName, awsRegion: awsRegion, bucketPolicy: bucketPolicy}\n}\n\n// NoInstanceTypeError is returned when none of the given instance type options are available in all AZs in a region\ntype NoInstanceTypeError struct {\n\tInstanceTypeOptions []string\n\tAzs                 []string\n}\n\nfunc (err NoInstanceTypeError) Error() string {\n\treturn fmt.Sprintf(\n\t\t\"None of the given instance types (%v) is available in all the AZs in this region (%v).\",\n\t\terr.InstanceTypeOptions,\n\t\terr.Azs,\n\t)\n}\n\n// NoRdsInstanceTypeError is returned when none of the given instance types are avaiable for the region, database engine, and database engine combination given\ntype NoRdsInstanceTypeError struct {\n\tInstanceTypeOptions   []string\n\tDatabaseEngine        string\n\tDatabaseEngineVersion string\n}\n\nfunc (err NoRdsInstanceTypeError) Error() string {\n\treturn fmt.Sprintf(\n\t\t\"None of the given RDS instance types (%v) is available in this region for database engine (%v) of version (%v).\",\n\t\terr.InstanceTypeOptions,\n\t\terr.DatabaseEngine,\n\t\terr.DatabaseEngineVersion,\n\t)\n}\n"
  },
  {
    "path": "modules/aws/iam.go",
    "content": "package aws\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\t\"github.com/aws/aws-sdk-go-v2/service/iam\"\n\t\"github.com/aws/aws-sdk-go-v2/service/iam/types\"\n\t\"github.com/gruntwork-io/terratest/modules/logger\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n)\n\n// GetIamCurrentUserName gets the username for the current IAM user.\nfunc GetIamCurrentUserName(t testing.TestingT) string {\n\tout, err := GetIamCurrentUserNameE(t)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn out\n}\n\n// GetIamCurrentUserNameE gets the username for the current IAM user.\nfunc GetIamCurrentUserNameE(t testing.TestingT) (string, error) {\n\tiamClient, err := NewIamClientE(t, defaultRegion)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tresp, err := iamClient.GetUser(context.Background(), &iam.GetUserInput{})\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn *resp.User.UserName, nil\n}\n\n// GetIamCurrentUserArn gets the ARN for the current IAM user.\nfunc GetIamCurrentUserArn(t testing.TestingT) string {\n\tout, err := GetIamCurrentUserArnE(t)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn out\n}\n\n// GetIamCurrentUserArnE gets the ARN for the current IAM user.\nfunc GetIamCurrentUserArnE(t testing.TestingT) (string, error) {\n\tiamClient, err := NewIamClientE(t, defaultRegion)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tresp, err := iamClient.GetUser(context.Background(), &iam.GetUserInput{})\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn *resp.User.Arn, nil\n}\n\n// GetIamPolicyDocument gets the most recent policy (JSON) document for an IAM policy.\nfunc GetIamPolicyDocument(t testing.TestingT, region string, policyARN string) string {\n\tout, err := GetIamPolicyDocumentE(t, region, policyARN)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn out\n}\n\n// GetIamPolicyDocumentE gets the most recent policy (JSON) document for an IAM policy.\nfunc GetIamPolicyDocumentE(t testing.TestingT, region string, policyARN string) (string, error) {\n\tiamClient, err := NewIamClientE(t, region)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tversions, err := iamClient.ListPolicyVersions(context.Background(), &iam.ListPolicyVersionsInput{\n\t\tPolicyArn: &policyARN,\n\t})\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tvar defaultVersion string\n\tfor _, version := range versions.Versions {\n\t\tif version.IsDefaultVersion == true {\n\t\t\tdefaultVersion = *version.VersionId\n\t\t}\n\t}\n\n\tdocument, err := iamClient.GetPolicyVersion(context.Background(), &iam.GetPolicyVersionInput{\n\t\tPolicyArn: aws.String(policyARN),\n\t\tVersionId: aws.String(defaultVersion),\n\t})\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tunescapedDocument := document.PolicyVersion.Document\n\tif unescapedDocument == nil {\n\t\treturn \"\", fmt.Errorf(\"no policy document found for policy %s\", policyARN)\n\t}\n\n\tescapedDocument, err := url.QueryUnescape(*unescapedDocument)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn escapedDocument, nil\n}\n\n// CreateMfaDevice creates an MFA device using the given IAM client.\nfunc CreateMfaDevice(t testing.TestingT, iamClient *iam.Client, deviceName string) *types.VirtualMFADevice {\n\tmfaDevice, err := CreateMfaDeviceE(t, iamClient, deviceName)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn mfaDevice\n}\n\n// CreateMfaDeviceE creates an MFA device using the given IAM client.\nfunc CreateMfaDeviceE(t testing.TestingT, iamClient *iam.Client, deviceName string) (*types.VirtualMFADevice, error) {\n\tlogger.Default.Logf(t, \"Creating an MFA device called %s\", deviceName)\n\n\toutput, err := iamClient.CreateVirtualMFADevice(context.Background(), &iam.CreateVirtualMFADeviceInput{\n\t\tVirtualMFADeviceName: aws.String(deviceName),\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err := EnableMfaDeviceE(t, iamClient, output.VirtualMFADevice); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn output.VirtualMFADevice, nil\n}\n\n// EnableMfaDevice enables a newly created MFA Device by supplying the first two one-time passwords, so that it can be used for future\n// logins by the given IAM User.\nfunc EnableMfaDevice(t testing.TestingT, iamClient *iam.Client, mfaDevice *types.VirtualMFADevice) {\n\terr := EnableMfaDeviceE(t, iamClient, mfaDevice)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n\n// EnableMfaDeviceE enables a newly created MFA Device by supplying the first two one-time passwords, so that it can be used for future\n// logins by the given IAM User.\nfunc EnableMfaDeviceE(t testing.TestingT, iamClient *iam.Client, mfaDevice *types.VirtualMFADevice) error {\n\tlogger.Default.Logf(t, \"Enabling MFA device %s\", aws.ToString(mfaDevice.SerialNumber))\n\n\tiamUserName, err := GetIamCurrentUserArnE(t)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tauthCode1, err := GetTimeBasedOneTimePassword(mfaDevice)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tlogger.Default.Logf(t, \"Waiting 30 seconds for a new MFA Token to be generated...\")\n\ttime.Sleep(30 * time.Second)\n\n\tauthCode2, err := GetTimeBasedOneTimePassword(mfaDevice)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t_, err = iamClient.EnableMFADevice(context.Background(), &iam.EnableMFADeviceInput{\n\t\tAuthenticationCode1: aws.String(authCode1),\n\t\tAuthenticationCode2: aws.String(authCode2),\n\t\tSerialNumber:        mfaDevice.SerialNumber,\n\t\tUserName:            aws.String(iamUserName),\n\t})\n\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tlogger.Log(t, \"Waiting for MFA Device enablement to propagate.\")\n\ttime.Sleep(10 * time.Second)\n\n\treturn nil\n}\n\n// NewIamClient creates a new IAM client.\nfunc NewIamClient(t testing.TestingT, region string) *iam.Client {\n\tclient, err := NewIamClientE(t, region)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn client\n}\n\n// NewIamClientE creates a new IAM client.\nfunc NewIamClientE(t testing.TestingT, region string) (*iam.Client, error) {\n\tsess, err := NewAuthenticatedSession(region)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn iam.NewFromConfig(*sess), nil\n}\n"
  },
  {
    "path": "modules/aws/iam_test.go",
    "content": "package aws\n\nimport (\n\t\"context\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\t\"github.com/aws/aws-sdk-go-v2/service/iam\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestGetIamCurrentUserName(t *testing.T) {\n\tt.Parallel()\n\n\tusername := GetIamCurrentUserName(t)\n\tassert.NotEmpty(t, username)\n}\n\nfunc TestGetIamCurrentUserArn(t *testing.T) {\n\tt.Parallel()\n\n\tusername := GetIamCurrentUserArn(t)\n\tassert.Regexp(t, \"^arn:aws:iam::[0-9]{12}:user/.+$\", username)\n}\n\nfunc TestGetIAMPolicyDocument(t *testing.T) {\n\tt.Parallel()\n\n\tregion := GetRandomRegion(t, nil, nil)\n\n\tt.Run(\"Exists\", func(t *testing.T) {\n\t\tiamClient, err := NewIamClientE(t, region)\n\t\trequire.NoError(t, err)\n\n\t\tpolicyDocument := `{\n\t\t\t\"Version\": \"2012-10-17\",\n\t\t\t\"Statement\": [\n\t\t\t\t{\n\t\t\t\t\t\"Sid\": \"Stmt1530709892083\",\n\t\t\t\t\t\"Action\": \"*\",\n\t\t\t\t\t\"Effect\": \"Allow\",\n\t\t\t\t\t\"Resource\": \"*\"\n\t\t\t\t}\n\t\t\t]\n\t\t}`\n\t\tinput := &iam.CreatePolicyInput{\n\t\t\tPolicyName:     aws.String(strings.ToLower(random.UniqueId())),\n\t\t\tPolicyDocument: aws.String(policyDocument),\n\t\t}\n\t\tpolicy, err := iamClient.CreatePolicy(context.Background(), input)\n\t\trequire.NoError(t, err)\n\n\t\tt.Cleanup(func() {\n\t\t\tt.Log(\"Deleting IAM Policy Document\")\n\t\t\t_, err := iamClient.DeletePolicy(context.Background(), &iam.DeletePolicyInput{\n\t\t\t\tPolicyArn: policy.Policy.Arn,\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\t\t})\n\n\t\tp := GetIamPolicyDocument(t, region, *policy.Policy.Arn)\n\t\tt.Log(\"Retrieved Policy Document:\", p)\n\t\tassert.JSONEq(t, policyDocument, p)\n\t})\n\n\tt.Run(\"DoesNotExist\", func(t *testing.T) {\n\t\t_, err := GetIamPolicyDocumentE(t, region, \"arn:aws:iam::1234567890:policy/does-not-exist\")\n\t\trequire.Error(t, err)\n\t})\n}\n"
  },
  {
    "path": "modules/aws/keypair.go",
    "content": "package aws\n\nimport (\n\t\"context\"\n\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\t\"github.com/aws/aws-sdk-go-v2/service/ec2\"\n\t\"github.com/gruntwork-io/terratest/modules/logger\"\n\t\"github.com/gruntwork-io/terratest/modules/ssh\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n)\n\n// Ec2Keypair is an EC2 key pair.\ntype Ec2Keypair struct {\n\t*ssh.KeyPair\n\tName   string // The name assigned in AWS to the EC2 Key Pair\n\tRegion string // The AWS region where the EC2 Key Pair lives\n}\n\n// CreateAndImportEC2KeyPair generates a public/private KeyPair and import it into EC2 in the given region under the given name.\nfunc CreateAndImportEC2KeyPair(t testing.TestingT, region string, name string) *Ec2Keypair {\n\tkeyPair, err := CreateAndImportEC2KeyPairE(t, region, name)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn keyPair\n}\n\n// CreateAndImportEC2KeyPairE generates a public/private KeyPair and import it into EC2 in the given region under the given name.\nfunc CreateAndImportEC2KeyPairE(t testing.TestingT, region string, name string) (*Ec2Keypair, error) {\n\tkeyPair, err := ssh.GenerateRSAKeyPairE(t, 2048)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn ImportEC2KeyPairE(t, region, name, keyPair)\n}\n\n// ImportEC2KeyPair creates a Key Pair in EC2 by importing an existing public key.\nfunc ImportEC2KeyPair(t testing.TestingT, region string, name string, keyPair *ssh.KeyPair) *Ec2Keypair {\n\tec2KeyPair, err := ImportEC2KeyPairE(t, region, name, keyPair)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn ec2KeyPair\n}\n\n// ImportEC2KeyPairE creates a Key Pair in EC2 by importing an existing public key.\nfunc ImportEC2KeyPairE(t testing.TestingT, region string, name string, keyPair *ssh.KeyPair) (*Ec2Keypair, error) {\n\tlogger.Default.Logf(t, \"Creating new Key Pair in EC2 region %s named %s\", region, name)\n\n\tclient, err := NewEc2ClientE(t, region)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tparams := &ec2.ImportKeyPairInput{\n\t\tKeyName:           aws.String(name),\n\t\tPublicKeyMaterial: []byte(keyPair.PublicKey),\n\t}\n\n\t_, err = client.ImportKeyPair(context.Background(), params)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &Ec2Keypair{Name: name, Region: region, KeyPair: keyPair}, nil\n}\n\n// DeleteEC2KeyPair deletes an EC2 key pair.\nfunc DeleteEC2KeyPair(t testing.TestingT, keyPair *Ec2Keypair) {\n\terr := DeleteEC2KeyPairE(t, keyPair)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n\n// DeleteEC2KeyPairE deletes an EC2 key pair.\nfunc DeleteEC2KeyPairE(t testing.TestingT, keyPair *Ec2Keypair) error {\n\tlogger.Default.Logf(t, \"Deleting Key Pair in EC2 region %s named %s\", keyPair.Region, keyPair.Name)\n\n\tclient, err := NewEc2ClientE(t, keyPair.Region)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tparams := &ec2.DeleteKeyPairInput{\n\t\tKeyName: aws.String(keyPair.Name),\n\t}\n\n\t_, err = client.DeleteKeyPair(context.Background(), params)\n\treturn err\n}\n"
  },
  {
    "path": "modules/aws/keypair_test.go",
    "content": "package aws\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/aws/aws-sdk-go-v2/service/ec2\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestCreateImportAndDeleteEC2KeyPair(t *testing.T) {\n\tt.Parallel()\n\n\tregion := GetRandomStableRegion(t, nil, nil)\n\tuniqueID := random.UniqueId()\n\tname := fmt.Sprintf(\"test-key-pair-%s\", uniqueID)\n\n\tkeyPair := CreateAndImportEC2KeyPair(t, region, name)\n\tdefer deleteKeyPair(t, keyPair)\n\n\tassert.True(t, keyPairExists(t, keyPair))\n\tassert.Equal(t, name, keyPair.Name)\n\tassert.Equal(t, region, keyPair.Region)\n\tassert.Contains(t, keyPair.PublicKey, \"ssh-rsa\")\n\tassert.Contains(t, keyPair.PrivateKey, \"-----BEGIN RSA PRIVATE KEY-----\")\n}\n\nfunc keyPairExists(t *testing.T, keyPair *Ec2Keypair) bool {\n\tclient := NewEc2Client(t, keyPair.Region)\n\n\tinput := ec2.DescribeKeyPairsInput{\n\t\tKeyNames: []string{keyPair.Name},\n\t}\n\n\tout, err := client.DescribeKeyPairs(context.Background(), &input)\n\tif err != nil {\n\t\tif strings.Contains(err.Error(), \"InvalidKeyPair.NotFound\") {\n\t\t\treturn false\n\t\t}\n\t\tt.Fatal(err)\n\t}\n\n\treturn len(out.KeyPairs) == 1\n}\n\nfunc deleteKeyPair(t *testing.T, keyPair *Ec2Keypair) {\n\tDeleteEC2KeyPair(t, keyPair)\n\tassert.False(t, keyPairExists(t, keyPair))\n}\n"
  },
  {
    "path": "modules/aws/kms.go",
    "content": "package aws\n\nimport (\n\t\"context\"\n\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\t\"github.com/aws/aws-sdk-go-v2/service/kms\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n)\n\n// GetCmkArn gets the ARN of a KMS Customer Master Key (CMK) in the given region with the given ID. The ID can be an alias, such\n// as \"alias/my-cmk\".\nfunc GetCmkArn(t testing.TestingT, region string, cmkID string) string {\n\tout, err := GetCmkArnE(t, region, cmkID)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn out\n}\n\n// GetCmkArnE gets the ARN of a KMS Customer Master Key (CMK) in the given region with the given ID. The ID can be an alias, such\n// as \"alias/my-cmk\".\nfunc GetCmkArnE(t testing.TestingT, region string, cmkID string) (string, error) {\n\tkmsClient, err := NewKmsClientE(t, region)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tresult, err := kmsClient.DescribeKey(context.Background(), &kms.DescribeKeyInput{\n\t\tKeyId: aws.String(cmkID),\n\t})\n\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn *result.KeyMetadata.Arn, nil\n}\n\n// NewKmsClient creates a KMS client.\nfunc NewKmsClient(t testing.TestingT, region string) *kms.Client {\n\tclient, err := NewKmsClientE(t, region)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn client\n}\n\n// NewKmsClientE creates a KMS client.\nfunc NewKmsClientE(t testing.TestingT, region string) (*kms.Client, error) {\n\tsess, err := NewAuthenticatedSession(region)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn kms.NewFromConfig(*sess), nil\n}\n"
  },
  {
    "path": "modules/aws/lambda.go",
    "content": "package aws\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\n\t\"github.com/aws/aws-sdk-go-v2/service/lambda\"\n\t\"github.com/aws/aws-sdk-go-v2/service/lambda/types\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/stretchr/testify/require\"\n)\n\ntype InvocationTypeOption string\n\nconst (\n\tInvocationTypeRequestResponse InvocationTypeOption = \"RequestResponse\"\n\tInvocationTypeDryRun                               = \"DryRun\"\n)\n\nfunc (itype *InvocationTypeOption) Value() (string, error) {\n\tif itype != nil {\n\t\tswitch *itype {\n\t\tcase\n\t\t\tInvocationTypeRequestResponse,\n\t\t\tInvocationTypeDryRun:\n\t\t\treturn string(*itype), nil\n\t\tdefault:\n\t\t\tmsg := fmt.Sprintf(\"LambdaOptions.InvocationType, if specified, must either be \\\"%s\\\" or \\\"%s\\\"\",\n\t\t\t\tInvocationTypeRequestResponse,\n\t\t\t\tInvocationTypeDryRun)\n\t\t\treturn \"\", errors.New(msg)\n\t\t}\n\t}\n\treturn string(InvocationTypeRequestResponse), nil\n}\n\n// LambdaOptions contains additional parameters for InvokeFunctionWithParams().\n// It contains a subset of the fields found in the lambda.InvokeInput struct.\ntype LambdaOptions struct {\n\t// InvocationType can be one of InvocationTypeOption values:\n\t//    * InvocationTypeRequestResponse (default) - Invoke the function\n\t//      synchronously.  Keep the connection open until the function\n\t//      returns a response or times out.\n\t//    * InvocationTypeDryRun - Validate parameter values and verify\n\t//      that the user or role has permission to invoke the function.\n\tInvocationType *InvocationTypeOption\n\n\t// Lambda function input; will be converted to JSON.\n\tPayload interface{}\n}\n\n// LambdaOutput contains the output from InvokeFunctionWithParams().  The\n// fields may or may not have a value depending on the invocation type and\n// whether an error occurred or not.\ntype LambdaOutput struct {\n\t// The response from the function, or an error object.\n\tPayload []byte\n\n\t// The HTTP status code for a successful request is in the 200 range.\n\t// For RequestResponse invocation type, the status code is 200.\n\t// For the DryRun invocation type, the status code is 204.\n\tStatusCode int32\n}\n\n// InvokeFunction invokes a lambda function.\nfunc InvokeFunction(t testing.TestingT, region, functionName string, payload interface{}) []byte {\n\tout, err := InvokeFunctionE(t, region, functionName, payload)\n\trequire.NoError(t, err)\n\treturn out\n}\n\n// InvokeFunctionE invokes a lambda function.\nfunc InvokeFunctionE(t testing.TestingT, region, functionName string, payload interface{}) ([]byte, error) {\n\tlambdaClient, err := NewLambdaClientE(t, region)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tinvokeInput := &lambda.InvokeInput{\n\t\tFunctionName: &functionName,\n\t}\n\n\tif payload != nil {\n\t\tpayloadJson, err := json.Marshal(payload)\n\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tinvokeInput.Payload = payloadJson\n\t}\n\n\tout, err := lambdaClient.Invoke(context.Background(), invokeInput)\n\trequire.NoError(t, err)\n\n\tif out.FunctionError != nil {\n\t\treturn out.Payload, &FunctionError{Message: *out.FunctionError, StatusCode: out.StatusCode, Payload: out.Payload}\n\t}\n\n\treturn out.Payload, nil\n}\n\n// InvokeFunctionWithParams invokes a lambda function using parameters\n// supplied in the LambdaOptions struct and returns values in a LambdaOutput\n// struct.  Checks for failure using \"require\".\nfunc InvokeFunctionWithParams(t testing.TestingT, region, functionName string, input *LambdaOptions) *LambdaOutput {\n\tout, err := InvokeFunctionWithParamsE(t, region, functionName, input)\n\trequire.NoError(t, err)\n\treturn out\n}\n\n// InvokeFunctionWithParamsE invokes a lambda function using parameters\n// supplied in the LambdaOptions struct.  Returns the status code and payload\n// in a LambdaOutput struct and the error.  A non-nil error will either reflect\n// a problem with the parameters supplied to this function or an error returned\n// by the Lambda.\nfunc InvokeFunctionWithParamsE(t testing.TestingT, region, functionName string, input *LambdaOptions) (*LambdaOutput, error) {\n\tlambdaClient, err := NewLambdaClientE(t, region)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Verify the InvocationType is one of the allowed values and report\n\t// an error if it's not.  By default, the InvocationType will be\n\t// \"RequestResponse\".\n\tinvocationType, err := input.InvocationType.Value()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tinvokeInput := &lambda.InvokeInput{\n\t\tFunctionName:   &functionName,\n\t\tInvocationType: types.InvocationType(invocationType),\n\t}\n\n\tif input.Payload != nil {\n\t\tpayloadJson, err := json.Marshal(input.Payload)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tinvokeInput.Payload = payloadJson\n\t}\n\n\tout, err := lambdaClient.Invoke(context.Background(), invokeInput)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// As this function supports different invocation types, it must\n\t// then support different combinations of output other than just\n\t// payload.\n\tlambdaOutput := LambdaOutput{\n\t\tPayload:    out.Payload,\n\t\tStatusCode: out.StatusCode,\n\t}\n\n\tif out.FunctionError != nil {\n\t\treturn &lambdaOutput, errors.New(*out.FunctionError)\n\t}\n\n\treturn &lambdaOutput, nil\n}\n\ntype FunctionError struct {\n\tMessage    string\n\tStatusCode int32\n\tPayload    []byte\n}\n\nfunc (err *FunctionError) Error() string {\n\treturn fmt.Sprintf(\"%q error with status code %d invoking lambda function: %q\", err.Message, err.StatusCode, err.Payload)\n}\n\n// NewLambdaClient creates a new Lambda client.\nfunc NewLambdaClient(t testing.TestingT, region string) *lambda.Client {\n\tclient, err := NewLambdaClientE(t, region)\n\trequire.NoError(t, err)\n\treturn client\n}\n\n// NewLambdaClientE creates a new Lambda client.\nfunc NewLambdaClientE(t testing.TestingT, region string) (*lambda.Client, error) {\n\tsess, err := NewAuthenticatedSession(region)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn lambda.NewFromConfig(*sess), nil\n}\n"
  },
  {
    "path": "modules/aws/lambda_test.go",
    "content": "package aws\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestFunctionError(t *testing.T) {\n\tt.Parallel()\n\n\t// assert that the error message contains all the components of the error, in a readable form\n\terr := &FunctionError{Message: \"message\", StatusCode: 123, Payload: []byte(\"payload\")}\n\trequire.Contains(t, err.Error(), \"message\")\n\trequire.Contains(t, err.Error(), \"123\")\n\trequire.Contains(t, err.Error(), \"payload\")\n}\n"
  },
  {
    "path": "modules/aws/rds.go",
    "content": "package aws\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"fmt\"\n\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\t\"github.com/aws/aws-sdk-go-v2/service/rds\"\n\t\"github.com/aws/aws-sdk-go-v2/service/rds/types\"\n\t_ \"github.com/go-sql-driver/mysql\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t_ \"github.com/jackc/pgx/v5/stdlib\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// GetAddressOfRdsInstance gets the address of the given RDS Instance in the given region.\nfunc GetAddressOfRdsInstance(t testing.TestingT, dbInstanceID string, awsRegion string) string {\n\taddress, err := GetAddressOfRdsInstanceE(t, dbInstanceID, awsRegion)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn address\n}\n\n// GetAddressOfRdsInstanceE gets the address of the given RDS Instance in the given region.\nfunc GetAddressOfRdsInstanceE(t testing.TestingT, dbInstanceID string, awsRegion string) (string, error) {\n\tdbInstance, err := GetRdsInstanceDetailsE(t, dbInstanceID, awsRegion)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn aws.ToString(dbInstance.Endpoint.Address), nil\n}\n\n// GetPortOfRdsInstance gets the port of the given RDS Instance in the given region.\nfunc GetPortOfRdsInstance(t testing.TestingT, dbInstanceID string, awsRegion string) int32 {\n\tport, err := GetPortOfRdsInstanceE(t, dbInstanceID, awsRegion)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn port\n}\n\n// GetPortOfRdsInstanceE gets the port of the given RDS Instance in the given region.\nfunc GetPortOfRdsInstanceE(t testing.TestingT, dbInstanceID string, awsRegion string) (int32, error) {\n\tdbInstance, err := GetRdsInstanceDetailsE(t, dbInstanceID, awsRegion)\n\tif err != nil {\n\t\treturn -1, err\n\t}\n\n\treturn *dbInstance.Endpoint.Port, nil\n}\n\n// GetWhetherSchemaExistsInRdsMySqlInstance checks whether the specified schema/table name exists in the RDS instance\nfunc GetWhetherSchemaExistsInRdsMySqlInstance(t testing.TestingT, dbUrl string, dbPort int32, dbUsername string, dbPassword string, expectedSchemaName string) bool {\n\toutput, err := GetWhetherSchemaExistsInRdsMySqlInstanceE(t, dbUrl, dbPort, dbUsername, dbPassword, expectedSchemaName)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn output\n}\n\n// GetWhetherSchemaExistsInRdsMySqlInstanceE checks whether the specified schema/table name exists in the RDS instance\nfunc GetWhetherSchemaExistsInRdsMySqlInstanceE(t testing.TestingT, dbUrl string, dbPort int32, dbUsername string, dbPassword string, expectedSchemaName string) (bool, error) {\n\tconnectionString := fmt.Sprintf(\"%s:%s@tcp(%s:%d)/\", dbUsername, dbPassword, dbUrl, dbPort)\n\tdb, connErr := sql.Open(\"mysql\", connectionString)\n\tif connErr != nil {\n\t\treturn false, connErr\n\t}\n\tdefer db.Close()\n\tvar (\n\t\tschemaName string\n\t)\n\tsqlStatement := \"SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME=?;\"\n\trow := db.QueryRow(sqlStatement, expectedSchemaName)\n\tscanErr := row.Scan(&schemaName)\n\tif scanErr != nil {\n\t\treturn false, scanErr\n\t}\n\treturn true, nil\n}\n\n// GetWhetherSchemaExistsInRdsPostgresInstance checks whether the specified schema/table name exists in the RDS instance\nfunc GetWhetherSchemaExistsInRdsPostgresInstance(t testing.TestingT, dbUrl string, dbPort int32, dbUsername string, dbPassword string, expectedSchemaName string) bool {\n\toutput, err := GetWhetherSchemaExistsInRdsPostgresInstanceE(t, dbUrl, dbPort, dbUsername, dbPassword, expectedSchemaName)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn output\n}\n\n// GetWhetherSchemaExistsInRdsPostgresInstanceE checks whether the specified schema/table name exists in the RDS instance\nfunc GetWhetherSchemaExistsInRdsPostgresInstanceE(t testing.TestingT, dbUrl string, dbPort int32, dbUsername string, dbPassword string, expectedSchemaName string) (bool, error) {\n\tconnectionString := fmt.Sprintf(\"host=%s port=%d user=%s password=%s dbname=%s\", dbUrl, dbPort, dbUsername, dbPassword, expectedSchemaName)\n\n\tdb, connErr := sql.Open(\"pgx\", connectionString)\n\tif connErr != nil {\n\t\treturn false, connErr\n\t}\n\tdefer db.Close()\n\tvar (\n\t\tschemaName string\n\t)\n\tsqlStatement := `SELECT \"catalog_name\" FROM \"information_schema\".\"schemata\" where catalog_name=$1`\n\trow := db.QueryRow(sqlStatement, expectedSchemaName)\n\tscanErr := row.Scan(&schemaName)\n\tif scanErr != nil {\n\t\treturn false, scanErr\n\t}\n\treturn true, nil\n}\n\n// GetParameterValueForParameterOfRdsInstance gets the value of the parameter name specified for the RDS instance in the given region.\nfunc GetParameterValueForParameterOfRdsInstance(t testing.TestingT, parameterName string, dbInstanceID string, awsRegion string) string {\n\tparameterValue, err := GetParameterValueForParameterOfRdsInstanceE(t, parameterName, dbInstanceID, awsRegion)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn parameterValue\n}\n\n// GetParameterValueForParameterOfRdsInstanceE gets the value of the parameter name specified for the RDS instance in the given region.\nfunc GetParameterValueForParameterOfRdsInstanceE(t testing.TestingT, parameterName string, dbInstanceID string, awsRegion string) (string, error) {\n\toutput := GetAllParametersOfRdsInstance(t, dbInstanceID, awsRegion)\n\tfor _, parameter := range output {\n\t\tif aws.ToString(parameter.ParameterName) == parameterName {\n\t\t\treturn aws.ToString(parameter.ParameterValue), nil\n\t\t}\n\t}\n\treturn \"\", ParameterForDbInstanceNotFound{ParameterName: parameterName, DbInstanceID: dbInstanceID, AwsRegion: awsRegion}\n}\n\n// GetOptionSettingForOfRdsInstance gets the value of the option name in the option group specified for the RDS instance in the given region.\nfunc GetOptionSettingForOfRdsInstance(t testing.TestingT, optionName string, optionSettingName string, dbInstanceID, awsRegion string) string {\n\toptionValue, err := GetOptionSettingForOfRdsInstanceE(t, optionName, optionSettingName, dbInstanceID, awsRegion)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn optionValue\n}\n\n// GetOptionSettingForOfRdsInstanceE gets the value of the option name in the option group specified for the RDS instance in the given region.\nfunc GetOptionSettingForOfRdsInstanceE(t testing.TestingT, optionName string, optionSettingName string, dbInstanceID, awsRegion string) (string, error) {\n\toptionGroupName := GetOptionGroupNameOfRdsInstance(t, dbInstanceID, awsRegion)\n\toptions := GetOptionsOfOptionGroup(t, optionGroupName, awsRegion)\n\tfor _, option := range options {\n\t\tif aws.ToString(option.OptionName) == optionName {\n\t\t\tfor _, optionSetting := range option.OptionSettings {\n\t\t\t\tif aws.ToString(optionSetting.Name) == optionSettingName {\n\t\t\t\t\treturn aws.ToString(optionSetting.Value), nil\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\", OptionGroupOptionSettingForDbInstanceNotFound{OptionName: optionName, OptionSettingName: optionSettingName, DbInstanceID: dbInstanceID, AwsRegion: awsRegion}\n}\n\n// GetOptionGroupNameOfRdsInstance gets the name of the option group associated with the RDS instance\nfunc GetOptionGroupNameOfRdsInstance(t testing.TestingT, dbInstanceID string, awsRegion string) string {\n\tdbInstance, err := GetOptionGroupNameOfRdsInstanceE(t, dbInstanceID, awsRegion)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn dbInstance\n}\n\n// GetOptionGroupNameOfRdsInstanceE gets the name of the option group associated with the RDS instance\nfunc GetOptionGroupNameOfRdsInstanceE(t testing.TestingT, dbInstanceID string, awsRegion string) (string, error) {\n\tdbInstance, err := GetRdsInstanceDetailsE(t, dbInstanceID, awsRegion)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn aws.ToString(dbInstance.OptionGroupMemberships[0].OptionGroupName), nil\n}\n\n// GetOptionsOfOptionGroup gets the options of the option group specified\nfunc GetOptionsOfOptionGroup(t testing.TestingT, optionGroupName string, awsRegion string) []types.Option {\n\toutput, err := GetOptionsOfOptionGroupE(t, optionGroupName, awsRegion)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn output\n}\n\n// GetOptionsOfOptionGroupE gets the options of the option group specified\nfunc GetOptionsOfOptionGroupE(t testing.TestingT, optionGroupName string, awsRegion string) ([]types.Option, error) {\n\trdsClient := NewRdsClient(t, awsRegion)\n\tinput := rds.DescribeOptionGroupsInput{OptionGroupName: aws.String(optionGroupName)}\n\toutput, err := rdsClient.DescribeOptionGroups(context.Background(), &input)\n\tif err != nil {\n\t\treturn []types.Option{}, err\n\t}\n\treturn output.OptionGroupsList[0].Options, nil\n}\n\n// GetAllParametersOfRdsInstance gets all the parameters defined in the parameter group for the RDS instance in the given region.\nfunc GetAllParametersOfRdsInstance(t testing.TestingT, dbInstanceID string, awsRegion string) []types.Parameter {\n\tparameters, err := GetAllParametersOfRdsInstanceE(t, dbInstanceID, awsRegion)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn parameters\n}\n\n// GetAllParametersOfRdsInstanceE gets all the parameters defined in the parameter group for the RDS instance in the given region.\nfunc GetAllParametersOfRdsInstanceE(t testing.TestingT, dbInstanceID string, awsRegion string) ([]types.Parameter, error) {\n\tdbInstance, dbInstanceErr := GetRdsInstanceDetailsE(t, dbInstanceID, awsRegion)\n\tif dbInstanceErr != nil {\n\t\treturn []types.Parameter{}, dbInstanceErr\n\t}\n\tparameterGroupName := aws.ToString(dbInstance.DBParameterGroups[0].DBParameterGroupName)\n\n\trdsClient := NewRdsClient(t, awsRegion)\n\tinput := rds.DescribeDBParametersInput{DBParameterGroupName: aws.String(parameterGroupName)}\n\n\tvar allParameters []types.Parameter\n\tfor {\n\t\toutput, err := rdsClient.DescribeDBParameters(context.Background(), &input)\n\t\tif err != nil {\n\t\t\treturn []types.Parameter{}, err\n\t\t}\n\n\t\tallParameters = append(allParameters, output.Parameters...)\n\t\tif output.Marker == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tinput.Marker = output.Marker\n\t}\n\treturn allParameters, nil\n}\n\n// GetRdsInstanceDetailsE gets the details of a single DB instance whose identifier is passed.\nfunc GetRdsInstanceDetailsE(t testing.TestingT, dbInstanceID string, awsRegion string) (*types.DBInstance, error) {\n\trdsClient := NewRdsClient(t, awsRegion)\n\tinput := rds.DescribeDBInstancesInput{DBInstanceIdentifier: aws.String(dbInstanceID)}\n\toutput, err := rdsClient.DescribeDBInstances(context.Background(), &input)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &output.DBInstances[0], nil\n}\n\n// NewRdsClient creates an RDS client.\nfunc NewRdsClient(t testing.TestingT, region string) *rds.Client {\n\tclient, err := NewRdsClientE(t, region)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn client\n}\n\n// NewRdsClientE creates an RDS client.\nfunc NewRdsClientE(t testing.TestingT, region string) (*rds.Client, error) {\n\tsess, err := NewAuthenticatedSession(region)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn rds.NewFromConfig(*sess), nil\n}\n\n// GetRecommendedRdsInstanceType takes in a list of RDS instance types (e.g., \"db.t2.micro\", \"db.t3.micro\") and returns the\n// first instance type in the list that is available in the given region and for the given database engine type.\n// If none of the instances provided are available for your combination of region and database engine, this function will exit with an error.\nfunc GetRecommendedRdsInstanceType(t testing.TestingT, region string, engine string, engineVersion string, instanceTypeOptions []string) string {\n\tout, err := GetRecommendedRdsInstanceTypeE(t, region, engine, engineVersion, instanceTypeOptions)\n\trequire.NoError(t, err)\n\treturn out\n}\n\n// GetRecommendedRdsInstanceTypeE takes in a list of RDS instance types (e.g., \"db.t2.micro\", \"db.t3.micro\") and returns the\n// first instance type in the list that is available in the given region and for the given database engine type.\n// If none of the instances provided are available for your combination of region and database engine, this function will return an error.\nfunc GetRecommendedRdsInstanceTypeE(t testing.TestingT, region string, engine string, engineVersion string, instanceTypeOptions []string) (string, error) {\n\tclient, err := NewRdsClientE(t, region)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn GetRecommendedRdsInstanceTypeWithClientE(t, client, engine, engineVersion, instanceTypeOptions)\n}\n\n// GetRecommendedRdsInstanceTypeWithClientE takes in a list of RDS instance types (e.g., \"db.t2.micro\", \"db.t3.micro\") and returns the\n// first instance type in the list that is available in the given region and for the given database engine type.\n// If none of the instances provided are available for your combination of region and database engine, this function will return an error.\n// This function expects an authenticated RDS client from the AWS SDK Go library.\nfunc GetRecommendedRdsInstanceTypeWithClientE(t testing.TestingT, rdsClient *rds.Client, engine string, engineVersion string, instanceTypeOptions []string) (string, error) {\n\tfor _, instanceTypeOption := range instanceTypeOptions {\n\t\tinstanceTypeExists, err := instanceTypeExistsForEngineAndRegionE(rdsClient, engine, engineVersion, instanceTypeOption)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\tif instanceTypeExists {\n\t\t\treturn instanceTypeOption, nil\n\t\t}\n\t}\n\treturn \"\", NoRdsInstanceTypeError{InstanceTypeOptions: instanceTypeOptions, DatabaseEngine: engine, DatabaseEngineVersion: engineVersion}\n}\n\n// instanceTypeExistsForEngineAndRegionE returns a boolean that represents whether the provided instance type (e.g. db.t2.micro) exists for the given region and db engine type\n// This function will return an error if the RDS AWS SDK call fails.\nfunc instanceTypeExistsForEngineAndRegionE(client *rds.Client, engine string, engineVersion string, instanceType string) (bool, error) {\n\tinput := rds.DescribeOrderableDBInstanceOptionsInput{\n\t\tEngine:          aws.String(engine),\n\t\tEngineVersion:   aws.String(engineVersion),\n\t\tDBInstanceClass: aws.String(instanceType),\n\t}\n\n\tout, err := client.DescribeOrderableDBInstanceOptions(context.Background(), &input)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tif len(out.OrderableDBInstanceOptions) > 0 {\n\t\treturn true, nil\n\t}\n\n\treturn false, nil\n}\n\n// GetValidEngineVersion returns a string containing a valid RDS engine version for the provided region and engine type.\n// This function will fail the test if no valid engine is found.\nfunc GetValidEngineVersion(t testing.TestingT, region string, engine string, majorVersion string) string {\n\tout, err := GetValidEngineVersionE(t, region, engine, majorVersion)\n\trequire.NoError(t, err)\n\treturn out\n}\n\n// GetValidEngineVersionE returns a string containing a valid RDS engine version or an error if no valid version is found.\nfunc GetValidEngineVersionE(t testing.TestingT, region string, engine string, majorVersion string) (string, error) {\n\tclient, err := NewRdsClientE(t, region)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tinput := rds.DescribeDBEngineVersionsInput{\n\t\tEngine:        aws.String(engine),\n\t\tEngineVersion: aws.String(majorVersion),\n\t}\n\tout, err := client.DescribeDBEngineVersions(context.Background(), &input)\n\tif err != nil || len(out.DBEngineVersions) == 0 {\n\t\treturn \"\", err\n\t}\n\treturn *out.DBEngineVersions[0].EngineVersion, nil\n}\n\n// ParameterForDbInstanceNotFound is an error that occurs when the parameter group specified is not found for the DB instance\ntype ParameterForDbInstanceNotFound struct {\n\tParameterName string\n\tDbInstanceID  string\n\tAwsRegion     string\n}\n\nfunc (err ParameterForDbInstanceNotFound) Error() string {\n\treturn fmt.Sprintf(\"Could not find a parameter %s in parameter group of database %s in %s\", err.ParameterName, err.DbInstanceID, err.AwsRegion)\n}\n\n// OptionGroupOptionSettingForDbInstanceNotFound is an error that occurs when the option setting specified is not found in the option group of the DB instance\ntype OptionGroupOptionSettingForDbInstanceNotFound struct {\n\tOptionName        string\n\tOptionSettingName string\n\tDbInstanceID      string\n\tAwsRegion         string\n}\n\nfunc (err OptionGroupOptionSettingForDbInstanceNotFound) Error() string {\n\treturn fmt.Sprintf(\"Could not find a option setting %s in option name %s of database %s in %s\", err.OptionName, err.OptionSettingName, err.DbInstanceID, err.AwsRegion)\n}\n"
  },
  {
    "path": "modules/aws/rds_test.go",
    "content": "package aws\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestGetRecommendedRdsInstanceTypeHappyPath(t *testing.T) {\n\ttype TestingScenerios struct {\n\t\tname               string\n\t\tregion             string\n\t\tdatabaseEngine     string\n\t\tengineMajorVersion string\n\t\tinstanceTypes      []string\n\t\texpected           string\n\t}\n\n\ttestingScenerios := []TestingScenerios{\n\t\t{\n\t\t\tname:               \"US region, mysql, first offering available\",\n\t\t\tregion:             \"us-east-2\",\n\t\t\tdatabaseEngine:     \"mysql\",\n\t\t\tengineMajorVersion: \"8.0\",\n\t\t\tinstanceTypes:      []string{\"db.t4g.micro\", \"db.t4g.small\"},\n\t\t\texpected:           \"db.t4g.micro\",\n\t\t},\n\t\t{\n\t\t\tname:               \"EU region, postgres, 2nd offering available based on region\",\n\t\t\tregion:             \"eu-north-1\",\n\t\t\tdatabaseEngine:     \"postgres\",\n\t\t\tengineMajorVersion: \"13\",\n\t\t\tinstanceTypes:      []string{\"db.t2.micro\", \"db.m5.large\"},\n\t\t\texpected:           \"db.m5.large\",\n\t\t},\n\t\t{\n\t\t\tname:               \"US region, oracle-ee, 2nd offering available based on db type\",\n\t\t\tregion:             \"us-west-2\",\n\t\t\tdatabaseEngine:     \"oracle-ee\",\n\t\t\tengineMajorVersion: \"19\",\n\t\t\tinstanceTypes:      []string{\"db.m5d.xlarge\", \"db.m5.large\"},\n\t\t\texpected:           \"db.m5d.xlarge\",\n\t\t},\n\t\t{\n\t\t\tname:               \"US region, oracle-ee, 2nd offering available based on db engine version\",\n\t\t\tregion:             \"us-west-2\",\n\t\t\tdatabaseEngine:     \"oracle-ee\",\n\t\t\tengineMajorVersion: \"19\",\n\t\t\tinstanceTypes:      []string{\"db.t3.micro\", \"db.t3.small\"},\n\t\t\texpected:           \"db.t3.small\",\n\t\t},\n\t}\n\n\tfor _, scenerio := range testingScenerios {\n\t\tscenerio := scenerio\n\n\t\tt.Run(scenerio.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tengineVersion := GetValidEngineVersion(t, scenerio.region, scenerio.databaseEngine, scenerio.engineMajorVersion)\n\t\t\tactual, err := GetRecommendedRdsInstanceTypeE(t, scenerio.region, scenerio.databaseEngine, engineVersion, scenerio.instanceTypes)\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Equal(t, scenerio.expected, actual)\n\t\t})\n\t}\n}\n\nfunc TestGetRecommendedRdsInstanceTypeErrors(t *testing.T) {\n\ttype TestingScenerios struct {\n\t\tname                  string\n\t\tregion                string\n\t\tdatabaseEngine        string\n\t\tdatabaseEngineVersion string\n\t\tinstanceTypes         []string\n\t}\n\n\ttestingScenerios := []TestingScenerios{\n\t\t{\n\t\t\tname:                  \"All empty\",\n\t\t\tregion:                \"\",\n\t\t\tdatabaseEngine:        \"\",\n\t\t\tdatabaseEngineVersion: \"\",\n\t\t\tinstanceTypes:         nil,\n\t\t},\n\t\t{\n\t\t\tname:                  \"No engine, version, or instance type\",\n\t\t\tregion:                \"us-east-2\",\n\t\t\tdatabaseEngine:        \"\",\n\t\t\tdatabaseEngineVersion: \"\",\n\t\t\tinstanceTypes:         nil,\n\t\t},\n\t\t{\n\t\t\tname:                  \"No instance types or version\",\n\t\t\tregion:                \"us-east-2\",\n\t\t\tdatabaseEngine:        \"mysql\",\n\t\t\tdatabaseEngineVersion: \"\",\n\t\t\tinstanceTypes:         nil,\n\t\t},\n\t\t{\n\t\t\tname:                  \"No engine version\",\n\t\t\tregion:                \"us-east-2\",\n\t\t\tdatabaseEngine:        \"mysql\",\n\t\t\tdatabaseEngineVersion: \"\",\n\t\t\tinstanceTypes:         []string{\"db.t3.small\"},\n\t\t},\n\t\t{\n\t\t\tname:                  \"Invalid instance types\",\n\t\t\tregion:                \"us-east-2\",\n\t\t\tdatabaseEngine:        \"mysql\",\n\t\t\tdatabaseEngineVersion: \"\",\n\t\t\tinstanceTypes:         []string{\"db.nonexistent.type\", \"db.fake.instance\"},\n\t\t},\n\t\t{\n\t\t\tname:                  \"Instance type not available for engine\",\n\t\t\tregion:                \"us-east-2\",\n\t\t\tdatabaseEngine:        \"mysql\",\n\t\t\tdatabaseEngineVersion: \"\",\n\t\t\tinstanceTypes:         []string{\"db.x2iedn.metal\"},\n\t\t},\n\t\t{\n\t\t\tname:                  \"No instance type available for engine\",\n\t\t\tregion:                \"us-east-1\",\n\t\t\tdatabaseEngine:        \"oracle-ee\",\n\t\t\tdatabaseEngineVersion: \"19.0.0.0.ru-2024-04.rur-2024-04.r1\",\n\t\t\tinstanceTypes:         []string{\"db.r5a.large\"},\n\t\t},\n\t\t{\n\t\t\tname:                  \"No instance type available for engine version\",\n\t\t\tregion:                \"us-east-1\",\n\t\t\tdatabaseEngine:        \"oracle-ee\",\n\t\t\tdatabaseEngineVersion: \"19.0.0.0.ru-2021-01.rur-2021-01.r1\",\n\t\t\tinstanceTypes:         []string{\"db.t3.micro\"},\n\t\t},\n\t}\n\n\tfor _, scenerio := range testingScenerios {\n\t\tscenerio := scenerio\n\n\t\tt.Run(scenerio.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\t_, err := GetRecommendedRdsInstanceTypeE(t, scenerio.region, scenerio.databaseEngine, scenerio.databaseEngineVersion, scenerio.instanceTypes)\n\t\t\tfmt.Println(err)\n\t\t\tassert.EqualError(t, err, NoRdsInstanceTypeError{InstanceTypeOptions: scenerio.instanceTypes, DatabaseEngine: scenerio.databaseEngine, DatabaseEngineVersion: scenerio.databaseEngineVersion}.Error())\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "modules/aws/region.go",
    "content": "package aws\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\t\"github.com/aws/aws-sdk-go-v2/service/ec2\"\n\t\"github.com/aws/aws-sdk-go-v2/service/ssm\"\n\t\"github.com/gruntwork-io/terratest/modules/collections\"\n\t\"github.com/gruntwork-io/terratest/modules/logger\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n)\n\n// You can set this environment variable to force Terratest to use a specific region rather than a random one. This is\n// convenient when iterating locally.\nconst regionOverrideEnvVarName = \"TERRATEST_REGION\"\n\n// AWS API calls typically require an AWS region. We typically require the user to set one explicitly, but in some\n// cases, this doesn't make sense (e.g., for fetching the list of regions in an account), so for those cases, we use\n// this region as a default.\nconst defaultRegion = \"us-east-1\"\n\n// Reference for launch dates: https://aws.amazon.com/about-aws/global-infrastructure/\nvar stableRegions = []string{\n\t\"us-east-1\",      // Launched 2006\n\t\"us-east-2\",      // Launched 2016\n\t\"us-west-1\",      // Launched 2009\n\t\"us-west-2\",      // Launched 2011\n\t\"ca-central-1\",   // Launched 2016\n\t\"sa-east-1\",      // Launched 2011\n\t\"eu-west-1\",      // Launched 2007\n\t\"eu-west-2\",      // Launched 2016\n\t\"eu-west-3\",      // Launched 2017\n\t\"eu-central-1\",   // Launched 2014\n\t\"ap-southeast-1\", // Launched 2010\n\t\"ap-southeast-2\", // Launched 2012\n\t\"ap-northeast-1\", // Launched 2011\n\t\"ap-northeast-2\", // Launched 2016\n\t\"ap-south-1\",     // Launched 2016\n\t\"eu-north-1\",     // Launched 2018\n}\n\n// GetRandomStableRegion gets a randomly chosen AWS region that is considered stable. Like GetRandomRegion, you can\n// further restrict the stable region list using approvedRegions and forbiddenRegions. We consider stable regions to be\n// those that have been around for at least 1 year.\n// Note that regions in the approvedRegions list that are not considered stable are ignored.\nfunc GetRandomStableRegion(t testing.TestingT, approvedRegions []string, forbiddenRegions []string) string {\n\tregionsToPickFrom := stableRegions\n\tif len(approvedRegions) > 0 {\n\t\tregionsToPickFrom = collections.ListIntersection(regionsToPickFrom, approvedRegions)\n\t}\n\tif len(forbiddenRegions) > 0 {\n\t\tregionsToPickFrom = collections.ListSubtract(regionsToPickFrom, forbiddenRegions)\n\t}\n\treturn GetRandomRegion(t, regionsToPickFrom, nil)\n}\n\n// GetRandomRegion gets a randomly chosen AWS region. If approvedRegions is not empty, this will be a region from the approvedRegions\n// list; otherwise, this method will fetch the latest list of regions from the AWS APIs and pick one of those. If\n// forbiddenRegions is not empty, this method will make sure the returned region is not in the forbiddenRegions list.\nfunc GetRandomRegion(t testing.TestingT, approvedRegions []string, forbiddenRegions []string) string {\n\tregion, err := GetRandomRegionE(t, approvedRegions, forbiddenRegions)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn region\n}\n\n// GetRandomRegionE gets a randomly chosen AWS region. If approvedRegions is not empty, this will be a region from the approvedRegions\n// list; otherwise, this method will fetch the latest list of regions from the AWS APIs and pick one of those. If\n// forbiddenRegions is not empty, this method will make sure the returned region is not in the forbiddenRegions list.\nfunc GetRandomRegionE(t testing.TestingT, approvedRegions []string, forbiddenRegions []string) (string, error) {\n\tregionFromEnvVar := os.Getenv(regionOverrideEnvVarName)\n\tif regionFromEnvVar != \"\" {\n\t\tlogger.Default.Logf(t, \"Using AWS region %s from environment variable %s\", regionFromEnvVar, regionOverrideEnvVarName)\n\t\treturn regionFromEnvVar, nil\n\t}\n\n\tregionsToPickFrom := approvedRegions\n\n\tif len(regionsToPickFrom) == 0 {\n\t\tallRegions, err := GetAllAwsRegionsE(t)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\tregionsToPickFrom = allRegions\n\t}\n\n\tregionsToPickFrom = collections.ListSubtract(regionsToPickFrom, forbiddenRegions)\n\tregion := random.RandomString(regionsToPickFrom)\n\n\tlogger.Default.Logf(t, \"Using region %s\", region)\n\treturn region, nil\n}\n\n// GetAllAwsRegions gets the list of AWS regions available in this account.\nfunc GetAllAwsRegions(t testing.TestingT) []string {\n\tout, err := GetAllAwsRegionsE(t)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn out\n}\n\n// GetAllAwsRegionsE gets the list of AWS regions available in this account.\nfunc GetAllAwsRegionsE(t testing.TestingT) ([]string, error) {\n\tlogger.Default.Logf(t, \"Looking up all AWS regions available in this account\")\n\n\tec2Client, err := NewEc2ClientE(t, defaultRegion)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tout, err := ec2Client.DescribeRegions(context.Background(), &ec2.DescribeRegionsInput{})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar regions []string\n\tfor _, region := range out.Regions {\n\t\tregions = append(regions, aws.ToString(region.RegionName))\n\t}\n\n\treturn regions, nil\n}\n\n// GetAvailabilityZones gets the Availability Zones for a given AWS region. Note that for certain regions (e.g. us-east-1), different AWS\n// accounts have access to different availability zones.\nfunc GetAvailabilityZones(t testing.TestingT, region string) []string {\n\tout, err := GetAvailabilityZonesE(t, region)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn out\n}\n\n// GetAvailabilityZonesE gets the Availability Zones for a given AWS region. Note that for certain regions (e.g. us-east-1), different AWS\n// accounts have access to different availability zones.\nfunc GetAvailabilityZonesE(t testing.TestingT, region string) ([]string, error) {\n\tlogger.Default.Logf(t, \"Looking up all availability zones available in this account for region %s\", region)\n\n\tec2Client, err := NewEc2ClientE(t, region)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresp, err := ec2Client.DescribeAvailabilityZones(context.Background(), &ec2.DescribeAvailabilityZonesInput{})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar out []string\n\tfor _, availabilityZone := range resp.AvailabilityZones {\n\t\tout = append(out, aws.ToString(availabilityZone.ZoneName))\n\t}\n\n\treturn out, nil\n}\n\n// GetRegionsForService gets all AWS regions in which a service is available.\nfunc GetRegionsForService(t testing.TestingT, serviceName string) []string {\n\tout, err := GetRegionsForServiceE(t, serviceName)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn out\n}\n\n// GetRegionsForServiceE gets all AWS regions in which a service is available and returns errors.\n// See https://docs.aws.amazon.com/systems-manager/latest/userguide/parameter-store-public-parameters-global-infrastructure.html\nfunc GetRegionsForServiceE(t testing.TestingT, serviceName string) ([]string, error) {\n\t// These values are available in any region, defaulting to us-east-1 since it's the oldest\n\tssmClient, err := NewSsmClientE(t, \"us-east-1\")\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tparamPath := \"/aws/service/global-infrastructure/services/%s/regions\"\n\tresp, err := ssmClient.GetParametersByPath(context.Background(), &ssm.GetParametersByPathInput{\n\t\tPath: aws.String(fmt.Sprintf(paramPath, serviceName)),\n\t})\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar availableRegions []string\n\tfor _, p := range resp.Parameters {\n\t\tavailableRegions = append(availableRegions, *p.Value)\n\t}\n\n\treturn availableRegions, nil\n}\n\n// GetRandomRegionForService retrieves a list of AWS regions in which a service is available\n// Then returns one region randomly from the list\nfunc GetRandomRegionForService(t testing.TestingT, serviceName string) string {\n\tavailableRegions, err := GetRegionsForServiceE(t, serviceName)\n\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\treturn GetRandomRegion(t, availableRegions, nil)\n}\n"
  },
  {
    "path": "modules/aws/region_test.go",
    "content": "package aws\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestGetRandomRegion(t *testing.T) {\n\tt.Parallel()\n\n\trandomRegion := GetRandomRegion(t, nil, nil)\n\tassertLooksLikeRegionName(t, randomRegion)\n}\n\nfunc TestGetRandomRegionExcludesForbiddenRegions(t *testing.T) {\n\tt.Parallel()\n\n\tapprovedRegions := []string{\"ca-central-1\", \"us-east-1\", \"us-east-2\", \"us-west-1\", \"us-west-2\", \"eu-west-1\", \"eu-west-2\", \"eu-central-1\", \"ap-southeast-1\", \"ap-northeast-1\", \"ap-northeast-2\", \"ap-south-1\"}\n\tforbiddenRegions := []string{\"us-west-2\", \"ap-northeast-2\"}\n\n\tfor i := 0; i < 1000; i++ {\n\t\trandomRegion := GetRandomRegion(t, approvedRegions, forbiddenRegions)\n\t\tassert.NotContains(t, forbiddenRegions, randomRegion)\n\t}\n}\n\nfunc TestGetAllAwsRegions(t *testing.T) {\n\tt.Parallel()\n\n\tregions := GetAllAwsRegions(t)\n\n\t// The typical account had access to 15 regions as of April, 2018: https://aws.amazon.com/about-aws/global-infrastructure/\n\tassert.True(t, len(regions) >= 15, \"Number of regions: %d\", len(regions))\n\tfor _, region := range regions {\n\t\tassertLooksLikeRegionName(t, region)\n\t}\n}\n\nfunc assertLooksLikeRegionName(t *testing.T, regionName string) {\n\tassert.Regexp(t, \"[a-z]{2}-[a-z]+?-[[:digit:]]+\", regionName)\n}\n\nfunc TestGetAvailabilityZones(t *testing.T) {\n\tt.Parallel()\n\n\trandomRegion := GetRandomStableRegion(t, nil, nil)\n\tazs := GetAvailabilityZones(t, randomRegion)\n\n\t// Every AWS account has access to different AZs, so he best we can do is make sure we get at least one back\n\tassert.True(t, len(azs) > 1)\n\tfor _, az := range azs {\n\t\tassert.Regexp(t, fmt.Sprintf(\"^%s[a-z]$\", randomRegion), az)\n\t}\n}\n\nfunc TestGetRandomRegionForService(t *testing.T) {\n\tt.Parallel()\n\n\tserviceName := \"apigatewayv2\"\n\n\tregionsForService, _ := GetRegionsForServiceE(t, serviceName)\n\trandomRegionForService := GetRandomRegionForService(t, serviceName)\n\n\tassert.Contains(t, regionsForService, randomRegionForService)\n}\n"
  },
  {
    "path": "modules/aws/route53.go",
    "content": "package aws\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\t\"github.com/aws/aws-sdk-go-v2/service/route53\"\n\t\"github.com/aws/aws-sdk-go-v2/service/route53/types\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// GetRoute53Record returns a Route 53 Record\nfunc GetRoute53Record(t *testing.T, hostedZoneID, recordName, recordType, awsRegion string) *types.ResourceRecordSet {\n\tr, err := GetRoute53RecordE(t, hostedZoneID, recordName, recordType, awsRegion)\n\trequire.NoError(t, err)\n\n\treturn r\n}\n\n// GetRoute53RecordE returns a Route 53 Record\nfunc GetRoute53RecordE(t *testing.T, hostedZoneID, recordName, recordType, awsRegion string) (*types.ResourceRecordSet, error) {\n\troute53Client, err := NewRoute53ClientE(t, awsRegion)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\to, err := route53Client.ListResourceRecordSets(context.Background(), &route53.ListResourceRecordSetsInput{\n\t\tHostedZoneId:    &hostedZoneID,\n\t\tStartRecordName: &recordName,\n\t\tStartRecordType: types.RRType(recordType),\n\t\tMaxItems:        aws.Int32(1),\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfor _, record := range o.ResourceRecordSets {\n\t\tif strings.EqualFold(recordName+\".\", *record.Name) {\n\t\t\treturn &record, nil\n\t\t}\n\t}\n\n\treturn nil, fmt.Errorf(\"record not found\")\n}\n\n// NewRoute53Client creates a route 53 client.\nfunc NewRoute53Client(t *testing.T, region string) *route53.Client {\n\tc, err := NewRoute53ClientE(t, region)\n\trequire.NoError(t, err)\n\n\treturn c\n}\n\n// NewRoute53ClientE creates a route 53 client.\nfunc NewRoute53ClientE(t *testing.T, region string) (*route53.Client, error) {\n\tsess, err := NewAuthenticatedSession(region)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn route53.NewFromConfig(*sess), nil\n}\n"
  },
  {
    "path": "modules/aws/route53_test.go",
    "content": "package aws\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\t\"github.com/aws/aws-sdk-go-v2/service/route53\"\n\t\"github.com/aws/aws-sdk-go-v2/service/route53/types\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestRoute53Record(t *testing.T) {\n\tt.Parallel()\n\tregion := GetRandomStableRegion(t, nil, nil)\n\tc, err := NewRoute53ClientE(t, region)\n\trequire.NoError(t, err)\n\n\tdomain := fmt.Sprintf(\"terratest%dexample.com\", time.Now().UnixNano())\n\thostedZone, err := c.CreateHostedZone(context.Background(), &route53.CreateHostedZoneInput{\n\t\tName:            aws.String(domain),\n\t\tCallerReference: aws.String(fmt.Sprint(time.Now().UnixNano())),\n\t})\n\trequire.NoError(t, err)\n\tt.Cleanup(func() {\n\t\t_, err := c.DeleteHostedZone(context.Background(), &route53.DeleteHostedZoneInput{\n\t\t\tId: hostedZone.HostedZone.Id,\n\t\t})\n\t\trequire.NoError(t, err)\n\t})\n\n\trecordName := fmt.Sprintf(\"record.%s\", domain)\n\tresourceRecordSet := &types.ResourceRecordSet{\n\t\tName: &recordName,\n\t\tType: types.RRTypeA,\n\t\tTTL:  aws.Int64(60),\n\t\tResourceRecords: []types.ResourceRecord{\n\t\t\t{\n\t\t\t\tValue: aws.String(\"127.0.0.1\"),\n\t\t\t},\n\t\t},\n\t}\n\t_, err = c.ChangeResourceRecordSets(context.Background(), &route53.ChangeResourceRecordSetsInput{\n\t\tHostedZoneId: hostedZone.HostedZone.Id,\n\t\tChangeBatch: &types.ChangeBatch{\n\t\t\tChanges: []types.Change{\n\t\t\t\t{\n\t\t\t\t\tAction:            types.ChangeActionCreate,\n\t\t\t\t\tResourceRecordSet: resourceRecordSet,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t})\n\trequire.NoError(t, err)\n\tt.Cleanup(func() {\n\t\t_, err := c.ChangeResourceRecordSets(context.Background(), &route53.ChangeResourceRecordSetsInput{\n\t\t\tHostedZoneId: hostedZone.HostedZone.Id,\n\t\t\tChangeBatch: &types.ChangeBatch{\n\t\t\t\tChanges: []types.Change{\n\t\t\t\t\t{\n\t\t\t\t\t\tAction:            types.ChangeActionDelete,\n\t\t\t\t\t\tResourceRecordSet: resourceRecordSet,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t})\n\t\trequire.NoError(t, err)\n\t})\n\n\tt.Run(\"ExistingRecord\", func(t *testing.T) {\n\t\troute53Record := GetRoute53Record(t, *hostedZone.HostedZone.Id, recordName, string(resourceRecordSet.Type), region)\n\t\trequire.NotNil(t, route53Record)\n\t\tassert.Equal(t, recordName+\".\", *route53Record.Name)\n\t\tassert.Equal(t, resourceRecordSet.Type, route53Record.Type)\n\t\tassert.Equal(t, \"127.0.0.1\", *route53Record.ResourceRecords[0].Value)\n\t})\n\n\tt.Run(\"NotExistRecord\", func(t *testing.T) {\n\t\troute53Record, err := GetRoute53RecordE(t, *hostedZone.HostedZone.Id, \"ne\"+recordName, \"A\", region)\n\t\tassert.Error(t, err)\n\t\tassert.Nil(t, route53Record)\n\t})\n\n}\n"
  },
  {
    "path": "modules/aws/s3.go",
    "content": "package aws\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"strings\"\n\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\t\"github.com/aws/aws-sdk-go-v2/feature/s3/manager\"\n\t\"github.com/aws/aws-sdk-go-v2/service/s3\"\n\t\"github.com/aws/aws-sdk-go-v2/service/s3/types\"\n\t\"github.com/gruntwork-io/terratest/modules/logger\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// FindS3BucketWithTag finds the name of the S3 bucket in the given region with the given tag key=value.\nfunc FindS3BucketWithTag(t testing.TestingT, awsRegion string, key string, value string) string {\n\tbucket, err := FindS3BucketWithTagE(t, awsRegion, key, value)\n\trequire.NoError(t, err)\n\n\treturn bucket\n}\n\n// FindS3BucketWithTagE finds the name of the S3 bucket in the given region with the given tag key=value.\nfunc FindS3BucketWithTagE(t testing.TestingT, awsRegion string, key string, value string) (string, error) {\n\ts3Client, err := NewS3ClientE(t, awsRegion)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tresp, err := s3Client.ListBuckets(context.Background(), &s3.ListBucketsInput{})\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tfor _, bucket := range resp.Buckets {\n\t\ttagResponse, err := s3Client.GetBucketTagging(context.Background(), &s3.GetBucketTaggingInput{Bucket: bucket.Name})\n\n\t\tif err != nil {\n\t\t\tif strings.Contains(err.Error(), \"NoSuchBucket\") {\n\t\t\t\t// Occasionally, the ListBuckets call will return a bucket that has been deleted by S3\n\t\t\t\t// but hasn't yet been actually removed from the backend. Listing tags on that bucket\n\t\t\t\t// will return this error. If the bucket has been deleted, it can't be the one to find,\n\t\t\t\t// so just ignore this error, and keep checking the other buckets.\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif !strings.Contains(err.Error(), \"AuthorizationHeaderMalformed\") &&\n\t\t\t\t!strings.Contains(err.Error(), \"BucketRegionError\") &&\n\t\t\t\t!strings.Contains(err.Error(), \"NoSuchTagSet\") {\n\t\t\t\treturn \"\", err\n\t\t\t}\n\t\t}\n\n\t\tfor _, tag := range tagResponse.TagSet {\n\t\t\tif *tag.Key == key && *tag.Value == value {\n\t\t\t\tlogger.Default.Logf(t, \"Found S3 bucket %s with tag %s=%s\", *bucket.Name, key, value)\n\t\t\t\treturn *bucket.Name, nil\n\t\t\t}\n\t\t}\n\t}\n\n\treturn \"\", nil\n}\n\n// GetS3BucketTags fetches the given bucket's tags and returns them as a string map of strings.\nfunc GetS3BucketTags(t testing.TestingT, awsRegion string, bucket string) map[string]string {\n\ttags, err := GetS3BucketTagsE(t, awsRegion, bucket)\n\trequire.NoError(t, err)\n\n\treturn tags\n}\n\n// GetS3BucketTagsE fetches the given bucket's tags and returns them as a string map of strings.\nfunc GetS3BucketTagsE(t testing.TestingT, awsRegion string, bucket string) (map[string]string, error) {\n\ts3Client, err := NewS3ClientE(t, awsRegion)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tout, err := s3Client.GetBucketTagging(context.Background(), &s3.GetBucketTaggingInput{\n\t\tBucket: &bucket,\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\ttags := map[string]string{}\n\tfor _, tag := range out.TagSet {\n\t\ttags[aws.ToString(tag.Key)] = aws.ToString(tag.Value)\n\t}\n\n\treturn tags, nil\n}\n\n// GetS3ObjectContents fetches the contents of the object in the given bucket with the given key and return it as a string.\nfunc GetS3ObjectContents(t testing.TestingT, awsRegion string, bucket string, key string) string {\n\tcontents, err := GetS3ObjectContentsE(t, awsRegion, bucket, key)\n\trequire.NoError(t, err)\n\n\treturn contents\n}\n\n// GetS3ObjectContentsE fetches the contents of the object in the given bucket with the given key and return it as a string.\nfunc GetS3ObjectContentsE(t testing.TestingT, awsRegion string, bucket string, key string) (string, error) {\n\ts3Client, err := NewS3ClientE(t, awsRegion)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tres, err := s3Client.GetObject(context.Background(), &s3.GetObjectInput{\n\t\tBucket: &bucket,\n\t\tKey:    &key,\n\t})\n\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tbuf := new(bytes.Buffer)\n\t_, err = buf.ReadFrom(res.Body)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tcontents := buf.String()\n\tlogger.Default.Logf(t, \"Read contents from s3://%s/%s\", bucket, key)\n\n\treturn contents, nil\n}\n\n// PutS3ObjectContents puts the contents of the object in the given bucket with the given key.\nfunc PutS3ObjectContents(t testing.TestingT, awsRegion string, bucket string, key string, body io.Reader) {\n\terr := PutS3ObjectContentsE(t, awsRegion, bucket, key, body)\n\trequire.NoError(t, err)\n}\n\n// PutS3ObjectContents puts the contents of the object in the given bucket with the given key.\nfunc PutS3ObjectContentsE(t testing.TestingT, awsRegion string, bucket string, key string, body io.Reader) error {\n\ts3Client, err := NewS3ClientE(t, awsRegion)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to instantiate s3 client: %w\", err)\n\t}\n\n\tparams := &s3.PutObjectInput{\n\t\tBucket: aws.String(bucket),\n\t\tKey:    aws.String(key),\n\t\tBody:   body,\n\t}\n\n\t_, err = s3Client.PutObject(context.Background(), params)\n\treturn err\n}\n\n// CreateS3Bucket creates an S3 bucket in the given region with the given name. Note that S3 bucket names must be globally unique.\nfunc CreateS3Bucket(t testing.TestingT, region string, name string) {\n\terr := CreateS3BucketE(t, region, name)\n\trequire.NoError(t, err)\n}\n\n// CreateS3BucketE creates an S3 bucket in the given region with the given name. Note that S3 bucket names must be globally unique.\nfunc CreateS3BucketE(t testing.TestingT, region string, name string) error {\n\tlogger.Default.Logf(t, \"Creating bucket %s in %s\", name, region)\n\n\ts3Client, err := NewS3ClientE(t, region)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tparams := &s3.CreateBucketInput{\n\t\tBucket:          aws.String(name),\n\t\tObjectOwnership: types.ObjectOwnershipObjectWriter,\n\t}\n\n\tif region != \"us-east-1\" {\n\t\tparams.CreateBucketConfiguration = &types.CreateBucketConfiguration{\n\t\t\tLocationConstraint: types.BucketLocationConstraint(region),\n\t\t}\n\t}\n\n\t_, err = s3Client.CreateBucket(context.Background(), params)\n\treturn err\n}\n\n// PutS3BucketPolicy applies an IAM resource policy to a given S3 bucket to create its bucket policy\nfunc PutS3BucketPolicy(t testing.TestingT, region string, bucketName string, policyJSONString string) {\n\terr := PutS3BucketPolicyE(t, region, bucketName, policyJSONString)\n\trequire.NoError(t, err)\n}\n\n// PutS3BucketPolicyE applies an IAM resource policy to a given S3 bucket to create its bucket policy\nfunc PutS3BucketPolicyE(t testing.TestingT, region string, bucketName string, policyJSONString string) error {\n\tlogger.Default.Logf(t, \"Applying bucket policy for bucket %s in %s\", bucketName, region)\n\n\ts3Client, err := NewS3ClientE(t, region)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tinput := &s3.PutBucketPolicyInput{\n\t\tBucket: aws.String(bucketName),\n\t\tPolicy: aws.String(policyJSONString),\n\t}\n\n\t_, err = s3Client.PutBucketPolicy(context.Background(), input)\n\treturn err\n}\n\n// PutS3BucketVersioning creates an S3 bucket versioning configuration in the given region against the given bucket name, WITHOUT requiring MFA to remove versioning.\nfunc PutS3BucketVersioning(t testing.TestingT, region string, bucketName string) {\n\terr := PutS3BucketVersioningE(t, region, bucketName)\n\trequire.NoError(t, err)\n}\n\n// PutS3BucketVersioningE creates an S3 bucket versioning configuration in the given region against the given bucket name, WITHOUT requiring MFA to remove versioning.\nfunc PutS3BucketVersioningE(t testing.TestingT, region string, bucketName string) error {\n\tlogger.Default.Logf(t, \"Creating bucket versioning configuration for bucket %s in %s\", bucketName, region)\n\n\ts3Client, err := NewS3ClientE(t, region)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tinput := &s3.PutBucketVersioningInput{\n\t\tBucket: aws.String(bucketName),\n\t\tVersioningConfiguration: &types.VersioningConfiguration{\n\t\t\tMFADelete: types.MFADeleteDisabled,\n\t\t\tStatus:    types.BucketVersioningStatusEnabled,\n\t\t},\n\t}\n\n\t_, err = s3Client.PutBucketVersioning(context.Background(), input)\n\treturn err\n}\n\n// DeleteS3Bucket destroys the S3 bucket in the given region with the given name.\nfunc DeleteS3Bucket(t testing.TestingT, region string, name string) {\n\terr := DeleteS3BucketE(t, region, name)\n\trequire.NoError(t, err)\n}\n\n// DeleteS3BucketE destroys the S3 bucket in the given region with the given name.\nfunc DeleteS3BucketE(t testing.TestingT, region string, name string) error {\n\tlogger.Default.Logf(t, \"Deleting bucket %s in %s\", region, name)\n\n\ts3Client, err := NewS3ClientE(t, region)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tparams := &s3.DeleteBucketInput{\n\t\tBucket: aws.String(name),\n\t}\n\t_, err = s3Client.DeleteBucket(context.Background(), params)\n\treturn err\n}\n\n// EmptyS3Bucket removes the contents of an S3 bucket in the given region with the given name.\nfunc EmptyS3Bucket(t testing.TestingT, region string, name string) {\n\terr := EmptyS3BucketE(t, region, name)\n\trequire.NoError(t, err)\n}\n\n// EmptyS3BucketE removes the contents of an S3 bucket in the given region with the given name.\nfunc EmptyS3BucketE(t testing.TestingT, region string, name string) error {\n\tlogger.Default.Logf(t, \"Emptying bucket %s in %s\", name, region)\n\n\ts3Client, err := NewS3ClientE(t, region)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tparams := &s3.ListObjectVersionsInput{\n\t\tBucket: aws.String(name),\n\t}\n\n\tfor {\n\t\t// Requesting a batch of objects from s3 bucket\n\t\tbucketObjects, err := s3Client.ListObjectVersions(context.Background(), params)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t// Checks if the bucket is already empty\n\t\tif len((*bucketObjects).Versions) == 0 {\n\t\t\tlogger.Default.Logf(t, \"Bucket %s is already empty\", name)\n\t\t\treturn nil\n\t\t}\n\n\t\t// creating an array of pointers of ObjectIdentifier\n\t\tobjectsToDelete := make([]types.ObjectIdentifier, 0, 1000)\n\t\tfor _, object := range (*bucketObjects).Versions {\n\t\t\tobj := types.ObjectIdentifier{\n\t\t\t\tKey:       object.Key,\n\t\t\t\tVersionId: object.VersionId,\n\t\t\t}\n\t\t\tobjectsToDelete = append(objectsToDelete, obj)\n\t\t}\n\n\t\tfor _, object := range (*bucketObjects).DeleteMarkers {\n\t\t\tobj := types.ObjectIdentifier{\n\t\t\t\tKey:       object.Key,\n\t\t\t\tVersionId: object.VersionId,\n\t\t\t}\n\t\t\tobjectsToDelete = append(objectsToDelete, obj)\n\t\t}\n\n\t\t// Creating JSON payload for bulk delete\n\t\tdeleteArray := types.Delete{Objects: objectsToDelete}\n\t\tdeleteParams := &s3.DeleteObjectsInput{\n\t\t\tBucket: aws.String(name),\n\t\t\tDelete: &deleteArray,\n\t\t}\n\n\t\t// Running the Bulk delete job (limit 1000)\n\t\t_, err = s3Client.DeleteObjects(context.Background(), deleteParams)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif *(*bucketObjects).IsTruncated { // if there are more objects in the bucket, IsTruncated = true\n\t\t\t// params.Marker = (*deleteParams).Delete.Objects[len((*deleteParams).Delete.Objects)-1].Key\n\t\t\tparams.KeyMarker = bucketObjects.NextKeyMarker\n\t\t\tlogger.Default.Logf(t, \"Requesting next batch | %s\", *(params.KeyMarker))\n\t\t} else { // if all objects in the bucket have been cleaned up.\n\t\t\tbreak\n\t\t}\n\t}\n\tlogger.Default.Logf(t, \"Bucket %s is now empty\", name)\n\treturn err\n}\n\n// GetS3BucketLoggingTarget fetches the given bucket's logging target bucket and returns it as a string\nfunc GetS3BucketLoggingTarget(t testing.TestingT, awsRegion string, bucket string) string {\n\tloggingTarget, err := GetS3BucketLoggingTargetE(t, awsRegion, bucket)\n\trequire.NoError(t, err)\n\n\treturn loggingTarget\n}\n\n// GetS3BucketLoggingTargetE fetches the given bucket's logging target bucket and returns it as the following string:\n// `TargetBucket` of the `LoggingEnabled` property for an S3 bucket\nfunc GetS3BucketLoggingTargetE(t testing.TestingT, awsRegion string, bucket string) (string, error) {\n\ts3Client, err := NewS3ClientE(t, awsRegion)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tres, err := s3Client.GetBucketLogging(context.Background(), &s3.GetBucketLoggingInput{\n\t\tBucket: &bucket,\n\t})\n\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif res.LoggingEnabled == nil {\n\t\treturn \"\", S3AccessLoggingNotEnabledErr{bucket, awsRegion}\n\t}\n\n\treturn aws.ToString(res.LoggingEnabled.TargetBucket), nil\n}\n\n// GetS3BucketLoggingTargetPrefix fetches the given bucket's logging object prefix and returns it as a string\nfunc GetS3BucketLoggingTargetPrefix(t testing.TestingT, awsRegion string, bucket string) string {\n\tloggingObjectTargetPrefix, err := GetS3BucketLoggingTargetPrefixE(t, awsRegion, bucket)\n\trequire.NoError(t, err)\n\n\treturn loggingObjectTargetPrefix\n}\n\n// GetS3BucketLoggingTargetPrefixE fetches the given bucket's logging object prefix and returns it as the following string:\n// `TargetPrefix` of the `LoggingEnabled` property for an S3 bucket\nfunc GetS3BucketLoggingTargetPrefixE(t testing.TestingT, awsRegion string, bucket string) (string, error) {\n\ts3Client, err := NewS3ClientE(t, awsRegion)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tres, err := s3Client.GetBucketLogging(context.Background(), &s3.GetBucketLoggingInput{\n\t\tBucket: &bucket,\n\t})\n\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif res.LoggingEnabled == nil {\n\t\treturn \"\", S3AccessLoggingNotEnabledErr{bucket, awsRegion}\n\t}\n\n\treturn aws.ToString(res.LoggingEnabled.TargetPrefix), nil\n}\n\n// GetS3BucketVersioning fetches the given bucket's versioning configuration status and returns it as a string\nfunc GetS3BucketVersioning(t testing.TestingT, awsRegion string, bucket string) string {\n\tversioningStatus, err := GetS3BucketVersioningE(t, awsRegion, bucket)\n\trequire.NoError(t, err)\n\n\treturn versioningStatus\n}\n\n// GetS3BucketVersioningE fetches the given bucket's versioning configuration status and returns it as a string\nfunc GetS3BucketVersioningE(t testing.TestingT, awsRegion string, bucket string) (string, error) {\n\ts3Client, err := NewS3ClientE(t, awsRegion)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tres, err := s3Client.GetBucketVersioning(context.Background(), &s3.GetBucketVersioningInput{\n\t\tBucket: &bucket,\n\t})\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn string(res.Status), nil\n}\n\n// GetS3BucketPolicy fetches the given bucket's resource policy and returns it as a string\nfunc GetS3BucketPolicy(t testing.TestingT, awsRegion string, bucket string) string {\n\tbucketPolicy, err := GetS3BucketPolicyE(t, awsRegion, bucket)\n\trequire.NoError(t, err)\n\n\treturn bucketPolicy\n}\n\n// GetS3BucketPolicyE fetches the given bucket's resource policy and returns it as a string\nfunc GetS3BucketPolicyE(t testing.TestingT, awsRegion string, bucket string) (string, error) {\n\ts3Client, err := NewS3ClientE(t, awsRegion)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tres, err := s3Client.GetBucketPolicy(context.Background(), &s3.GetBucketPolicyInput{\n\t\tBucket: &bucket,\n\t})\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn aws.ToString(res.Policy), nil\n}\n\nfunc GetS3BucketOwnershipControls(t testing.TestingT, awsRegion, bucket string) []string {\n\trules, err := GetS3BucketOwnershipControlsE(t, awsRegion, bucket)\n\trequire.NoError(t, err)\n\n\treturn rules\n}\n\nfunc GetS3BucketOwnershipControlsE(t testing.TestingT, awsRegion, bucket string) ([]string, error) {\n\ts3Client, err := NewS3ClientE(t, awsRegion)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tout, err := s3Client.GetBucketOwnershipControls(context.Background(), &s3.GetBucketOwnershipControlsInput{\n\t\tBucket: &bucket,\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\trules := make([]string, 0, len(out.OwnershipControls.Rules))\n\tfor _, rule := range out.OwnershipControls.Rules {\n\t\trules = append(rules, string(rule.ObjectOwnership))\n\t}\n\treturn rules, nil\n}\n\n// AssertS3BucketExists checks if the given S3 bucket exists in the given region and fail the test if it does not.\nfunc AssertS3BucketExists(t testing.TestingT, region string, name string) {\n\terr := AssertS3BucketExistsE(t, region, name)\n\trequire.NoError(t, err)\n}\n\n// AssertS3BucketExistsE checks if the given S3 bucket exists in the given region and return an error if it does not.\nfunc AssertS3BucketExistsE(t testing.TestingT, region string, name string) error {\n\ts3Client, err := NewS3ClientE(t, region)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tparams := &s3.HeadBucketInput{\n\t\tBucket: aws.String(name),\n\t}\n\t_, err = s3Client.HeadBucket(context.Background(), params)\n\treturn err\n}\n\n// AssertS3BucketVersioningExists checks if the given S3 bucket has a versioning configuration enabled and returns an error if it does not.\nfunc AssertS3BucketVersioningExists(t testing.TestingT, region string, bucketName string) {\n\terr := AssertS3BucketVersioningExistsE(t, region, bucketName)\n\trequire.NoError(t, err)\n}\n\n// AssertS3BucketVersioningExistsE checks if the given S3 bucket has a versioning configuration enabled and returns an error if it does not.\nfunc AssertS3BucketVersioningExistsE(t testing.TestingT, region string, bucketName string) error {\n\tstatus, err := GetS3BucketVersioningE(t, region, bucketName)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif status == \"Enabled\" {\n\t\treturn nil\n\t}\n\treturn NewBucketVersioningNotEnabledError(bucketName, region, status)\n}\n\n// AssertS3BucketPolicyExists checks if the given S3 bucket has a resource policy attached and returns an error if it does not\nfunc AssertS3BucketPolicyExists(t testing.TestingT, region string, bucketName string) {\n\terr := AssertS3BucketPolicyExistsE(t, region, bucketName)\n\trequire.NoError(t, err)\n}\n\n// AssertS3BucketPolicyExistsE checks if the given S3 bucket has a resource policy attached and returns an error if it does not\nfunc AssertS3BucketPolicyExistsE(t testing.TestingT, region string, bucketName string) error {\n\tpolicy, err := GetS3BucketPolicyE(t, region, bucketName)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif policy == \"\" {\n\t\treturn NewNoBucketPolicyError(bucketName, region, policy)\n\t}\n\treturn nil\n}\n\n// NewS3Client creates an S3 client.\nfunc NewS3Client(t testing.TestingT, region string) *s3.Client {\n\tclient, err := NewS3ClientE(t, region)\n\trequire.NoError(t, err)\n\n\treturn client\n}\n\n// NewS3ClientE creates an S3 client.\nfunc NewS3ClientE(t testing.TestingT, region string) (*s3.Client, error) {\n\tsess, err := NewAuthenticatedSession(region)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn s3.NewFromConfig(*sess), nil\n}\n\n// NewS3Uploader creates an S3 Uploader.\nfunc NewS3Uploader(t testing.TestingT, region string) *manager.Uploader {\n\tuploader, err := NewS3UploaderE(t, region)\n\trequire.NoError(t, err)\n\treturn uploader\n}\n\n// NewS3UploaderE creates an S3 Uploader.\nfunc NewS3UploaderE(t testing.TestingT, region string) (*manager.Uploader, error) {\n\tsess, err := NewAuthenticatedSession(region)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn manager.NewUploader(s3.NewFromConfig(*sess)), nil\n}\n\n// S3AccessLoggingNotEnabledErr is a custom error that occurs when acess logging hasn't been enabled on the S3 Bucket\ntype S3AccessLoggingNotEnabledErr struct {\n\tOriginBucket string\n\tRegion       string\n}\n\nfunc (err S3AccessLoggingNotEnabledErr) Error() string {\n\treturn fmt.Sprintf(\"Server Access Logging hasn't been enabled for S3 Bucket %s in region %s\", err.OriginBucket, err.Region)\n}\n"
  },
  {
    "path": "modules/aws/s3_test.go",
    "content": "// Integration tests that validate S3-related code in AWS.\npackage aws\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"math/rand\"\n\t\"strconv\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\t\"github.com/aws/aws-sdk-go-v2/service/s3\"\n\t\"github.com/aws/aws-sdk-go-v2/service/s3/types\"\n\t\"github.com/gruntwork-io/terratest/modules/logger\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestCreateAndDestroyS3Bucket(t *testing.T) {\n\tt.Parallel()\n\n\tregion := GetRandomStableRegion(t, nil, nil)\n\tid := random.UniqueId()\n\tlogger.Default.Logf(t, \"Random values selected. Region = %s, Id = %s\\n\", region, id)\n\n\ts3BucketName := \"gruntwork-terratest-\" + strings.ToLower(id)\n\n\tCreateS3Bucket(t, region, s3BucketName)\n\tDeleteS3Bucket(t, region, s3BucketName)\n}\n\nfunc TestAssertS3BucketExistsNoFalseNegative(t *testing.T) {\n\tt.Parallel()\n\n\tregion := GetRandomStableRegion(t, nil, nil)\n\ts3BucketName := \"gruntwork-terratest-\" + strings.ToLower(random.UniqueId())\n\tlogger.Default.Logf(t, \"Random values selected. Region = %s, s3BucketName = %s\\n\", region, s3BucketName)\n\n\tCreateS3Bucket(t, region, s3BucketName)\n\tdefer DeleteS3Bucket(t, region, s3BucketName)\n\n\tAssertS3BucketExists(t, region, s3BucketName)\n}\n\nfunc TestAssertS3BucketExistsNoFalsePositive(t *testing.T) {\n\tt.Parallel()\n\n\tregion := GetRandomStableRegion(t, nil, nil)\n\ts3BucketName := \"gruntwork-terratest-\" + strings.ToLower(random.UniqueId())\n\tlogger.Default.Logf(t, \"Random values selected. Region = %s, s3BucketName = %s\\n\", region, s3BucketName)\n\n\t// We elect not to create the S3 bucket to confirm that our function correctly reports it doesn't exist.\n\t// aws.CreateS3Bucket(region, s3BucketName)\n\n\terr := AssertS3BucketExistsE(t, region, s3BucketName)\n\tif err == nil {\n\t\tt.Fatalf(\"Function claimed that S3 Bucket '%s' exists, but in fact it does not.\", s3BucketName)\n\t}\n}\n\nfunc TestAssertS3BucketVersioningEnabled(t *testing.T) {\n\tt.Parallel()\n\n\tregion := GetRandomStableRegion(t, nil, nil)\n\ts3BucketName := \"gruntwork-terratest-\" + strings.ToLower(random.UniqueId())\n\tlogger.Default.Logf(t, \"Random values selected. Region = %s, s3BucketName = %s\\n\", region, s3BucketName)\n\n\tCreateS3Bucket(t, region, s3BucketName)\n\tdefer DeleteS3Bucket(t, region, s3BucketName)\n\tPutS3BucketVersioning(t, region, s3BucketName)\n\n\tAssertS3BucketVersioningExists(t, region, s3BucketName)\n}\n\nfunc TestEmptyS3Bucket(t *testing.T) {\n\tt.Parallel()\n\n\tregion := GetRandomStableRegion(t, nil, nil)\n\tid := random.UniqueId()\n\tlogger.Default.Logf(t, \"Random values selected. Region = %s, Id = %s\\n\", region, id)\n\n\ts3BucketName := \"gruntwork-terratest-\" + strings.ToLower(id)\n\n\tCreateS3Bucket(t, region, s3BucketName)\n\tdefer DeleteS3Bucket(t, region, s3BucketName)\n\n\ts3Client, err := NewS3ClientE(t, region)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\ttestEmptyBucket(t, s3Client, region, s3BucketName)\n}\n\nfunc TestEmptyS3BucketVersioned(t *testing.T) {\n\tt.Parallel()\n\n\tregion := GetRandomStableRegion(t, nil, nil)\n\n\tid := random.UniqueId()\n\tlogger.Default.Logf(t, \"Random values selected. Region = %s, Id = %s\\n\", region, id)\n\n\ts3BucketName := \"gruntwork-terratest-\" + strings.ToLower(id)\n\n\tCreateS3Bucket(t, region, s3BucketName)\n\tdefer DeleteS3Bucket(t, region, s3BucketName)\n\n\ts3Client, err := NewS3ClientE(t, region)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tversionInput := &s3.PutBucketVersioningInput{\n\t\tBucket: aws.String(s3BucketName),\n\t\tVersioningConfiguration: &types.VersioningConfiguration{\n\t\t\tMFADelete: types.MFADeleteDisabled,\n\t\t\tStatus:    types.BucketVersioningStatusEnabled,\n\t\t},\n\t}\n\n\t_, err = s3Client.PutBucketVersioning(context.Background(), versionInput)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\ttestEmptyBucket(t, s3Client, region, s3BucketName)\n}\n\nfunc TestAssertS3BucketPolicyExists(t *testing.T) {\n\tt.Parallel()\n\n\tregion := GetRandomStableRegion(t, nil, nil)\n\n\tid := random.UniqueId()\n\tlogger.Default.Logf(t, \"Random values selected. Region = %s, Id = %s\\n\", region, id)\n\n\ts3BucketName := \"gruntwork-terratest-\" + strings.ToLower(id)\n\texampleBucketPolicy := fmt.Sprintf(`{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Deny\",\"Principal\":{\"AWS\":[\"*\"]},\"Action\":\"s3:Get*\",\"Resource\":\"arn:aws:s3:::%s/*\",\"Condition\":{\"Bool\":{\"aws:SecureTransport\":\"false\"}}}]}`, s3BucketName)\n\n\tCreateS3Bucket(t, region, s3BucketName)\n\tdefer DeleteS3Bucket(t, region, s3BucketName)\n\tPutS3BucketPolicy(t, region, s3BucketName, exampleBucketPolicy)\n\n\tAssertS3BucketPolicyExists(t, region, s3BucketName)\n\n}\n\nfunc TestGetS3BucketTags(t *testing.T) {\n\tt.Parallel()\n\n\tregion := GetRandomStableRegion(t, nil, nil)\n\tid := random.UniqueId()\n\tlogger.Default.Logf(t, \"Random values selected. Region = %s, Id = %s\\n\", region, id)\n\ts3BucketName := \"gruntwork-terratest-\" + strings.ToLower(id)\n\n\tCreateS3Bucket(t, region, s3BucketName)\n\tdefer DeleteS3Bucket(t, region, s3BucketName)\n\n\ts3Client, err := NewS3ClientE(t, region)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t_, err = s3Client.PutBucketTagging(context.Background(), &s3.PutBucketTaggingInput{\n\t\tBucket: &s3BucketName,\n\t\tTagging: &types.Tagging{\n\t\t\tTagSet: []types.Tag{\n\t\t\t\t{\n\t\t\t\t\tKey:   aws.String(\"Key1\"),\n\t\t\t\t\tValue: aws.String(\"Value1\"),\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tKey:   aws.String(\"Key2\"),\n\t\t\t\t\tValue: aws.String(\"Value2\"),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tactualTags := GetS3BucketTags(t, region, s3BucketName)\n\tassert.True(t, actualTags[\"Key1\"] == \"Value1\")\n\tassert.True(t, actualTags[\"Key2\"] == \"Value2\")\n\tassert.True(t, actualTags[\"NonExistentKey\"] == \"\")\n}\n\nfunc testEmptyBucket(t *testing.T, s3Client *s3.Client, region string, s3BucketName string) {\n\texpectedFileCount := rand.Intn(1000)\n\tlogger.Default.Logf(t, \"Uploading %s files to bucket %s\", strconv.Itoa(expectedFileCount), s3BucketName)\n\n\tdeleted := 0\n\n\t// Upload expectedFileCount files\n\tfor i := 1; i <= expectedFileCount; i++ {\n\t\tkey := fmt.Sprintf(\"test-%s\", strconv.Itoa(i))\n\t\tbody := strings.NewReader(\"This is the body\")\n\n\t\tparams := &s3.PutObjectInput{\n\t\t\tBucket: aws.String(s3BucketName),\n\t\t\tKey:    &key,\n\t\t\tBody:   body,\n\t\t}\n\n\t\tuploader := NewS3Uploader(t, region)\n\n\t\t_, err := uploader.Upload(context.Background(), params)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\t// Delete the first 10 files to be able to test if all files, including delete markers are deleted\n\t\tif i < 10 {\n\t\t\t_, err := s3Client.DeleteObject(context.Background(), &s3.DeleteObjectInput{\n\t\t\t\tBucket: aws.String(s3BucketName),\n\t\t\t\tKey:    aws.String(key),\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tdeleted++\n\t\t}\n\n\t\tif i != 0 && i%100 == 0 {\n\t\t\tlogger.Default.Logf(t, \"Uploaded %s files to bucket %s successfully\", strconv.Itoa(i), s3BucketName)\n\t\t}\n\t}\n\n\tlogger.Default.Logf(t, \"Uploaded %s files to bucket %s successfully\", strconv.Itoa(expectedFileCount), s3BucketName)\n\n\t// verify bucket contains 1 file now\n\tlistObjectsParams := &s3.ListObjectsV2Input{\n\t\tBucket: aws.String(s3BucketName),\n\t}\n\n\tlogger.Default.Logf(t, \"Verifying %s files were uploaded to bucket %s\", strconv.Itoa(expectedFileCount), s3BucketName)\n\tactualCount := 0\n\tfor {\n\t\tbucketObjects, err := s3Client.ListObjectsV2(context.Background(), listObjectsParams)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tpageLength := len((*bucketObjects).Contents)\n\t\tactualCount += pageLength\n\n\t\tif !*bucketObjects.IsTruncated {\n\t\t\tbreak\n\t\t}\n\n\t\tlistObjectsParams.ContinuationToken = bucketObjects.NextContinuationToken\n\t}\n\n\trequire.Equal(t, expectedFileCount-deleted, actualCount)\n\n\t// empty bucket\n\tlogger.Default.Logf(t, \"Emptying bucket %s\", s3BucketName)\n\tEmptyS3Bucket(t, region, s3BucketName)\n\n\t// verify the bucket is empty\n\tbucketObjects, err := s3Client.ListObjectsV2(context.Background(), listObjectsParams)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\trequire.Equal(t, 0, len((*bucketObjects).Contents))\n}\n\nfunc TestGetS3BucketOwnershipControls(t *testing.T) {\n\tt.Parallel()\n\n\tregion := GetRandomStableRegion(t, nil, nil)\n\tid := random.UniqueId()\n\tlogger.Default.Logf(t, \"Random values selected. Region = %s, Id = %s\\n\", region, id)\n\n\ts3BucketName := \"gruntwork-terratest-\" + strings.ToLower(id)\n\tCreateS3Bucket(t, region, s3BucketName)\n\tt.Cleanup(func() {\n\t\tDeleteS3Bucket(t, region, s3BucketName)\n\t})\n\n\tt.Run(\"Exist\", func(t *testing.T) {\n\t\ts3Client, err := NewS3ClientE(t, region)\n\t\trequire.NoError(t, err)\n\t\t_, err = s3Client.PutBucketOwnershipControls(context.Background(), &s3.PutBucketOwnershipControlsInput{\n\t\t\tBucket: &s3BucketName,\n\t\t\tOwnershipControls: &types.OwnershipControls{\n\t\t\t\tRules: []types.OwnershipControlsRule{\n\t\t\t\t\t{\n\t\t\t\t\t\tObjectOwnership: types.ObjectOwnershipBucketOwnerEnforced,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t})\n\t\trequire.NoError(t, err)\n\t\tt.Cleanup(func() {\n\t\t\t_, err := s3Client.DeleteBucketOwnershipControls(context.Background(), &s3.DeleteBucketOwnershipControlsInput{\n\t\t\t\tBucket: &s3BucketName,\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\t\t})\n\n\t\tcontrols := GetS3BucketOwnershipControls(t, region, s3BucketName)\n\t\tassert.Equal(t, 1, len(controls))\n\t\tassert.Equal(t, string(types.ObjectOwnershipBucketOwnerEnforced), controls[0])\n\t})\n\n\tt.Run(\"NotExist\", func(t *testing.T) {\n\t\t_, err := GetS3BucketOwnershipControlsE(t, region, s3BucketName)\n\t\tassert.Error(t, err)\n\t})\n}\n\nfunc TestS3ObjectContents(t *testing.T) {\n\tt.Parallel()\n\n\tregion := GetRandomStableRegion(t, nil, nil)\n\tid := random.UniqueId()\n\tlogger.Default.Logf(t, \"Random values selected. Region = %s, Id = %s\\n\", region, id)\n\ts3BucketName := \"gruntwork-terratest-\" + strings.ToLower(id)\n\n\tCreateS3Bucket(t, region, s3BucketName)\n\tdefer DeleteS3Bucket(t, region, s3BucketName)\n\tdefer EmptyS3BucketE(t, region, s3BucketName)\n\n\tkey := fmt.Sprintf(\"content-%s\", id)\n\tbody := make([]byte, 1024)\n\trand.Read(body)\n\n\tPutS3ObjectContentsE(t, region, s3BucketName, key, bytes.NewReader(body))\n\tstoredBody := GetS3ObjectContents(t, region, s3BucketName, key)\n\n\tassert.Equal(t, body, []byte(storedBody))\n}\n"
  },
  {
    "path": "modules/aws/secretsmanager.go",
    "content": "package aws\n\nimport (\n\t\"context\"\n\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\t\"github.com/aws/aws-sdk-go-v2/service/secretsmanager\"\n\t\"github.com/gruntwork-io/terratest/modules/logger\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// CreateSecretStringWithDefaultKey creates a new secret in Secrets Manager using the default \"aws/secretsmanager\" KMS key and returns the secret ARN\nfunc CreateSecretStringWithDefaultKey(t testing.TestingT, awsRegion, description, name, secretString string) string {\n\tarn, err := CreateSecretStringWithDefaultKeyE(t, awsRegion, description, name, secretString)\n\trequire.NoError(t, err)\n\treturn arn\n}\n\n// CreateSecretStringWithDefaultKeyE creates a new secret in Secrets Manager using the default \"aws/secretsmanager\" KMS key and returns the secret ARN\nfunc CreateSecretStringWithDefaultKeyE(t testing.TestingT, awsRegion, description, name, secretString string) (string, error) {\n\tlogger.Default.Logf(t, \"Creating new secret in secrets manager named %s\", name)\n\n\tclient := NewSecretsManagerClient(t, awsRegion)\n\n\tsecret, err := client.CreateSecret(context.Background(), &secretsmanager.CreateSecretInput{\n\t\tDescription:  aws.String(description),\n\t\tName:         aws.String(name),\n\t\tSecretString: aws.String(secretString),\n\t})\n\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn aws.ToString(secret.ARN), nil\n}\n\n// GetSecretValue takes the friendly name or ARN of a secret and returns the plaintext value\nfunc GetSecretValue(t testing.TestingT, awsRegion, id string) string {\n\tsecret, err := GetSecretValueE(t, awsRegion, id)\n\trequire.NoError(t, err)\n\treturn secret\n}\n\n// GetSecretValueE takes the friendly name or ARN of a secret and returns the plaintext value\nfunc GetSecretValueE(t testing.TestingT, awsRegion, id string) (string, error) {\n\tlogger.Default.Logf(t, \"Getting value of secret with ID %s\", id)\n\n\tclient := NewSecretsManagerClient(t, awsRegion)\n\n\tsecret, err := client.GetSecretValue(context.Background(), &secretsmanager.GetSecretValueInput{\n\t\tSecretId: aws.String(id),\n\t})\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn aws.ToString(secret.SecretString), nil\n}\n\n// PutSecretString updates a secret in Secrets Manager to a new string value\nfunc PutSecretString(t testing.TestingT, awsRegion, id string, secretString string) {\n\terr := PutSecretStringE(t, awsRegion, id, secretString)\n\trequire.NoError(t, err)\n}\n\n// PutSecretStringE updates a secret in Secrets Manager to a new string value\nfunc PutSecretStringE(t testing.TestingT, awsRegion, id string, secretString string) error {\n\tlogger.Default.Logf(t, \"Updating secret with ID %s\", id)\n\n\tclient := NewSecretsManagerClient(t, awsRegion)\n\n\t_, err := client.PutSecretValue(context.Background(), &secretsmanager.PutSecretValueInput{\n\t\tSecretId:     aws.String(id),\n\t\tSecretString: aws.String(secretString),\n\t})\n\n\treturn err\n}\n\n// DeleteSecret deletes a secret. If forceDelete is true, the secret will be deleted after a short delay. If forceDelete is false, the secret will be deleted after a 30-day recovery window.\nfunc DeleteSecret(t testing.TestingT, awsRegion, id string, forceDelete bool) {\n\terr := DeleteSecretE(t, awsRegion, id, forceDelete)\n\trequire.NoError(t, err)\n}\n\n// DeleteSecretE deletes a secret. If forceDelete is true, the secret will be deleted after a short delay. If forceDelete is false, the secret will be deleted after a 30-day recovery window.\nfunc DeleteSecretE(t testing.TestingT, awsRegion, id string, forceDelete bool) error {\n\tlogger.Default.Logf(t, \"Deleting secret with ID %s\", id)\n\n\tclient := NewSecretsManagerClient(t, awsRegion)\n\n\t_, err := client.DeleteSecret(context.Background(), &secretsmanager.DeleteSecretInput{\n\t\tForceDeleteWithoutRecovery: aws.Bool(forceDelete),\n\t\tSecretId:                   aws.String(id),\n\t})\n\n\treturn err\n}\n\n// NewSecretsManagerClient creates a new SecretsManager client.\nfunc NewSecretsManagerClient(t testing.TestingT, region string) *secretsmanager.Client {\n\tclient, err := NewSecretsManagerClientE(t, region)\n\trequire.NoError(t, err)\n\treturn client\n}\n\n// NewSecretsManagerClientE creates a new SecretsManager client.\nfunc NewSecretsManagerClientE(t testing.TestingT, region string) (*secretsmanager.Client, error) {\n\tsess, err := NewAuthenticatedSession(region)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn secretsmanager.NewFromConfig(*sess), nil\n}\n"
  },
  {
    "path": "modules/aws/secretsmanager_test.go",
    "content": "package aws\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestSecretsManagerMethods(t *testing.T) {\n\tt.Parallel()\n\n\tregion := GetRandomStableRegion(t, nil, nil)\n\tname := random.UniqueId()\n\tdescription := \"This is just a secrets manager test description.\"\n\tsecretOriginalValue := \"This is the secret value.\"\n\tsecretUpdatedValue := \"This is the NEW secret value.\"\n\n\tsecretARN := CreateSecretStringWithDefaultKey(t, region, description, name, secretOriginalValue)\n\tdefer deleteSecret(t, region, secretARN)\n\n\tstoredValue := GetSecretValue(t, region, secretARN)\n\tassert.Equal(t, secretOriginalValue, storedValue)\n\n\tPutSecretString(t, region, secretARN, secretUpdatedValue)\n\n\tstoredValueAfterUpdate := GetSecretValue(t, region, secretARN)\n\tassert.Equal(t, secretUpdatedValue, storedValueAfterUpdate)\n}\n\nfunc deleteSecret(t *testing.T, region, id string) {\n\tDeleteSecret(t, region, id, true)\n\n\t_, err := GetSecretValueE(t, region, id)\n\trequire.Error(t, err)\n}\n"
  },
  {
    "path": "modules/aws/sns.go",
    "content": "package aws\n\nimport (\n\t\"context\"\n\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\t\"github.com/aws/aws-sdk-go-v2/service/sns\"\n\t\"github.com/gruntwork-io/terratest/modules/logger\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n)\n\n// CreateSnsTopic creates an SNS Topic and return the ARN.\nfunc CreateSnsTopic(t testing.TestingT, region string, snsTopicName string) string {\n\tout, err := CreateSnsTopicE(t, region, snsTopicName)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn out\n}\n\n// CreateSnsTopicE creates an SNS Topic and return the ARN.\nfunc CreateSnsTopicE(t testing.TestingT, region string, snsTopicName string) (string, error) {\n\tlogger.Default.Logf(t, \"Creating SNS topic %s in %s\", snsTopicName, region)\n\n\tsnsClient, err := NewSnsClientE(t, region)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tcreateTopicInput := &sns.CreateTopicInput{\n\t\tName: &snsTopicName,\n\t}\n\n\toutput, err := snsClient.CreateTopic(context.Background(), createTopicInput)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn aws.ToString(output.TopicArn), err\n}\n\n// DeleteSNSTopic deletes an SNS Topic.\nfunc DeleteSNSTopic(t testing.TestingT, region string, snsTopicArn string) {\n\terr := DeleteSNSTopicE(t, region, snsTopicArn)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n\n// DeleteSNSTopicE deletes an SNS Topic.\nfunc DeleteSNSTopicE(t testing.TestingT, region string, snsTopicArn string) error {\n\tlogger.Default.Logf(t, \"Deleting SNS topic %s in %s\", snsTopicArn, region)\n\n\tsnsClient, err := NewSnsClientE(t, region)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdeleteTopicInput := &sns.DeleteTopicInput{\n\t\tTopicArn: aws.String(snsTopicArn),\n\t}\n\n\t_, err = snsClient.DeleteTopic(context.Background(), deleteTopicInput)\n\treturn err\n}\n\n// NewSnsClient creates a new SNS client.\nfunc NewSnsClient(t testing.TestingT, region string) *sns.Client {\n\tclient, err := NewSnsClientE(t, region)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn client\n}\n\n// NewSnsClientE creates a new SNS client.\nfunc NewSnsClientE(t testing.TestingT, region string) (*sns.Client, error) {\n\tsess, err := NewAuthenticatedSession(region)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn sns.NewFromConfig(*sess), nil\n}\n"
  },
  {
    "path": "modules/aws/sns_test.go",
    "content": "package aws\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\t\"github.com/aws/aws-sdk-go-v2/service/sns\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestCreateAndDeleteSnsTopic(t *testing.T) {\n\tt.Parallel()\n\n\tregion := GetRandomStableRegion(t, nil, nil)\n\tuniqueID := random.UniqueId()\n\tname := fmt.Sprintf(\"test-sns-topic-%s\", uniqueID)\n\n\tarn := CreateSnsTopic(t, region, name)\n\tdefer deleteTopic(t, region, arn)\n\n\tassert.True(t, snsTopicExists(t, region, arn))\n}\n\nfunc snsTopicExists(t *testing.T, region string, arn string) bool {\n\tsnsClient := NewSnsClient(t, region)\n\n\tinput := sns.GetTopicAttributesInput{TopicArn: aws.String(arn)}\n\n\tif _, err := snsClient.GetTopicAttributes(context.Background(), &input); err != nil {\n\t\tif strings.Contains(err.Error(), \"NotFound\") {\n\t\t\treturn false\n\t\t}\n\t\tt.Fatal(err)\n\t}\n\n\treturn true\n}\n\nfunc deleteTopic(t *testing.T, region string, arn string) {\n\tDeleteSNSTopic(t, region, arn)\n\tassert.False(t, snsTopicExists(t, region, arn))\n}\n"
  },
  {
    "path": "modules/aws/sqs.go",
    "content": "package aws\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\t\"github.com/aws/aws-sdk-go-v2/service/sqs\"\n\t\"github.com/aws/aws-sdk-go-v2/service/sqs/types\"\n\t\"github.com/google/uuid\"\n\t\"github.com/gruntwork-io/terratest/modules/logger\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n)\n\n// CreateRandomQueue creates a new SQS queue with a random name that starts with the given prefix and return the queue URL.\nfunc CreateRandomQueue(t testing.TestingT, awsRegion string, prefix string) string {\n\turl, err := CreateRandomQueueE(t, awsRegion, prefix)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn url\n}\n\n// CreateRandomQueueE creates a new SQS queue with a random name that starts with the given prefix and return the queue URL.\nfunc CreateRandomQueueE(t testing.TestingT, awsRegion string, prefix string) (string, error) {\n\tlogger.Default.Logf(t, \"Creating randomly named SQS queue with prefix %s\", prefix)\n\n\tsqsClient, err := NewSqsClientE(t, awsRegion)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tchannel, err := uuid.NewUUID()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tchannelName := fmt.Sprintf(\"%s-%s\", prefix, channel.String())\n\n\tqueue, err := sqsClient.CreateQueue(context.Background(), &sqs.CreateQueueInput{\n\t\tQueueName: aws.String(channelName),\n\t})\n\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn aws.ToString(queue.QueueUrl), nil\n}\n\n// CreateRandomFifoQueue creates a new FIFO SQS queue with a random name that starts with the given prefix and return the queue URL.\nfunc CreateRandomFifoQueue(t testing.TestingT, awsRegion string, prefix string) string {\n\turl, err := CreateRandomFifoQueueE(t, awsRegion, prefix)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn url\n}\n\n// CreateRandomFifoQueueE creates a new FIFO SQS queue with a random name that starts with the given prefix and return the queue URL.\nfunc CreateRandomFifoQueueE(t testing.TestingT, awsRegion string, prefix string) (string, error) {\n\tlogger.Default.Logf(t, \"Creating randomly named FIFO SQS queue with prefix %s\", prefix)\n\n\tsqsClient, err := NewSqsClientE(t, awsRegion)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tchannel, err := uuid.NewUUID()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tchannelName := fmt.Sprintf(\"%s-%s.fifo\", prefix, channel.String())\n\n\tqueue, err := sqsClient.CreateQueue(context.Background(), &sqs.CreateQueueInput{\n\t\tQueueName: aws.String(channelName),\n\t\tAttributes: map[string]string{\n\t\t\t\"ContentBasedDeduplication\": \"true\",\n\t\t\t\"FifoQueue\":                 \"true\",\n\t\t},\n\t})\n\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn aws.ToString(queue.QueueUrl), nil\n}\n\n// DeleteQueue deletes the SQS queue with the given URL.\nfunc DeleteQueue(t testing.TestingT, awsRegion string, queueURL string) {\n\terr := DeleteQueueE(t, awsRegion, queueURL)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n\n// DeleteQueueE deletes the SQS queue with the given URL.\nfunc DeleteQueueE(t testing.TestingT, awsRegion string, queueURL string) error {\n\tlogger.Default.Logf(t, \"Deleting SQS Queue %s\", queueURL)\n\n\tsqsClient, err := NewSqsClientE(t, awsRegion)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t_, err = sqsClient.DeleteQueue(context.Background(), &sqs.DeleteQueueInput{\n\t\tQueueUrl: aws.String(queueURL),\n\t})\n\n\treturn err\n}\n\n// DeleteMessageFromQueue deletes the message with the given receipt from the SQS queue with the given URL.\nfunc DeleteMessageFromQueue(t testing.TestingT, awsRegion string, queueURL string, receipt string) {\n\terr := DeleteMessageFromQueueE(t, awsRegion, queueURL, receipt)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n\n// DeleteMessageFromQueueE deletes the message with the given receipt from the SQS queue with the given URL.\nfunc DeleteMessageFromQueueE(t testing.TestingT, awsRegion string, queueURL string, receipt string) error {\n\tlogger.Default.Logf(t, \"Deleting message from queue %s (%s)\", queueURL, receipt)\n\n\tsqsClient, err := NewSqsClientE(t, awsRegion)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t_, err = sqsClient.DeleteMessage(context.Background(), &sqs.DeleteMessageInput{\n\t\tReceiptHandle: &receipt,\n\t\tQueueUrl:      &queueURL,\n\t})\n\n\treturn err\n}\n\n// SendMessageToQueue sends the given message to the SQS queue with the given URL.\nfunc SendMessageToQueue(t testing.TestingT, awsRegion string, queueURL string, message string) {\n\terr := SendMessageToQueueE(t, awsRegion, queueURL, message)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n\n// SendMessageToQueueE sends the given message to the SQS queue with the given URL.\nfunc SendMessageToQueueE(t testing.TestingT, awsRegion string, queueURL string, message string) error {\n\tlogger.Default.Logf(t, \"Sending message %s to queue %s\", message, queueURL)\n\n\tsqsClient, err := NewSqsClientE(t, awsRegion)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tres, err := sqsClient.SendMessage(context.Background(), &sqs.SendMessageInput{\n\t\tMessageBody: &message,\n\t\tQueueUrl:    &queueURL,\n\t})\n\n\tif err != nil {\n\t\tif strings.Contains(err.Error(), \"AWS.SimpleQueueService.NonExistentQueue\") {\n\t\t\tlogger.Default.Logf(t, \"WARN: Client has stopped listening on queue %s\", queueURL)\n\t\t\treturn nil\n\t\t}\n\t\treturn err\n\t}\n\n\tlogger.Default.Logf(t, \"Message id %s sent to queue %s\", aws.ToString(res.MessageId), queueURL)\n\n\treturn nil\n}\n\n// SendMessageFifoToQueue sends the given message to the FIFO SQS queue with the given URL.\nfunc SendMessageFifoToQueue(t testing.TestingT, awsRegion string, queueURL string, message string, messageGroupID string) {\n\terr := SendMessageToFifoQueueE(t, awsRegion, queueURL, message, messageGroupID)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n\n// SendMessageToFifoQueueE sends the given message to the FIFO SQS queue with the given URL.\nfunc SendMessageToFifoQueueE(t testing.TestingT, awsRegion string, queueURL string, message string, messageGroupID string) error {\n\tlogger.Default.Logf(t, \"Sending message %s to queue %s\", message, queueURL)\n\n\tsqsClient, err := NewSqsClientE(t, awsRegion)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tres, err := sqsClient.SendMessage(context.Background(), &sqs.SendMessageInput{\n\t\tMessageBody:    &message,\n\t\tQueueUrl:       &queueURL,\n\t\tMessageGroupId: &messageGroupID,\n\t})\n\n\tif err != nil {\n\t\tif strings.Contains(err.Error(), \"AWS.SimpleQueueService.NonExistentQueue\") {\n\t\t\tlogger.Default.Logf(t, \"WARN: Client has stopped listening on queue %s\", queueURL)\n\t\t\treturn nil\n\t\t}\n\t\treturn err\n\t}\n\n\tlogger.Default.Logf(t, \"Message id %s sent to queue %s\", aws.ToString(res.MessageId), queueURL)\n\n\treturn nil\n}\n\n// QueueMessageResponse contains a queue message.\ntype QueueMessageResponse struct {\n\tReceiptHandle string\n\tMessageBody   string\n\tError         error\n}\n\n// WaitForQueueMessage waits to receive a message from on the queueURL. Since the API only allows us to wait a max 20 seconds for a new\n// message to arrive, we must loop TIMEOUT/20 number of times to be able to wait for a total of TIMEOUT seconds\nfunc WaitForQueueMessage(t testing.TestingT, awsRegion string, queueURL string, timeout int) QueueMessageResponse {\n\tsqsClient, err := NewSqsClientE(t, awsRegion)\n\tif err != nil {\n\t\treturn QueueMessageResponse{Error: err}\n\t}\n\n\tcycles := timeout\n\tcycleLength := 1\n\tif timeout >= 20 {\n\t\tcycleLength = 20\n\t\tcycles = timeout / cycleLength\n\t}\n\n\tfor i := 0; i < cycles; i++ {\n\t\tlogger.Default.Logf(t, \"Waiting for message on %s (%ss)\", queueURL, strconv.Itoa(i*cycleLength))\n\t\tresult, err := sqsClient.ReceiveMessage(context.Background(), &sqs.ReceiveMessageInput{\n\t\t\tQueueUrl:                    aws.String(queueURL),\n\t\t\tMessageSystemAttributeNames: []types.MessageSystemAttributeName{types.MessageSystemAttributeNameSentTimestamp},\n\t\t\tMaxNumberOfMessages:         int32(1),\n\t\t\tMessageAttributeNames:       []string{\"All\"},\n\t\t\tWaitTimeSeconds:             int32(cycleLength),\n\t\t})\n\n\t\tif err != nil {\n\t\t\treturn QueueMessageResponse{Error: err}\n\t\t}\n\n\t\tif len(result.Messages) > 0 {\n\t\t\tlogger.Default.Logf(t, \"Message %s received on %s\", *result.Messages[0].MessageId, queueURL)\n\t\t\treturn QueueMessageResponse{ReceiptHandle: *result.Messages[0].ReceiptHandle, MessageBody: *result.Messages[0].Body}\n\t\t}\n\t}\n\n\treturn QueueMessageResponse{Error: ReceiveMessageTimeout{QueueUrl: queueURL, TimeoutSec: timeout}}\n}\n\n// NewSqsClient creates a new SQS client.\nfunc NewSqsClient(t testing.TestingT, region string) *sqs.Client {\n\tclient, err := NewSqsClientE(t, region)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn client\n}\n\n// NewSqsClientE creates a new SQS client.\nfunc NewSqsClientE(t testing.TestingT, region string) (*sqs.Client, error) {\n\tsess, err := NewAuthenticatedSession(region)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn sqs.NewFromConfig(*sess), nil\n}\n\n// ReceiveMessageTimeout is an error that occurs if receiving a message times out.\ntype ReceiveMessageTimeout struct {\n\tQueueUrl   string\n\tTimeoutSec int\n}\n\nfunc (err ReceiveMessageTimeout) Error() string {\n\treturn fmt.Sprintf(\"Failed to receive messages on %s within %s seconds\", err.QueueUrl, strconv.Itoa(err.TimeoutSec))\n}\n"
  },
  {
    "path": "modules/aws/sqs_test.go",
    "content": "package aws\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\t\"github.com/aws/aws-sdk-go-v2/service/sqs\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestSqsQueueMethods(t *testing.T) {\n\tt.Parallel()\n\n\tregion := GetRandomStableRegion(t, nil, nil)\n\tuniqueID := random.UniqueId()\n\tnamePrefix := fmt.Sprintf(\"sqs-queue-test-%s\", uniqueID)\n\n\turl := CreateRandomQueue(t, region, namePrefix)\n\tdefer deleteQueue(t, region, url)\n\n\tassert.True(t, queueExists(t, region, url))\n\n\tmessage := fmt.Sprintf(\"test-message-%s\", uniqueID)\n\ttimeoutSec := 20\n\n\tSendMessageToQueue(t, region, url, message)\n\n\tfirstResponse := WaitForQueueMessage(t, region, url, timeoutSec)\n\tassert.NoError(t, firstResponse.Error)\n\tassert.Equal(t, message, firstResponse.MessageBody)\n\n\tDeleteMessageFromQueue(t, region, url, firstResponse.ReceiptHandle)\n\n\tsecondResponse := WaitForQueueMessage(t, region, url, timeoutSec)\n\tassert.Error(t, secondResponse.Error, ReceiveMessageTimeout{QueueUrl: url, TimeoutSec: timeoutSec})\n}\n\nfunc TestFifoSqsQueueMethods(t *testing.T) {\n\tt.Parallel()\n\n\tregion := GetRandomStableRegion(t, nil, nil)\n\tuniqueID := random.UniqueId()\n\tnamePrefix := fmt.Sprintf(\"sqs-queue-test-%s\", uniqueID)\n\tfifoMessageGroupID := \"g1\"\n\n\turl := CreateRandomFifoQueue(t, region, namePrefix)\n\tdefer deleteQueue(t, region, url)\n\n\tassert.True(t, queueExists(t, region, url))\n\n\tmessage := fmt.Sprintf(\"test-message-%s\", uniqueID)\n\ttimeoutSec := 20\n\n\tSendMessageFifoToQueue(t, region, url, message, fifoMessageGroupID)\n\n\tfirstResponse := WaitForQueueMessage(t, region, url, timeoutSec)\n\tassert.NoError(t, firstResponse.Error)\n\tassert.Equal(t, message, firstResponse.MessageBody)\n\n\tDeleteMessageFromQueue(t, region, url, firstResponse.ReceiptHandle)\n\n\tsecondResponse := WaitForQueueMessage(t, region, url, timeoutSec)\n\tassert.Error(t, secondResponse.Error, ReceiveMessageTimeout{QueueUrl: url, TimeoutSec: timeoutSec})\n}\n\nfunc queueExists(t *testing.T, region string, url string) bool {\n\tsqsClient := NewSqsClient(t, region)\n\n\tinput := sqs.GetQueueAttributesInput{QueueUrl: aws.String(url)}\n\n\tif _, err := sqsClient.GetQueueAttributes(context.Background(), &input); err != nil {\n\t\tif strings.Contains(err.Error(), \"NonExistentQueue\") {\n\t\t\treturn false\n\t\t}\n\t\tt.Fatal(err)\n\t}\n\n\treturn true\n}\n\nfunc deleteQueue(t *testing.T, region string, url string) {\n\tDeleteQueue(t, region, url)\n\tassert.False(t, queueExists(t, region, url))\n}\n"
  },
  {
    "path": "modules/aws/ssm.go",
    "content": "package aws\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\t\"github.com/aws/aws-sdk-go-v2/service/ssm\"\n\t\"github.com/aws/aws-sdk-go-v2/service/ssm/types\"\n\t\"github.com/gruntwork-io/terratest/modules/logger\"\n\t\"github.com/gruntwork-io/terratest/modules/retry\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// GetParameter retrieves the latest version of SSM Parameter at keyName with decryption.\nfunc GetParameter(t testing.TestingT, awsRegion string, keyName string) string {\n\tkeyValue, err := GetParameterE(t, awsRegion, keyName)\n\trequire.NoError(t, err)\n\treturn keyValue\n}\n\n// GetParameterE retrieves the latest version of SSM Parameter at keyName with decryption.\nfunc GetParameterE(t testing.TestingT, awsRegion string, keyName string) (string, error) {\n\tssmClient, err := NewSsmClientE(t, awsRegion)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn GetParameterWithClientE(t, ssmClient, keyName)\n}\n\n// GetParameterWithClientE retrieves the latest version of SSM Parameter at keyName with decryption with the ability to provide the SSM client.\nfunc GetParameterWithClientE(t testing.TestingT, client *ssm.Client, keyName string) (string, error) {\n\tresp, err := client.GetParameter(context.Background(), &ssm.GetParameterInput{Name: aws.String(keyName), WithDecryption: aws.Bool(true)})\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tparameter := *resp.Parameter\n\treturn *parameter.Value, nil\n}\n\n// PutParameter creates new version of SSM Parameter at keyName with keyValue as SecureString.\nfunc PutParameter(t testing.TestingT, awsRegion string, keyName string, keyDescription string, keyValue string) int64 {\n\tversion, err := PutParameterE(t, awsRegion, keyName, keyDescription, keyValue)\n\trequire.NoError(t, err)\n\treturn version\n}\n\n// PutParameterE creates new version of SSM Parameter at keyName with keyValue as SecureString.\nfunc PutParameterE(t testing.TestingT, awsRegion string, keyName string, keyDescription string, keyValue string) (int64, error) {\n\tssmClient, err := NewSsmClientE(t, awsRegion)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\treturn PutParameterWithClientE(t, ssmClient, keyName, keyDescription, keyValue)\n}\n\n// PutParameterWithClientE creates new version of SSM Parameter at keyName with keyValue as SecureString with the ability to provide the SSM client.\nfunc PutParameterWithClientE(t testing.TestingT, client *ssm.Client, keyName string, keyDescription string, keyValue string) (int64, error) {\n\tresp, err := client.PutParameter(context.Background(), &ssm.PutParameterInput{\n\t\tName:        aws.String(keyName),\n\t\tDescription: aws.String(keyDescription),\n\t\tValue:       aws.String(keyValue),\n\t\tType:        types.ParameterTypeSecureString,\n\t})\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\treturn resp.Version, nil\n}\n\n// DeleteParameter deletes all versions of SSM Parameter at keyName.\nfunc DeleteParameter(t testing.TestingT, awsRegion string, keyName string) {\n\terr := DeleteParameterE(t, awsRegion, keyName)\n\trequire.NoError(t, err)\n}\n\n// DeleteParameterE deletes all versions of SSM Parameter at keyName.\nfunc DeleteParameterE(t testing.TestingT, awsRegion string, keyName string) error {\n\tssmClient, err := NewSsmClientE(t, awsRegion)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn DeleteParameterWithClientE(t, ssmClient, keyName)\n}\n\n// DeleteParameterWithClientE deletes all versions of SSM Parameter at keyName with the ability to provide the SSM client.\nfunc DeleteParameterWithClientE(t testing.TestingT, client *ssm.Client, keyName string) error {\n\t_, err := client.DeleteParameter(context.Background(), &ssm.DeleteParameterInput{Name: aws.String(keyName)})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// NewSsmClient creates an SSM client.\nfunc NewSsmClient(t testing.TestingT, region string) *ssm.Client {\n\tclient, err := NewSsmClientE(t, region)\n\trequire.NoError(t, err)\n\treturn client\n}\n\n// NewSsmClientE creates an SSM client.\nfunc NewSsmClientE(t testing.TestingT, region string) (*ssm.Client, error) {\n\tsess, err := NewAuthenticatedSession(region)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn ssm.NewFromConfig(*sess), nil\n}\n\n// WaitForSsmInstanceE waits until the instance get registered to the SSM inventory.\nfunc WaitForSsmInstanceE(t testing.TestingT, awsRegion, instanceID string, timeout time.Duration) error {\n\tclient, err := NewSsmClientE(t, awsRegion)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn WaitForSsmInstanceWithClientE(t, client, instanceID, timeout)\n}\n\n// WaitForSsmInstanceWithClientE waits until the instance get registered to the SSM inventory with the ability to provide the SSM client.\nfunc WaitForSsmInstanceWithClientE(t testing.TestingT, client *ssm.Client, instanceID string, timeout time.Duration) error {\n\ttimeBetweenRetries := 2 * time.Second\n\tmaxRetries := int(timeout.Seconds() / timeBetweenRetries.Seconds())\n\tdescription := fmt.Sprintf(\"Waiting for %s to appear in the SSM inventory\", instanceID)\n\n\tinput := &ssm.GetInventoryInput{\n\t\tFilters: []types.InventoryFilter{\n\t\t\t{\n\t\t\t\tKey:    aws.String(\"AWS:InstanceInformation.InstanceId\"),\n\t\t\t\tType:   types.InventoryQueryOperatorTypeEqual,\n\t\t\t\tValues: []string{instanceID},\n\t\t\t},\n\t\t},\n\t}\n\t_, err := retry.DoWithRetryE(t, description, maxRetries, timeBetweenRetries, func() (string, error) {\n\t\tresp, err := client.GetInventory(context.Background(), input)\n\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\tif len(resp.Entities) != 1 {\n\t\t\treturn \"\", fmt.Errorf(\"%s is not in the SSM inventory\", instanceID)\n\t\t}\n\n\t\treturn \"\", nil\n\t})\n\n\treturn err\n}\n\n// WaitForSsmInstance waits until the instance get registered to the SSM inventory.\nfunc WaitForSsmInstance(t testing.TestingT, awsRegion, instanceID string, timeout time.Duration) {\n\terr := WaitForSsmInstanceE(t, awsRegion, instanceID, timeout)\n\trequire.NoError(t, err)\n}\n\n// CheckSsmCommand checks that you can run the given command on the given instance through AWS SSM.\nfunc CheckSsmCommand(t testing.TestingT, awsRegion, instanceID, command string, timeout time.Duration) *CommandOutput {\n\treturn CheckSsmCommandWithDocument(t, awsRegion, instanceID, command, \"AWS-RunShellScript\", timeout)\n}\n\n// CommandOutput contains the result of the SSM command.\ntype CommandOutput struct {\n\tStdout   string\n\tStderr   string\n\tExitCode int64\n}\n\n// CheckSsmCommandE checks that you can run the given command on the given instance through AWS SSM. Returns the result and an error if one occurs.\nfunc CheckSsmCommandE(t testing.TestingT, awsRegion, instanceID, command string, timeout time.Duration) (*CommandOutput, error) {\n\treturn CheckSsmCommandWithDocumentE(t, awsRegion, instanceID, command, \"AWS-RunShellScript\", timeout)\n}\n\n// CheckSSMCommandWithClientE checks that you can run the given command on the given instance through AWS SSM with the ability to provide the SSM client. Returns the result and an error if one occurs.\nfunc CheckSSMCommandWithClientE(t testing.TestingT, client *ssm.Client, instanceID, command string, timeout time.Duration) (*CommandOutput, error) {\n\treturn CheckSSMCommandWithClientWithDocumentE(t, client, instanceID, command, \"AWS-RunShellScript\", timeout)\n}\n\n// CheckSsmCommandWithDocument checks that you can run the given command on the given instance through AWS SSM with specified Command Doc type.\nfunc CheckSsmCommandWithDocument(t testing.TestingT, awsRegion, instanceID, command string, commandDocName string, timeout time.Duration) *CommandOutput {\n\tresult, err := CheckSsmCommandWithDocumentE(t, awsRegion, instanceID, command, commandDocName, timeout)\n\trequire.NoErrorf(t, err, \"failed to execute '%s' on %s (%v):]\\n  stdout: %#v\\n  stderr: %#v\", command, instanceID, err, result.Stdout, result.Stderr)\n\treturn result\n}\n\n// CheckSsmCommandWithDocumentE checks that you can run the given command on the given instance through AWS SSM with specified Command Doc type. Returns the result and an error if one occurs.\nfunc CheckSsmCommandWithDocumentE(t testing.TestingT, awsRegion, instanceID, command string, commandDocName string, timeout time.Duration) (*CommandOutput, error) {\n\tlogger.Default.Logf(t, \"Running command '%s' on EC2 instance with ID '%s'\", command, instanceID)\n\n\t// Now that we know the instance in the SSM inventory, we can send the command\n\tclient, err := NewSsmClientE(t, awsRegion)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn CheckSSMCommandWithClientWithDocumentE(t, client, instanceID, command, commandDocName, timeout)\n}\n\n// CheckSSMCommandWithClientWithDocumentE checks that you can run the given command on the given instance through AWS SSM with the ability to provide the SSM client with specified Command Doc type. Returns the result and an error if one occurs.\nfunc CheckSSMCommandWithClientWithDocumentE(t testing.TestingT, client *ssm.Client, instanceID, command string, commandDocName string, timeout time.Duration) (*CommandOutput, error) {\n\n\ttimeBetweenRetries := 2 * time.Second\n\tmaxRetries := int(timeout.Seconds() / timeBetweenRetries.Seconds())\n\n\tresp, err := client.SendCommand(\n\t\tcontext.Background(),\n\t\t&ssm.SendCommandInput{\n\t\t\tComment:      aws.String(\"Terratest SSM\"),\n\t\t\tDocumentName: aws.String(commandDocName),\n\t\t\tInstanceIds:  []string{instanceID},\n\t\t\tParameters: map[string][]string{\n\t\t\t\t\"commands\": {command},\n\t\t\t},\n\t\t},\n\t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Wait for the result\n\tdescription := \"Waiting for the result of the command\"\n\tretryableErrors := map[string]string{\n\t\t\"InvocationDoesNotExist\": \"InvocationDoesNotExist\",\n\t\t\"bad status: Pending\":    \"bad status: Pending\",\n\t\t\"bad status: InProgress\": \"bad status: InProgress\",\n\t\t\"bad status: Delayed\":    \"bad status: Delayed\",\n\t}\n\n\tresult := &CommandOutput{}\n\t_, err = retry.DoWithRetryableErrorsE(t, description, retryableErrors, maxRetries, timeBetweenRetries, func() (string, error) {\n\t\tresp, err := client.GetCommandInvocation(context.Background(), &ssm.GetCommandInvocationInput{\n\t\t\tCommandId:  resp.Command.CommandId,\n\t\t\tInstanceId: &instanceID,\n\t\t})\n\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\tresult.Stderr = aws.ToString(resp.StandardErrorContent)\n\t\tresult.Stdout = aws.ToString(resp.StandardOutputContent)\n\t\tresult.ExitCode = int64(resp.ResponseCode)\n\n\t\tstatus := resp.Status\n\n\t\tif status == types.CommandInvocationStatusSuccess {\n\t\t\treturn \"\", nil\n\t\t}\n\n\t\tif status == types.CommandInvocationStatusFailed {\n\t\t\treturn \"\", fmt.Errorf(\"%s\", aws.ToString(resp.StatusDetails))\n\t\t}\n\n\t\treturn \"\", fmt.Errorf(\"bad status: %s\", status)\n\t})\n\n\tif err != nil {\n\t\tvar actualErr retry.FatalError\n\t\tif errors.As(err, &actualErr) {\n\t\t\treturn result, actualErr.Underlying\n\t\t}\n\t\treturn result, fmt.Errorf(\"unexpected error: %v\", err)\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "modules/aws/ssm_test.go",
    "content": "package aws\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/logger\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestParameterIsFound(t *testing.T) {\n\tt.Parallel()\n\n\texpectedName := fmt.Sprintf(\"test-name-%s\", random.UniqueId())\n\tawsRegion := GetRandomRegion(t, nil, nil)\n\texpectedValue := fmt.Sprintf(\"test-value-%s\", random.UniqueId())\n\texpectedDescription := fmt.Sprintf(\"test-description-%s\", random.UniqueId())\n\tversion := PutParameter(t, awsRegion, expectedName, expectedDescription, expectedValue)\n\tlogger.Default.Logf(t, \"Created parameter with version %d\", version)\n\tkeyValue := GetParameter(t, awsRegion, expectedName)\n\tlogger.Default.Logf(t, \"Found key with name %s\", expectedName)\n\tassert.Equal(t, expectedValue, keyValue)\n}\n\nfunc TestParameterIsDeleted(t *testing.T) {\n\texpectedName := fmt.Sprintf(\"test-name-%s\", random.UniqueId())\n\tawsRegion := GetRandomRegion(t, nil, nil)\n\texpectedValue := fmt.Sprintf(\"test-value-%s\", random.UniqueId())\n\texpectedDescription := fmt.Sprintf(\"test-description-%s\", random.UniqueId())\n\tversion := PutParameter(t, awsRegion, expectedName, expectedDescription, expectedValue)\n\tlogger.Default.Logf(t, \"Created parameter with version %d\", version)\n\n\tDeleteParameter(t, awsRegion, expectedName)\n\tlogger.Default.Logf(t, \"Deleted paramter %s\", expectedName)\n\n\tactualValue, err := GetParameterE(t, awsRegion, expectedName)\n\tassert.Equal(t, actualValue, \"\")\n\tassert.Error(t, err)\n}\n"
  },
  {
    "path": "modules/aws/vpc.go",
    "content": "package aws\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\t\"github.com/aws/aws-sdk-go-v2/service/ec2\"\n\t\"github.com/aws/aws-sdk-go-v2/service/ec2/types\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// Vpc is an Amazon Virtual Private Cloud.\ntype Vpc struct {\n\tId                   string            // The ID of the VPC\n\tName                 string            // The name of the VPC\n\tSubnets              []Subnet          // A list of subnets in the VPC\n\tTags                 map[string]string // The tags associated with the VPC\n\tCidrBlock            *string           // The primary IPv4 CIDR block for the VPC.\n\tCidrAssociations     []*string         // Information about the IPv4 CIDR blocks associated with the VPC.\n\tIpv6CidrAssociations []*string         // Information about the IPv6 CIDR blocks associated with the VPC.\n}\n\n// Subnet is a subnet in an availability zone.\ntype Subnet struct {\n\tId               string            // The ID of the Subnet\n\tAvailabilityZone string            // The Availability Zone the subnet is in\n\tDefaultForAz     bool              // If the subnet is default for the Availability Zone\n\tTags             map[string]string // The tags associated with the subnet\n\tCidrBlock        string            // The CIDR block associated with the subnet\n}\n\nconst vpcIDFilterName = \"vpc-id\"\nconst defaultForAzFilterName = \"default-for-az\"\nconst resourceTypeFilterName = \"resource-type\"\nconst resourceIdFilterName = \"resource-id\"\nconst vpcResourceTypeFilterValue = \"vpc\"\nconst subnetResourceTypeFilterValue = \"subnet\"\nconst isDefaultFilterName = \"isDefault\"\nconst isDefaultFilterValue = \"true\"\nconst defaultVPCName = \"Default\"\n\n// GetDefaultVpc fetches information about the default VPC in the given region.\nfunc GetDefaultVpc(t testing.TestingT, region string) *Vpc {\n\tvpc, err := GetDefaultVpcE(t, region)\n\trequire.NoError(t, err)\n\treturn vpc\n}\n\n// GetDefaultVpcE fetches information about the default VPC in the given region.\nfunc GetDefaultVpcE(t testing.TestingT, region string) (*Vpc, error) {\n\tdefaultVpcFilter := types.Filter{Name: aws.String(isDefaultFilterName), Values: []string{isDefaultFilterValue}}\n\tvpcs, err := GetVpcsE(t, []types.Filter{defaultVpcFilter}, region)\n\n\tnumVpcs := len(vpcs)\n\tif numVpcs != 1 {\n\t\treturn nil, fmt.Errorf(\"expected to find one default VPC in region %s but found %s\", region, strconv.Itoa(numVpcs))\n\t}\n\n\treturn vpcs[0], err\n}\n\n// GetVpcById fetches information about a VPC with given ID in the given region.\nfunc GetVpcById(t testing.TestingT, vpcId string, region string) *Vpc {\n\tvpc, err := GetVpcByIdE(t, vpcId, region)\n\trequire.NoError(t, err)\n\treturn vpc\n}\n\n// GetVpcByIdE fetches information about a VPC with given ID in the given region.\nfunc GetVpcByIdE(t testing.TestingT, vpcId string, region string) (*Vpc, error) {\n\tvpcIdFilter := types.Filter{Name: aws.String(vpcIDFilterName), Values: []string{vpcId}}\n\tvpcs, err := GetVpcsE(t, []types.Filter{vpcIdFilter}, region)\n\n\tnumVpcs := len(vpcs)\n\tif numVpcs != 1 {\n\t\treturn nil, fmt.Errorf(\"expected to find one VPC with ID %s in region %s but found %s\", vpcId, region, strconv.Itoa(numVpcs))\n\t}\n\n\treturn vpcs[0], err\n}\n\n// GetVpcsE fetches information about VPCs from given regions limited by filters\nfunc GetVpcsE(t testing.TestingT, filters []types.Filter, region string) ([]*Vpc, error) {\n\tclient, err := NewEc2ClientE(t, region)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvpcs, err := client.DescribeVpcs(context.Background(), &ec2.DescribeVpcsInput{Filters: filters})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tnumVpcs := len(vpcs.Vpcs)\n\tretVal := make([]*Vpc, numVpcs)\n\n\tfor i, vpc := range vpcs.Vpcs {\n\t\tvpcIdFilter := generateVpcIdFilter(aws.ToString(vpc.VpcId))\n\t\tsubnets, err := GetSubnetsForVpcE(t, region, []types.Filter{vpcIdFilter})\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\ttags, err := GetTagsForVpcE(t, aws.ToString(vpc.VpcId), region)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\t// cidr block associations\n\t\tvar cidrBlockAssociations = func() (list []*string) {\n\t\t\tfor _, cidr := range vpc.CidrBlockAssociationSet {\n\t\t\t\tlist = append(list, cidr.CidrBlock)\n\t\t\t}\n\t\t\treturn\n\t\t}()\n\n\t\t// ipv6 cidr block associations\n\t\tvar Ipv6CidrAssociations = func() (list []*string) {\n\t\t\tfor _, cidr := range vpc.Ipv6CidrBlockAssociationSet {\n\t\t\t\tlist = append(list, cidr.Ipv6CidrBlock)\n\t\t\t}\n\t\t\treturn\n\t\t}()\n\n\t\tretVal[i] = &Vpc{\n\t\t\tId:                   aws.ToString(vpc.VpcId),\n\t\t\tName:                 FindVpcName(vpc),\n\t\t\tSubnets:              subnets,\n\t\t\tTags:                 tags,\n\t\t\tCidrBlock:            vpc.CidrBlock,\n\t\t\tCidrAssociations:     cidrBlockAssociations,\n\t\t\tIpv6CidrAssociations: Ipv6CidrAssociations,\n\t\t}\n\t}\n\n\treturn retVal, nil\n}\n\n// FindVpcName extracts the VPC name from its tags (if any). Fall back to \"Default\" if it's the default VPC or empty string\n// otherwise.\nfunc FindVpcName(vpc types.Vpc) string {\n\tfor _, tag := range vpc.Tags {\n\t\tif *tag.Key == \"Name\" {\n\t\t\treturn *tag.Value\n\t\t}\n\t}\n\n\tif *vpc.IsDefault {\n\t\treturn defaultVPCName\n\t}\n\n\treturn \"\"\n}\n\n// GetSubnetsForVpc gets the subnets in the specified VPC.\nfunc GetSubnetsForVpc(t testing.TestingT, vpcID string, region string) []Subnet {\n\tvpcIDFilter := generateVpcIdFilter(vpcID)\n\tsubnets, err := GetSubnetsForVpcE(t, region, []types.Filter{vpcIDFilter})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn subnets\n}\n\n// GetAzDefaultSubnetsForVpc gets the default az subnets in the specified VPC.\nfunc GetAzDefaultSubnetsForVpc(t testing.TestingT, vpcID string, region string) []Subnet {\n\tvpcIDFilter := generateVpcIdFilter(vpcID)\n\tdefaultForAzFilter := types.Filter{\n\t\tName:   aws.String(defaultForAzFilterName),\n\t\tValues: []string{\"true\"},\n\t}\n\tsubnets, err := GetSubnetsForVpcE(t, region, []types.Filter{vpcIDFilter, defaultForAzFilter})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn subnets\n}\n\n// generateVpcIdFilter is a helper method to generate vpc id filter\nfunc generateVpcIdFilter(vpcID string) types.Filter {\n\treturn types.Filter{Name: aws.String(vpcIDFilterName), Values: []string{vpcID}}\n}\n\n// GetSubnetsForVpcE gets the subnets in the specified VPC.\nfunc GetSubnetsForVpcE(t testing.TestingT, region string, filters []types.Filter) ([]Subnet, error) {\n\tclient, err := NewEc2ClientE(t, region)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tsubnetOutput, err := client.DescribeSubnets(context.Background(), &ec2.DescribeSubnetsInput{Filters: filters})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar subnets []Subnet\n\n\tfor _, ec2Subnet := range subnetOutput.Subnets {\n\t\tsubnetTags := GetTagsForSubnet(t, *ec2Subnet.SubnetId, region)\n\t\tsubnet := Subnet{Id: aws.ToString(ec2Subnet.SubnetId), AvailabilityZone: aws.ToString(ec2Subnet.AvailabilityZone), DefaultForAz: aws.ToBool(ec2Subnet.DefaultForAz), Tags: subnetTags, CidrBlock: aws.ToString(ec2Subnet.CidrBlock)}\n\t\tsubnets = append(subnets, subnet)\n\t}\n\n\treturn subnets, nil\n}\n\n// GetTagsForVpc gets the tags for the specified VPC.\nfunc GetTagsForVpc(t testing.TestingT, vpcID string, region string) map[string]string {\n\ttags, err := GetTagsForVpcE(t, vpcID, region)\n\trequire.NoError(t, err)\n\n\treturn tags\n}\n\n// GetTagsForVpcE gets the tags for the specified VPC.\nfunc GetTagsForVpcE(t testing.TestingT, vpcID string, region string) (map[string]string, error) {\n\tclient, err := NewEc2ClientE(t, region)\n\trequire.NoError(t, err)\n\n\tvpcResourceTypeFilter := types.Filter{Name: aws.String(resourceTypeFilterName), Values: []string{vpcResourceTypeFilterValue}}\n\tvpcResourceIdFilter := types.Filter{Name: aws.String(resourceIdFilterName), Values: []string{vpcID}}\n\ttagsOutput, err := client.DescribeTags(context.Background(), &ec2.DescribeTagsInput{Filters: []types.Filter{vpcResourceTypeFilter, vpcResourceIdFilter}})\n\trequire.NoError(t, err)\n\n\ttags := map[string]string{}\n\tfor _, tag := range tagsOutput.Tags {\n\t\ttags[aws.ToString(tag.Key)] = aws.ToString(tag.Value)\n\t}\n\n\treturn tags, nil\n}\n\n// GetDefaultSubnetIDsForVpc gets the ids of the subnets that are the default subnet for the AvailabilityZone\nfunc GetDefaultSubnetIDsForVpc(t testing.TestingT, vpc Vpc) []string {\n\tsubnetIDs, err := GetDefaultSubnetIDsForVpcE(t, vpc)\n\trequire.NoError(t, err)\n\treturn subnetIDs\n}\n\n// GetDefaultSubnetIDsForVpcE gets the ids of the subnets that are the default subnet for the AvailabilityZone\nfunc GetDefaultSubnetIDsForVpcE(t testing.TestingT, vpc Vpc) ([]string, error) {\n\tif vpc.Name != defaultVPCName {\n\t\t// You cannot create a default subnet in a nondefault VPC\n\t\t// https://docs.aws.amazon.com/vpc/latest/userguide/default-vpc.html\n\t\treturn nil, fmt.Errorf(\"only default VPCs have default subnets but VPC with id %s is not default VPC\", vpc.Id)\n\t}\n\tvar subnetIDs []string\n\tnumSubnets := len(vpc.Subnets)\n\tif numSubnets == 0 {\n\t\treturn nil, fmt.Errorf(\"expected to find at least one subnet in vpc with ID %s but found zero\", vpc.Id)\n\t}\n\n\tfor _, subnet := range vpc.Subnets {\n\t\tif subnet.DefaultForAz {\n\t\t\tsubnetIDs = append(subnetIDs, subnet.Id)\n\t\t}\n\t}\n\treturn subnetIDs, nil\n}\n\n// GetTagsForSubnet gets the tags for the specified subnet.\nfunc GetTagsForSubnet(t testing.TestingT, subnetId string, region string) map[string]string {\n\ttags, err := GetTagsForSubnetE(t, subnetId, region)\n\trequire.NoError(t, err)\n\n\treturn tags\n}\n\n// GetTagsForSubnetE gets the tags for the specified subnet.\nfunc GetTagsForSubnetE(t testing.TestingT, subnetId string, region string) (map[string]string, error) {\n\tclient, err := NewEc2ClientE(t, region)\n\trequire.NoError(t, err)\n\n\tsubnetResourceTypeFilter := types.Filter{Name: aws.String(resourceTypeFilterName), Values: []string{subnetResourceTypeFilterValue}}\n\tsubnetResourceIdFilter := types.Filter{Name: aws.String(resourceIdFilterName), Values: []string{subnetId}}\n\ttagsOutput, err := client.DescribeTags(context.Background(), &ec2.DescribeTagsInput{Filters: []types.Filter{subnetResourceTypeFilter, subnetResourceIdFilter}})\n\trequire.NoError(t, err)\n\n\ttags := map[string]string{}\n\tfor _, tag := range tagsOutput.Tags {\n\t\ttags[aws.ToString(tag.Key)] = aws.ToString(tag.Value)\n\t}\n\n\treturn tags, nil\n}\n\n// IsPublicSubnet returns True if the subnet identified by the given id in the provided region is public.\nfunc IsPublicSubnet(t testing.TestingT, subnetId string, region string) bool {\n\tisPublic, err := IsPublicSubnetE(t, subnetId, region)\n\trequire.NoError(t, err)\n\treturn isPublic\n}\n\n// IsPublicSubnetE returns True if the subnet identified by the given id in the provided region is public.\nfunc IsPublicSubnetE(t testing.TestingT, subnetId string, region string) (bool, error) {\n\tsubnetIdFilterName := \"association.subnet-id\"\n\n\tsubnetIdFilter := types.Filter{\n\t\tName:   &subnetIdFilterName,\n\t\tValues: []string{subnetId},\n\t}\n\n\tclient, err := NewEc2ClientE(t, region)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\trts, err := client.DescribeRouteTables(context.Background(), &ec2.DescribeRouteTablesInput{Filters: []types.Filter{subnetIdFilter}})\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tif len(rts.RouteTables) == 0 {\n\t\t// Subnets not explicitly associated with any route table are implicitly associated with the main route table\n\t\trts, err = getImplicitRouteTableForSubnetE(t, subnetId, region)\n\t\tif err != nil {\n\t\t\treturn false, err\n\t\t}\n\t}\n\n\tfor _, rt := range rts.RouteTables {\n\t\tfor _, r := range rt.Routes {\n\t\t\tif strings.HasPrefix(aws.ToString(r.GatewayId), \"igw-\") {\n\t\t\t\treturn true, nil\n\t\t\t}\n\t\t}\n\t}\n\n\treturn false, nil\n}\n\nfunc getImplicitRouteTableForSubnetE(t testing.TestingT, subnetId string, region string) (*ec2.DescribeRouteTablesOutput, error) {\n\tmainRouteFilterName := \"association.main\"\n\tmainRouteFilterValue := \"true\"\n\tsubnetFilterName := \"subnet-id\"\n\n\tclient, err := NewEc2ClientE(t, region)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tsubnetFilter := types.Filter{\n\t\tName:   &subnetFilterName,\n\t\tValues: []string{subnetId},\n\t}\n\tsubnetOutput, err := client.DescribeSubnets(context.Background(), &ec2.DescribeSubnetsInput{Filters: []types.Filter{subnetFilter}})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tnumSubnets := len(subnetOutput.Subnets)\n\tif numSubnets != 1 {\n\t\treturn nil, fmt.Errorf(\"expected to find one subnet with id %s but found %s\", subnetId, strconv.Itoa(numSubnets))\n\t}\n\n\tmainRouteFilter := types.Filter{\n\t\tName:   &mainRouteFilterName,\n\t\tValues: []string{mainRouteFilterValue},\n\t}\n\tvpcFilter := types.Filter{\n\t\tName:   aws.String(vpcIDFilterName),\n\t\tValues: []string{*subnetOutput.Subnets[0].VpcId},\n\t}\n\treturn client.DescribeRouteTables(context.Background(), &ec2.DescribeRouteTablesInput{Filters: []types.Filter{mainRouteFilter, vpcFilter}})\n}\n\n// GetRandomPrivateCidrBlock gets a random CIDR block from the range of acceptable private IP addresses per RFC 1918\n// (https://tools.ietf.org/html/rfc1918#section-3)\n// The routingPrefix refers to the \"/28\" in 1.2.3.4/28.\n// Note that, as written, this function will return a subset of all valid ranges. Since we will probably use this function\n// mostly for generating random CIDR ranges for VPCs and Subnets, having comprehensive set coverage is not essential.\nfunc GetRandomPrivateCidrBlock(routingPrefix int) string {\n\n\tvar o1, o2, o3, o4 int\n\n\tswitch routingPrefix {\n\tcase 32:\n\t\to1 = random.RandomInt([]int{10, 172, 192})\n\n\t\tswitch o1 {\n\t\tcase 10:\n\t\t\to2 = random.Random(0, 255)\n\t\t\to3 = random.Random(0, 255)\n\t\t\to4 = random.Random(0, 255)\n\t\tcase 172:\n\t\t\to2 = random.Random(16, 31)\n\t\t\to3 = random.Random(0, 255)\n\t\t\to4 = random.Random(0, 255)\n\t\tcase 192:\n\t\t\to2 = 168\n\t\t\to3 = random.Random(0, 255)\n\t\t\to4 = random.Random(0, 255)\n\t\t}\n\n\tcase 31, 30, 29, 28, 27, 26, 25:\n\t\tfallthrough\n\tcase 24:\n\t\to1 = random.RandomInt([]int{10, 172, 192})\n\n\t\tswitch o1 {\n\t\tcase 10:\n\t\t\to2 = random.Random(0, 255)\n\t\t\to3 = random.Random(0, 255)\n\t\t\to4 = 0\n\t\tcase 172:\n\t\t\to2 = 16\n\t\t\to3 = 0\n\t\t\to4 = 0\n\t\tcase 192:\n\t\t\to2 = 168\n\t\t\to3 = 0\n\t\t\to4 = 0\n\t\t}\n\tcase 23, 22, 21, 20, 19:\n\t\tfallthrough\n\tcase 18:\n\t\to1 = random.RandomInt([]int{10, 172, 192})\n\n\t\tswitch o1 {\n\t\tcase 10:\n\t\t\to2 = 0\n\t\t\to3 = 0\n\t\t\to4 = 0\n\t\tcase 172:\n\t\t\to2 = 16\n\t\t\to3 = 0\n\t\t\to4 = 0\n\t\tcase 192:\n\t\t\to2 = 168\n\t\t\to3 = 0\n\t\t\to4 = 0\n\t\t}\n\t}\n\treturn fmt.Sprintf(\"%d.%d.%d.%d/%d\", o1, o2, o3, o4, routingPrefix)\n}\n\n// GetFirstTwoOctets gets the first two octets from a CIDR block.\nfunc GetFirstTwoOctets(cidrBlock string) string {\n\tipAddr := strings.Split(cidrBlock, \"/\")[0]\n\toctets := strings.Split(ipAddr, \".\")\n\treturn octets[0] + \".\" + octets[1]\n}\n"
  },
  {
    "path": "modules/aws/vpc_test.go",
    "content": "package aws\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\t\"github.com/aws/aws-sdk-go-v2/service/ec2\"\n\t\"github.com/aws/aws-sdk-go-v2/service/ec2/types\"\n)\n\nfunc TestGetDefaultVpc(t *testing.T) {\n\tt.Parallel()\n\n\tregion := GetRandomStableRegion(t, nil, nil)\n\tvpc := GetDefaultVpc(t, region)\n\n\tassert.NotEmpty(t, vpc.Name)\n\tassert.True(t, len(vpc.Subnets) > 0)\n\tassert.Regexp(t, \"^vpc-[[:alnum:]]+$\", vpc.Id)\n}\n\nfunc TestGetVpcById(t *testing.T) {\n\tt.Parallel()\n\n\tregion := GetRandomStableRegion(t, nil, nil)\n\tvpc := createVpc(t, region)\n\tdefer deleteVpc(t, *vpc.VpcId, region)\n\n\tvpcTest := GetVpcById(t, *vpc.VpcId, region)\n\tassert.Equal(t, *vpc.VpcId, vpcTest.Id)\n\tassert.NotEmpty(t, vpcTest.CidrAssociations)\n}\n\nfunc TestGetVpcsE(t *testing.T) {\n\tt.Parallel()\n\n\tregion := GetRandomStableRegion(t, nil, nil)\n\tazs := GetAvailabilityZones(t, region)\n\n\tisDefaultFilterName := \"isDefault\"\n\tisDefaultFilterValue := \"true\"\n\n\tdefaultVpcFilter := types.Filter{Name: &isDefaultFilterName, Values: []string{isDefaultFilterValue}}\n\tvpcs, _ := GetVpcsE(t, []types.Filter{defaultVpcFilter}, region)\n\n\trequire.Equal(t, len(vpcs), 1)\n\tassert.NotEmpty(t, vpcs[0].Name)\n\n\t// the default VPC has by default one subnet per availability zone\n\t// https://docs.aws.amazon.com/vpc/latest/userguide/default-vpc.html\n\tassert.True(t, len(vpcs[0].Subnets) >= len(azs))\n}\n\nfunc TestGetFirstTwoOctets(t *testing.T) {\n\tt.Parallel()\n\n\tfirstTwo := GetFirstTwoOctets(\"10.100.0.0/28\")\n\tif firstTwo != \"10.100\" {\n\t\tt.Errorf(\"Received: %s, Expected: 10.100\", firstTwo)\n\t}\n}\n\nfunc TestIsPublicSubnet(t *testing.T) {\n\tt.Parallel()\n\n\tregion := GetRandomStableRegion(t, nil, nil)\n\tvpc := createVpc(t, region)\n\tdefer deleteVpc(t, *vpc.VpcId, region)\n\n\trouteTable := createRouteTable(t, *vpc.VpcId, region)\n\tsubnet := createSubnet(t, *vpc.VpcId, *routeTable.RouteTableId, region)\n\tassert.False(t, IsPublicSubnet(t, *subnet.SubnetId, region))\n\n\tcreatePublicRoute(t, *vpc.VpcId, *routeTable.RouteTableId, region)\n\tassert.True(t, IsPublicSubnet(t, *subnet.SubnetId, region))\n}\n\nfunc TestGetDefaultSubnetIDsForVpc(t *testing.T) {\n\tt.Parallel()\n\n\tregion := GetRandomStableRegion(t, nil, nil)\n\tdefaultVpc := GetDefaultVpc(t, region)\n\n\tdefaultSubnetIDs := GetDefaultSubnetIDsForVpc(t, *defaultVpc)\n\tassert.NotEmpty(t, defaultSubnetIDs)\n\n\tavailabilityZones := []string{}\n\tfor _, id := range defaultSubnetIDs {\n\t\t// default subnets are by default public\n\t\t// https://docs.aws.amazon.com/vpc/latest/userguide/default-vpc.html\n\t\tassert.True(t, IsPublicSubnet(t, id, region))\n\t\tfor _, subnet := range defaultVpc.Subnets {\n\t\t\tif id == subnet.Id {\n\t\t\t\tavailabilityZones = append(availabilityZones, subnet.AvailabilityZone)\n\t\t\t}\n\t\t}\n\t}\n\t// only one default subnet is allowed per AZ\n\tuniqueAZs := map[string]bool{}\n\tfor _, az := range availabilityZones {\n\t\tuniqueAZs[az] = true\n\t}\n\tassert.Equal(t, len(defaultSubnetIDs), len(uniqueAZs))\n}\n\nfunc TestGetTagsForVpc(t *testing.T) {\n\tt.Parallel()\n\n\tregion := GetRandomStableRegion(t, nil, nil)\n\tvpc := createVpc(t, region)\n\tdefer deleteVpc(t, *vpc.VpcId, region)\n\n\tnoTags := GetTagsForVpc(t, *vpc.VpcId, region)\n\tassert.True(t, len(vpc.Tags) == 0)\n\tassert.True(t, len(noTags) == 0)\n\n\ttestTags := make(map[string]string)\n\ttestTags[\"TagKey1\"] = \"TagValue1\"\n\ttestTags[\"TagKey2\"] = \"TagValue2\"\n\n\tAddTagsToResource(t, region, *vpc.VpcId, testTags)\n\tvpcWithTags := GetVpcById(t, *vpc.VpcId, region)\n\ttags := GetTagsForVpc(t, *vpc.VpcId, region)\n\n\tassert.True(t, len(vpcWithTags.Tags) == len(testTags))\n\tassert.True(t, len(tags) == len(testTags))\n}\n\nfunc TestGetTagsForSubnet(t *testing.T) {\n\tt.Parallel()\n\n\tregion := GetRandomStableRegion(t, nil, nil)\n\tvpc := createVpc(t, region)\n\tdefer deleteVpc(t, *vpc.VpcId, region)\n\n\trouteTable := createRouteTable(t, *vpc.VpcId, region)\n\tsubnet := createSubnet(t, *vpc.VpcId, *routeTable.RouteTableId, region)\n\n\tnoTags := GetTagsForSubnet(t, *subnet.SubnetId, region)\n\tassert.True(t, len(subnet.Tags) == 0)\n\tassert.True(t, len(noTags) == 0)\n\n\ttestTags := make(map[string]string)\n\ttestTags[\"TagKey1\"] = \"TagValue1\"\n\ttestTags[\"TagKey2\"] = \"TagValue2\"\n\n\tAddTagsToResource(t, region, *subnet.SubnetId, testTags)\n\n\tsubnetWithTags := GetSubnetsForVpc(t, *vpc.VpcId, region)[0]\n\ttags := GetTagsForSubnet(t, *subnet.SubnetId, region)\n\n\tassert.True(t, len(subnetWithTags.Tags) == len(testTags))\n\tassert.True(t, len(tags) == len(testTags))\n\tassert.True(t, testTags[\"TagKey1\"] == \"TagValue1\")\n\tassert.True(t, testTags[\"TagKey2\"] == \"TagValue2\")\n}\n\nfunc TestGetDefaultAzSubnets(t *testing.T) {\n\tt.Parallel()\n\n\tregion := GetRandomStableRegion(t, nil, nil)\n\tvpc := GetDefaultVpc(t, region)\n\n\t// Note: cannot know exact list of default azs aheard of time, but we know that\n\t// it must be greater than 0 for default vpc.\n\tsubnets := GetAzDefaultSubnetsForVpc(t, vpc.Id, region)\n\tassert.NotZero(t, len(subnets))\n}\n\nfunc createPublicRoute(t *testing.T, vpcId string, routeTableId string, region string) {\n\tec2Client := NewEc2Client(t, region)\n\n\tcreateIGWOut, igerr := ec2Client.CreateInternetGateway(context.Background(), &ec2.CreateInternetGatewayInput{})\n\trequire.NoError(t, igerr)\n\n\t_, aigerr := ec2Client.AttachInternetGateway(context.Background(), &ec2.AttachInternetGatewayInput{\n\t\tInternetGatewayId: createIGWOut.InternetGateway.InternetGatewayId,\n\t\tVpcId:             aws.String(vpcId),\n\t})\n\trequire.NoError(t, aigerr)\n\n\t_, err := ec2Client.CreateRoute(context.Background(), &ec2.CreateRouteInput{\n\t\tRouteTableId:         aws.String(routeTableId),\n\t\tDestinationCidrBlock: aws.String(\"0.0.0.0/0\"),\n\t\tGatewayId:            createIGWOut.InternetGateway.InternetGatewayId,\n\t})\n\n\trequire.NoError(t, err)\n}\n\nfunc createRouteTable(t *testing.T, vpcId string, region string) types.RouteTable {\n\tec2Client := NewEc2Client(t, region)\n\n\tcreateRouteTableOutput, err := ec2Client.CreateRouteTable(context.Background(), &ec2.CreateRouteTableInput{\n\t\tVpcId: aws.String(vpcId),\n\t})\n\n\trequire.NoError(t, err)\n\treturn *createRouteTableOutput.RouteTable\n}\n\nfunc createSubnet(t *testing.T, vpcId string, routeTableId string, region string) types.Subnet {\n\tec2Client := NewEc2Client(t, region)\n\n\tcreateSubnetOutput, err := ec2Client.CreateSubnet(context.Background(), &ec2.CreateSubnetInput{\n\t\tCidrBlock: aws.String(\"10.10.1.0/24\"),\n\t\tVpcId:     aws.String(vpcId),\n\t})\n\trequire.NoError(t, err)\n\n\t_, err = ec2Client.AssociateRouteTable(context.Background(), &ec2.AssociateRouteTableInput{\n\t\tRouteTableId: aws.String(routeTableId),\n\t\tSubnetId:     aws.String(*createSubnetOutput.Subnet.SubnetId),\n\t})\n\trequire.NoError(t, err)\n\n\treturn *createSubnetOutput.Subnet\n}\n\nfunc createVpc(t *testing.T, region string) types.Vpc {\n\tec2Client := NewEc2Client(t, region)\n\n\tcreateVpcOutput, err := ec2Client.CreateVpc(context.Background(), &ec2.CreateVpcInput{\n\t\tCidrBlock: aws.String(\"10.10.0.0/16\"),\n\t})\n\n\trequire.NoError(t, err)\n\treturn *createVpcOutput.Vpc\n}\n\nfunc deleteRouteTables(t *testing.T, vpcId string, region string) {\n\tec2Client := NewEc2Client(t, region)\n\n\tvpcIDFilterName := \"vpc-id\"\n\tvpcIDFilter := types.Filter{Name: &vpcIDFilterName, Values: []string{vpcId}}\n\n\t// \"You can't delete the main route table.\"\n\tmainRTFilterName := \"association.main\"\n\tmainRTFilterValue := \"false\"\n\tnotMainRTFilter := types.Filter{Name: &mainRTFilterName, Values: []string{mainRTFilterValue}}\n\n\tfilters := []types.Filter{vpcIDFilter, notMainRTFilter}\n\n\trtOutput, err := ec2Client.DescribeRouteTables(context.Background(), &ec2.DescribeRouteTablesInput{Filters: filters})\n\trequire.NoError(t, err)\n\n\tfor _, rt := range rtOutput.RouteTables {\n\n\t\t// \"You must disassociate the route table from any subnets before you can delete it.\"\n\t\tfor _, assoc := range rt.Associations {\n\t\t\t_, disassocErr := ec2Client.DisassociateRouteTable(context.Background(), &ec2.DisassociateRouteTableInput{\n\t\t\t\tAssociationId: assoc.RouteTableAssociationId,\n\t\t\t})\n\t\t\trequire.NoError(t, disassocErr)\n\t\t}\n\n\t\t_, err := ec2Client.DeleteRouteTable(context.Background(), &ec2.DeleteRouteTableInput{\n\t\t\tRouteTableId: rt.RouteTableId,\n\t\t})\n\t\trequire.NoError(t, err)\n\t}\n}\n\nfunc deleteSubnets(t *testing.T, vpcId string, region string) {\n\tec2Client := NewEc2Client(t, region)\n\tvpcIDFilterName := \"vpc-id\"\n\tvpcIDFilter := types.Filter{Name: &vpcIDFilterName, Values: []string{vpcId}}\n\n\tsubnetsOutput, err := ec2Client.DescribeSubnets(context.Background(), &ec2.DescribeSubnetsInput{Filters: []types.Filter{vpcIDFilter}})\n\trequire.NoError(t, err)\n\n\tfor _, subnet := range subnetsOutput.Subnets {\n\t\t_, err := ec2Client.DeleteSubnet(context.Background(), &ec2.DeleteSubnetInput{\n\t\t\tSubnetId: subnet.SubnetId,\n\t\t})\n\t\trequire.NoError(t, err)\n\t}\n}\n\nfunc deleteInternetGateways(t *testing.T, vpcId string, region string) {\n\tec2Client := NewEc2Client(t, region)\n\tvpcIDFilterName := \"attachment.vpc-id\"\n\tvpcIDFilter := types.Filter{Name: &vpcIDFilterName, Values: []string{vpcId}}\n\n\tigwOutput, err := ec2Client.DescribeInternetGateways(context.Background(), &ec2.DescribeInternetGatewaysInput{Filters: []types.Filter{vpcIDFilter}})\n\trequire.NoError(t, err)\n\n\tfor _, igw := range igwOutput.InternetGateways {\n\n\t\t_, detachErr := ec2Client.DetachInternetGateway(context.Background(), &ec2.DetachInternetGatewayInput{\n\t\t\tInternetGatewayId: igw.InternetGatewayId,\n\t\t\tVpcId:             aws.String(vpcId),\n\t\t})\n\t\trequire.NoError(t, detachErr)\n\n\t\t_, err := ec2Client.DeleteInternetGateway(context.Background(), &ec2.DeleteInternetGatewayInput{\n\t\t\tInternetGatewayId: igw.InternetGatewayId,\n\t\t})\n\t\trequire.NoError(t, err)\n\t}\n}\n\nfunc deleteVpc(t *testing.T, vpcId string, region string) {\n\tec2Client := NewEc2Client(t, region)\n\n\tdeleteRouteTables(t, vpcId, region)\n\tdeleteSubnets(t, vpcId, region)\n\tdeleteInternetGateways(t, vpcId, region)\n\n\t_, err := ec2Client.DeleteVpc(context.Background(), &ec2.DeleteVpcInput{\n\t\tVpcId: aws.String(vpcId),\n\t})\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "modules/azure/actiongroup.go",
    "content": "package azure\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/Azure/azure-sdk-for-go/profiles/preview/preview/monitor/mgmt/insights\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// GetActionGroupResource gets the ActionGroupResource.\n// ruleName - required to find the ActionGroupResource.\n// resGroupName - use an empty string if you have the AZURE_RES_GROUP_NAME environment variable set\n// subscriptionId - use an empty string if you have the ARM_SUBSCRIPTION_ID environment variable set\nfunc GetActionGroupResource(t *testing.T, ruleName string, resGroupName string, subscriptionID string) *insights.ActionGroupResource {\n\tactionGroupResource, err := GetActionGroupResourceE(ruleName, resGroupName, subscriptionID)\n\trequire.NoError(t, err)\n\n\treturn actionGroupResource\n}\n\n// GetActionGroupResourceE gets the ActionGroupResource with Error details on error.\n// ruleName - required to find the ActionGroupResource.\n// resGroupName - use an empty string if you have the AZURE_RES_GROUP_NAME environment variable set\n// subscriptionId - use an empty string if you have the ARM_SUBSCRIPTION_ID environment variable set\nfunc GetActionGroupResourceE(ruleName string, resGroupName string, subscriptionID string) (*insights.ActionGroupResource, error) {\n\trgName, err := getTargetAzureResourceGroupName(resGroupName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tclient, err := CreateActionGroupClient(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tactionGroup, err := client.Get(context.Background(), rgName, ruleName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &actionGroup, nil\n}\n\n// TODO: remove in next version\nfunc getActionGroupClient(subscriptionID string) (*insights.ActionGroupsClient, error) {\n\tsubID, err := getTargetAzureSubscription(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tmetricAlertsClient := insights.NewActionGroupsClient(subID)\n\n\tauthorizer, err := NewAuthorizer()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tmetricAlertsClient.Authorizer = *authorizer\n\n\treturn &metricAlertsClient, nil\n}\n"
  },
  {
    "path": "modules/azure/actiongroup_test.go",
    "content": "//go:build azure\n// +build azure\n\n// NOTE: We use build tags to differentiate azure testing because we currently do not have azure access setup for\n// CircleCI.\n\npackage azure\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n/*\nThe below tests are currently stubbed out, with the expectation that they will throw errors.\nIf/when methods to create and delete network resources are added, these tests can be extended.\n*/\n\nfunc TestGetActionGroupResourceEWithMissingResourceGroupName(t *testing.T) {\n\tt.Parallel()\n\n\truleName := \"Hello\"\n\tresGroupName := \"\"\n\tsubscriptionID := \"\"\n\n\t_, err := GetActionGroupResourceE(ruleName, resGroupName, subscriptionID)\n\n\trequire.Error(t, err)\n}\n\nfunc TestGetActionGroupResourceEWithInvalidResourceGroupName(t *testing.T) {\n\tt.Parallel()\n\n\truleName := \"\"\n\tresGroupName := \"Hello\"\n\tsubscriptionID := \"\"\n\n\t_, err := GetActionGroupResourceE(ruleName, resGroupName, subscriptionID)\n\n\trequire.Error(t, err)\n}\n\nfunc TestGetActionGroupClient(t *testing.T) {\n\tt.Parallel()\n\n\tsubscriptionID := \"\"\n\n\tclient, err := getActionGroupClient(subscriptionID)\n\n\trequire.NoError(t, err)\n\tassert.NotEmpty(t, *client)\n}\n"
  },
  {
    "path": "modules/azure/aks.go",
    "content": "package azure\n\nimport (\n\t\"context\"\n\n\t\"github.com/Azure/azure-sdk-for-go/services/containerservice/mgmt/2019-11-01/containerservice\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n)\n\n// GetManagedClustersClientE is a helper function that will setup an Azure ManagedClusters client on your behalf\nfunc GetManagedClustersClientE(subscriptionID string) (*containerservice.ManagedClustersClient, error) {\n\t// Create a cluster client\n\tclient, err := CreateManagedClustersClientE(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// setup authorizer\n\tauthorizer, err := NewAuthorizer()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tclient.Authorizer = *authorizer\n\treturn &client, nil\n}\n\n// GetManagedClusterE will return ManagedCluster\nfunc GetManagedClusterE(t testing.TestingT, resourceGroupName, clusterName, subscriptionID string) (*containerservice.ManagedCluster, error) {\n\tsubscriptionID, err := getTargetAzureSubscription(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tclient, err := GetManagedClustersClientE(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tmanagedCluster, err := client.Get(context.Background(), resourceGroupName, clusterName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &managedCluster, nil\n}\n"
  },
  {
    "path": "modules/azure/appService.go",
    "content": "package azure\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/appservice/armappservice/v2\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// AppExists indicates whether the specified application exists.\n// This function would fail the test if there is an error.\nfunc AppExists(t *testing.T, appName string, resourceGroupName string, subscriptionID string) bool {\n\texists, err := AppExistsE(appName, resourceGroupName, subscriptionID)\n\trequire.NoError(t, err)\n\n\treturn exists\n}\n\n// AppExistsE indicates whether the specified application exists.\nfunc AppExistsE(appName string, resourceGroupName string, subscriptionID string) (bool, error) {\n\t_, err := GetAppServiceE(appName, resourceGroupName, subscriptionID)\n\tif err != nil {\n\t\tif ResourceNotFoundErrorExists(err) {\n\t\t\treturn false, nil\n\t\t}\n\t\treturn false, err\n\t}\n\treturn true, nil\n}\n\n// GetAppService gets the App service object\n// This function would fail the test if there is an error.\nfunc GetAppService(t *testing.T, appName string, resGroupName string, subscriptionID string) *armappservice.Site {\n\tsite, err := GetAppServiceE(appName, resGroupName, subscriptionID)\n\trequire.NoError(t, err)\n\n\treturn site\n}\n\n// GetAppServiceE gets the App service object\nfunc GetAppServiceE(appName string, resGroupName string, subscriptionID string) (*armappservice.Site, error) {\n\trgName, err := getTargetAzureResourceGroupName(resGroupName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tclient, err := GetAppServiceClientE(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresp, err := client.Get(context.Background(), rgName, appName, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &resp.Site, nil\n}\n\n// GetAppServiceClientE creates and returns an App Service web apps client\nfunc GetAppServiceClientE(subscriptionID string) (*armappservice.WebAppsClient, error) {\n\tclientFactory, err := getArmAppServiceClientFactory(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn clientFactory.NewWebAppsClient(), nil\n}\n"
  },
  {
    "path": "modules/azure/appService_test.go",
    "content": "//go:build azure\n// +build azure\n\n// NOTE: We use build tags to differentiate azure testing because we currently do not have azure access setup for\n// CircleCI.\n\npackage azure\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\n/*\nThe below tests are currently stubbed out, with the expectation that they will throw errors.\nIf/when CRUD methods are introduced for Azure MySQL server and database, these tests can be extended\n*/\n\nfunc TestAppExistsE(t *testing.T) {\n\tt.Parallel()\n\n\tresGroupName := \"\"\n\tappName := \"\"\n\tsubscriptionID := \"\"\n\n\t_, err := AppExistsE(appName, resGroupName, subscriptionID)\n\trequire.Error(t, err)\n}\n\nfunc TestGetAppServiceE(t *testing.T) {\n\tt.Parallel()\n\n\tresGroupName := \"\"\n\tappName := \"\"\n\tsubscriptionID := \"\"\n\n\t_, err := GetAppServiceE(appName, resGroupName, subscriptionID)\n\trequire.Error(t, err)\n}\n\nfunc TestGetAppServiceClientE(t *testing.T) {\n\tt.Parallel()\n\n\tsubscriptionID := \"\"\n\n\t_, err := GetAppServiceClientE(subscriptionID)\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "modules/azure/authorizer.go",
    "content": "package azure\n\nimport (\n\t\"os\"\n\n\t\"github.com/Azure/go-autorest/autorest\"\n\taz \"github.com/Azure/go-autorest/autorest/azure\"\n\t\"github.com/Azure/go-autorest/autorest/azure/auth\"\n)\n\nconst (\n\t// AuthFromEnvClient is an env variable supported by the Azure SDK\n\tAuthFromEnvClient = \"AZURE_CLIENT_ID\"\n\n\t// AuthFromEnvTenant is an env variable supported by the Azure SDK\n\tAuthFromEnvTenant = \"AZURE_TENANT_ID\"\n\n\t// AuthFromFile is an env variable supported by the Azure SDK\n\tAuthFromFile = \"AZURE_AUTH_LOCATION\"\n)\n\n// NewAuthorizer creates an Azure authorizer adhering to standard auth mechanisms provided by the Azure Go SDK\n// See Azure Go Auth docs here: https://docs.microsoft.com/en-us/go/azure/azure-sdk-go-authorization\nfunc NewAuthorizer() (*autorest.Authorizer, error) {\n\t// Carry out env var lookups\n\t_, clientIDExists := os.LookupEnv(AuthFromEnvClient)\n\t_, tenantIDExists := os.LookupEnv(AuthFromEnvTenant)\n\t_, fileAuthSet := os.LookupEnv(AuthFromFile)\n\n\t// Execute logic to return an authorizer from the correct method\n\tif clientIDExists && tenantIDExists {\n\t\tauthorizer, err := auth.NewAuthorizerFromEnvironment()\n\t\treturn &authorizer, err\n\t} else if fileAuthSet {\n\t\tauthorizer, err := auth.NewAuthorizerFromFile(az.PublicCloud.ResourceManagerEndpoint)\n\t\treturn &authorizer, err\n\t} else {\n\t\tauthorizer, err := auth.NewAuthorizerFromCLI()\n\t\treturn &authorizer, err\n\t}\n}\n"
  },
  {
    "path": "modules/azure/availabilityset.go",
    "content": "package azure\n\nimport (\n\t\"context\"\n\t\"strings\"\n\n\t\"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2019-07-01/compute\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// AvailabilitySetExists indicates whether the specified Azure Availability Set exists.\n// This function would fail the test if there is an error.\nfunc AvailabilitySetExists(t testing.TestingT, avsName string, resGroupName string, subscriptionID string) bool {\n\texists, err := AvailabilitySetExistsE(t, avsName, resGroupName, subscriptionID)\n\trequire.NoError(t, err)\n\treturn exists\n}\n\n// AvailabilitySetExistsE indicates whether the specified Azure Availability Set exists\nfunc AvailabilitySetExistsE(t testing.TestingT, avsName string, resGroupName string, subscriptionID string) (bool, error) {\n\t_, err := GetAvailabilitySetE(t, avsName, resGroupName, subscriptionID)\n\tif err != nil {\n\t\tif ResourceNotFoundErrorExists(err) {\n\t\t\treturn false, nil\n\t\t}\n\t\treturn false, err\n\t}\n\treturn true, nil\n}\n\n// CheckAvailabilitySetContainsVM checks if the Virtual Machine is contained in the Availability Set VMs.\n// This function would fail the test if there is an error.\nfunc CheckAvailabilitySetContainsVM(t testing.TestingT, vmName string, avsName string, resGroupName string, subscriptionID string) bool {\n\tsuccess, err := CheckAvailabilitySetContainsVME(t, vmName, avsName, resGroupName, subscriptionID)\n\trequire.NoError(t, err)\n\treturn success\n}\n\n// CheckAvailabilitySetContainsVME checks if the Virtual Machine is contained in the Availability Set VMs\nfunc CheckAvailabilitySetContainsVME(t testing.TestingT, vmName string, avsName string, resGroupName string, subscriptionID string) (bool, error) {\n\tclient, err := CreateAvailabilitySetClientE(subscriptionID)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\t// Get the Availability Set\n\tavs, err := client.Get(context.Background(), resGroupName, avsName)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\t// Check if the VM is found in the AVS VM collection and return true\n\tfor _, vm := range *avs.VirtualMachines {\n\t\t// VM IDs are always ALL CAPS in this property so ignoring case\n\t\tif strings.EqualFold(vmName, GetNameFromResourceID(*vm.ID)) {\n\t\t\treturn true, nil\n\t\t}\n\t}\n\n\treturn false, NewNotFoundError(\"Virtual Machine\", vmName, avsName)\n}\n\n// GetAvailabilitySetVMNamesInCaps gets a list of VM names in the specified Azure Availability Set.\n// This function would fail the test if there is an error.\nfunc GetAvailabilitySetVMNamesInCaps(t testing.TestingT, avsName string, resGroupName string, subscriptionID string) []string {\n\tvms, err := GetAvailabilitySetVMNamesInCapsE(t, avsName, resGroupName, subscriptionID)\n\trequire.NoError(t, err)\n\treturn vms\n}\n\n// GetAvailabilitySetVMNamesInCapsE gets a list of VM names in the specified Azure Availability Set\nfunc GetAvailabilitySetVMNamesInCapsE(t testing.TestingT, avsName string, resGroupName string, subscriptionID string) ([]string, error) {\n\tclient, err := CreateAvailabilitySetClientE(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tavs, err := client.Get(context.Background(), resGroupName, avsName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvms := []string{}\n\n\t// Get the names for all VMs in the Availability Set\n\tfor _, vm := range *avs.VirtualMachines {\n\t\t// IDs are returned in ALL CAPS for this property\n\t\tif vmName := GetNameFromResourceID(*vm.ID); len(vmName) > 0 {\n\t\t\tvms = append(vms, vmName)\n\t\t}\n\t}\n\n\treturn vms, nil\n}\n\n// GetAvailabilitySetFaultDomainCount gets the Fault Domain Count for the specified Azure Availability Set.\n// This function would fail the test if there is an error.\nfunc GetAvailabilitySetFaultDomainCount(t testing.TestingT, avsName string, resGroupName string, subscriptionID string) int32 {\n\tavsFaultDomainCount, err := GetAvailabilitySetFaultDomainCountE(t, avsName, resGroupName, subscriptionID)\n\trequire.NoError(t, err)\n\treturn avsFaultDomainCount\n}\n\n// GetAvailabilitySetFaultDomainCountE gets the Fault Domain Count for the specified Azure Availability Set\nfunc GetAvailabilitySetFaultDomainCountE(t testing.TestingT, avsName string, resGroupName string, subscriptionID string) (int32, error) {\n\tavs, err := GetAvailabilitySetE(t, avsName, resGroupName, subscriptionID)\n\tif err != nil {\n\t\treturn -1, err\n\t}\n\treturn *avs.PlatformFaultDomainCount, nil\n}\n\n// GetAvailabilitySetE gets an Availability Set in the specified Azure Resource Group\nfunc GetAvailabilitySetE(t testing.TestingT, avsName string, resGroupName string, subscriptionID string) (*compute.AvailabilitySet, error) {\n\t// Validate resource group name and subscription ID\n\tresGroupName, err := getTargetAzureResourceGroupName(resGroupName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Get the client reference\n\tclient, err := CreateAvailabilitySetClientE(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Get the Availability Set\n\tavs, err := client.Get(context.Background(), resGroupName, avsName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &avs, nil\n}\n\n// GetAvailabilitySetClientE gets a new Availability Set client in the specified Azure Subscription\n// TODO: remove in next version\nfunc GetAvailabilitySetClientE(subscriptionID string) (*compute.AvailabilitySetsClient, error) {\n\t// Validate Azure subscription ID\n\tsubscriptionID, err := getTargetAzureSubscription(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Get the Availability Set client\n\tclient := compute.NewAvailabilitySetsClient(subscriptionID)\n\n\t// Create an authorizer\n\tauthorizer, err := NewAuthorizer()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tclient.Authorizer = *authorizer\n\n\treturn &client, nil\n}\n"
  },
  {
    "path": "modules/azure/availabilityset_test.go",
    "content": "//go:build azure\n// +build azure\n\n// NOTE: We use build tags to differentiate azure testing because we currently do not have azure access setup for\n// CircleCI.\n\npackage azure\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n/*\nThe below tests are currently stubbed out, with the expectation that they will throw errors.\nIf/when methods to create and delete network resources are added, these tests can be extended.\n*/\n\nfunc TestCreateAvailabilitySetClientE(t *testing.T) {\n\tt.Parallel()\n\n\tsubscriptionID := \"\"\n\n\tclient, err := CreateAvailabilitySetClientE(subscriptionID)\n\n\trequire.NoError(t, err)\n\tassert.NotEmpty(t, *client)\n}\n\nfunc TestGetAvailabilitySetE(t *testing.T) {\n\tt.Parallel()\n\n\tavsName := \"\"\n\trgName := \"\"\n\tsubscriptionID := \"\"\n\n\t_, err := GetAvailabilitySetE(t, avsName, rgName, subscriptionID)\n\n\trequire.Error(t, err)\n}\n\nfunc TestCheckAvailabilitySetContainsVME(t *testing.T) {\n\tt.Parallel()\n\n\tvmName := \"\"\n\tavsName := \"\"\n\trgName := \"\"\n\tsubscriptionID := \"\"\n\n\t_, err := CheckAvailabilitySetContainsVME(t, vmName, avsName, rgName, subscriptionID)\n\n\trequire.Error(t, err)\n}\n\nfunc TestGetAvailabilitySetVMNamesInCapsE(t *testing.T) {\n\tt.Parallel()\n\n\tavsName := \"\"\n\trgName := \"\"\n\tsubscriptionID := \"\"\n\n\t_, err := GetAvailabilitySetVMNamesInCapsE(t, avsName, rgName, subscriptionID)\n\n\trequire.Error(t, err)\n}\n\nfunc TestGetAvailabilitySetFaultDomainCountE(t *testing.T) {\n\tt.Parallel()\n\n\tavsName := \"\"\n\trgName := \"\"\n\tsubscriptionID := \"\"\n\n\t_, err := GetAvailabilitySetFaultDomainCountE(t, avsName, rgName, subscriptionID)\n\n\trequire.Error(t, err)\n}\n\nfunc TestAvailabilitySetExistsE(t *testing.T) {\n\tt.Parallel()\n\n\tavsName := \"\"\n\trgName := \"\"\n\tsubscriptionID := \"\"\n\n\t_, err := AvailabilitySetExistsE(t, avsName, rgName, subscriptionID)\n\n\trequire.Error(t, err)\n}\n"
  },
  {
    "path": "modules/azure/azure.go",
    "content": "// Package `azure` allows users to interact with resources on the Microsoft Azure platform\n\npackage azure\n"
  },
  {
    "path": "modules/azure/client_factory.go",
    "content": "/*\n\nThis file implements an Azure client factory that automatically handles setting up Base URI\nvalues for sovereign cloud support. Note the list of clients below is not initially exhaustive;\nrather, additional clients will be added as-needed.\n\n*/\n\npackage azure\n\n// snippet-tag-start::client_factory_example.imports\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"reflect\"\n\t\"strings\"\n\n\t\"github.com/Azure/azure-sdk-for-go/profiles/latest/frontdoor/mgmt/frontdoor\"\n\t\"github.com/Azure/azure-sdk-for-go/profiles/latest/privatedns/mgmt/privatedns\"\n\t\"github.com/Azure/azure-sdk-for-go/profiles/latest/resources/mgmt/resources\"\n\t\"github.com/Azure/azure-sdk-for-go/profiles/preview/cosmos-db/mgmt/documentdb\"\n\t\"github.com/Azure/azure-sdk-for-go/profiles/preview/preview/monitor/mgmt/insights\"\n\t\"github.com/Azure/azure-sdk-for-go/sdk/azcore\"\n\t\"github.com/Azure/azure-sdk-for-go/sdk/azcore/arm\"\n\t\"github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud\"\n\t\"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy\"\n\t\"github.com/Azure/azure-sdk-for-go/sdk/azidentity\"\n\t\"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/appcontainers/armappcontainers/v3\"\n\t\"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/appservice/armappservice/v2\"\n\t\"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/datafactory/armdatafactory/v9\"\n\t\"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault\"\n\t\"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/mysql/armmysql\"\n\t\"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/postgresql/armpostgresql\"\n\t\"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources\"\n\t\"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql\"\n\t\"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/synapse/armsynapse\"\n\t\"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2019-07-01/compute\"\n\t\"github.com/Azure/azure-sdk-for-go/services/containerinstance/mgmt/2018-10-01/containerinstance\"\n\t\"github.com/Azure/azure-sdk-for-go/services/containerregistry/mgmt/2019-05-01/containerregistry\"\n\t\"github.com/Azure/azure-sdk-for-go/services/containerservice/mgmt/2019-11-01/containerservice\"\n\t\"github.com/Azure/azure-sdk-for-go/services/network/mgmt/2019-09-01/network\"\n\t\"github.com/Azure/azure-sdk-for-go/services/resources/mgmt/2019-06-01/subscriptions\"\n\t\"github.com/Azure/azure-sdk-for-go/services/storage/mgmt/2019-06-01/storage\"\n\tautorestAzure \"github.com/Azure/go-autorest/autorest/azure\"\n)\n\n// snippet-tag-end::client_factory_example.imports\n\nconst (\n\t// AzureEnvironmentEnvName is the name of the Azure environment to use. Set to one of the following:\n\t//\n\t// \"AzureChinaCloud\":        ChinaCloud\n\t// \"AzureGermanCloud\":       GermanCloud\n\t// \"AzurePublicCloud\":       PublicCloud\n\t// \"AzureUSGovernmentCloud\": USGovernmentCloud\n\t// \"AzureStackCloud\":\t\t Azure stack\n\tAzureEnvironmentEnvName = \"AZURE_ENVIRONMENT\"\n\n\t// ResourceManagerEndpointName is the name of the ResourceManagerEndpoint field in the Environment struct.\n\tResourceManagerEndpointName = \"ResourceManagerEndpoint\"\n)\n\n// ClientType describes the type of client a module can create.\ntype ClientType int\n\n// CreateSubscriptionsClientE returns a virtual machines client instance configured with the correct BaseURI depending on\n// the Azure environment that is currently setup (or \"Public\", if none is setup).\nfunc CreateSubscriptionsClientE() (subscriptions.Client, error) {\n\t// Lookup environment URI\n\tbaseURI, err := getBaseURI()\n\tif err != nil {\n\t\treturn subscriptions.Client{}, err\n\t}\n\n\t// Create correct client based on type passed\n\treturn subscriptions.NewClientWithBaseURI(baseURI), nil\n}\n\n// snippet-tag-start::client_factory_example.CreateClient\n\n// CreateVirtualMachinesClientE returns a virtual machines client instance configured with the correct BaseURI depending on\n// the Azure environment that is currently setup (or \"Public\", if none is setup).\nfunc CreateVirtualMachinesClientE(subscriptionID string) (*compute.VirtualMachinesClient, error) {\n\t// Validate Azure subscription ID\n\tsubscriptionID, err := getTargetAzureSubscription(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Lookup environment URI\n\tbaseURI, err := getBaseURI()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Create correct client based on type passed\n\tvmClient := compute.NewVirtualMachinesClientWithBaseURI(baseURI, subscriptionID)\n\n\treturn &vmClient, nil\n}\n\n// snippet-tag-end::client_factory_example.CreateClient\n\n// CreateManagedClustersClientE returns a virtual machines client instance configured with the correct BaseURI depending on\n// the Azure environment that is currently setup (or \"Public\", if none is setup).\nfunc CreateManagedClustersClientE(subscriptionID string) (containerservice.ManagedClustersClient, error) {\n\t// Validate Azure subscription ID\n\tsubscriptionID, err := getTargetAzureSubscription(subscriptionID)\n\tif err != nil {\n\t\treturn containerservice.ManagedClustersClient{}, err\n\t}\n\n\t// Lookup environment URI\n\tbaseURI, err := getBaseURI()\n\tif err != nil {\n\t\treturn containerservice.ManagedClustersClient{}, err\n\t}\n\n\t// Create correct client based on type passed\n\treturn containerservice.NewManagedClustersClientWithBaseURI(baseURI, subscriptionID), nil\n}\n\n// CreateCosmosDBAccountClientE is a helper function that will setup a CosmosDB account client with the correct BaseURI depending on\n// the Azure environment that is currently setup (or \"Public\", if none is setup).\nfunc CreateCosmosDBAccountClientE(subscriptionID string) (*documentdb.DatabaseAccountsClient, error) {\n\t// Validate Azure subscription ID\n\tsubscriptionID, err := getTargetAzureSubscription(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Lookup environment URI\n\tbaseURI, err := getBaseURI()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Create a CosmosDB client\n\tcosmosClient := documentdb.NewDatabaseAccountsClientWithBaseURI(baseURI, subscriptionID)\n\n\treturn &cosmosClient, nil\n}\n\n// CreateCosmosDBSQLClientE is a helper function that will setup a CosmosDB SQL client with the correct BaseURI depending on\n// the Azure environment that is currently setup (or \"Public\", if none is setup).\nfunc CreateCosmosDBSQLClientE(subscriptionID string) (*documentdb.SQLResourcesClient, error) {\n\t// Validate Azure subscription ID\n\tsubscriptionID, err := getTargetAzureSubscription(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Lookup environment URI\n\tbaseURI, err := getBaseURI()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Create a CosmosDB client\n\tcosmosClient := documentdb.NewSQLResourcesClientWithBaseURI(baseURI, subscriptionID)\n\n\treturn &cosmosClient, nil\n}\n\n// getArmKeyVaultClientFactory gets an arm keyvault client factory\nfunc getArmKeyVaultClientFactory(subscriptionID string) (*armkeyvault.ClientFactory, error) {\n\ttargetSubscriptionID, err := getTargetAzureSubscription(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tclientCloudConfig, err := getClientCloudConfig()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tcred, err := azidentity.NewDefaultAzureCredential(&azidentity.DefaultAzureCredentialOptions{\n\t\tClientOptions: azcore.ClientOptions{\n\t\t\tCloud: clientCloudConfig,\n\t\t},\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn armkeyvault.NewClientFactory(targetSubscriptionID, cred, &arm.ClientOptions{\n\t\tClientOptions: policy.ClientOptions{\n\t\t\tCloud: clientCloudConfig,\n\t\t},\n\t})\n}\n\n// getArmPostgreSQLClientFactory gets an arm postgresql client factory\nfunc getArmPostgreSQLClientFactory(subscriptionID string) (*armpostgresql.ClientFactory, error) {\n\ttargetSubscriptionID, err := getTargetAzureSubscription(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tclientCloudConfig, err := getClientCloudConfig()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tcred, err := azidentity.NewDefaultAzureCredential(&azidentity.DefaultAzureCredentialOptions{\n\t\tClientOptions: azcore.ClientOptions{\n\t\t\tCloud: clientCloudConfig,\n\t\t},\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn armpostgresql.NewClientFactory(targetSubscriptionID, cred, &arm.ClientOptions{\n\t\tClientOptions: policy.ClientOptions{\n\t\t\tCloud: clientCloudConfig,\n\t\t},\n\t})\n}\n\n// CreateStorageAccountClientE creates a storage account client.\nfunc CreateStorageAccountClientE(subscriptionID string) (*storage.AccountsClient, error) {\n\t// Validate Azure subscription ID\n\tsubscriptionID, err := getTargetAzureSubscription(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Lookup environment URI\n\tbaseURI, err := getBaseURI()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tstorageAccountClient := storage.NewAccountsClientWithBaseURI(baseURI, subscriptionID)\n\tauthorizer, err := NewAuthorizer()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tstorageAccountClient.Authorizer = *authorizer\n\treturn &storageAccountClient, nil\n}\n\n// CreateStorageBlobContainerClientE creates a storage container client.\nfunc CreateStorageBlobContainerClientE(subscriptionID string) (*storage.BlobContainersClient, error) {\n\tsubscriptionID, err := getTargetAzureSubscription(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Lookup environment URI\n\tbaseURI, err := getBaseURI()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tblobContainerClient := storage.NewBlobContainersClientWithBaseURI(baseURI, subscriptionID)\n\tauthorizer, err := NewAuthorizer()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tblobContainerClient.Authorizer = *authorizer\n\treturn &blobContainerClient, nil\n}\n\n// CreateStorageFileSharesClientE creates a storage file share client.\nfunc CreateStorageFileSharesClientE(subscriptionID string) (*storage.FileSharesClient, error) {\n\tsubscriptionID, err := getTargetAzureSubscription(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Lookup environment URI\n\tbaseURI, err := getBaseURI()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfileShareClient := storage.NewFileSharesClientWithBaseURI(baseURI, subscriptionID)\n\tauthorizer, err := NewAuthorizer()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tfileShareClient.Authorizer = *authorizer\n\treturn &fileShareClient, nil\n}\n\n// CreateAvailabilitySetClientE creates a new Availability Set client in the specified Azure Subscription\nfunc CreateAvailabilitySetClientE(subscriptionID string) (*compute.AvailabilitySetsClient, error) {\n\t// Validate Azure subscription ID\n\tsubscriptionID, err := getTargetAzureSubscription(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Lookup environment URI\n\tbaseURI, err := getBaseURI()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Get the Availability Set client\n\tclient := compute.NewAvailabilitySetsClientWithBaseURI(baseURI, subscriptionID)\n\n\t// Create an authorizer\n\tauthorizer, err := NewAuthorizer()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tclient.Authorizer = *authorizer\n\n\treturn &client, nil\n}\n\n// CreateResourceGroupClientE gets a resource group client in a subscription\nfunc CreateResourceGroupClientE(subscriptionID string) (*resources.GroupsClient, error) {\n\tsubscriptionID, err := getTargetAzureSubscription(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Lookup environment URI\n\tbaseURI, err := getBaseURI()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresourceGroupClient := resources.NewGroupsClientWithBaseURI(baseURI, subscriptionID)\n\n\tauthorizer, err := NewAuthorizer()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tresourceGroupClient.Authorizer = *authorizer\n\treturn &resourceGroupClient, nil\n}\n\n// CreateSQLServerClient is a helper function that will create and setup a sql server client\nfunc CreateSQLServerClient(subscriptionID string) (*armsql.ServersClient, error) {\n\tclientFactory, err := getArmSQLClientFactory(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn clientFactory.NewServersClient(), nil\n}\n\n// CreateSQLMangedInstanceClient is a helper function that will create and setup a sql managed instance client\nfunc CreateSQLMangedInstanceClient(subscriptionID string) (*armsql.ManagedInstancesClient, error) {\n\tclientFactory, err := getArmSQLClientFactory(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn clientFactory.NewManagedInstancesClient(), nil\n}\n\n// CreateSQLMangedDatabasesClient is a helper function that will create and setup a sql managed databases client\nfunc CreateSQLMangedDatabasesClient(subscriptionID string) (*armsql.ManagedDatabasesClient, error) {\n\tclientFactory, err := getArmSQLClientFactory(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn clientFactory.NewManagedDatabasesClient(), nil\n}\n\n// getArmSQLClientFactory gets an arm sql client factory\nfunc getArmSQLClientFactory(subscriptionID string) (*armsql.ClientFactory, error) {\n\ttargetSubscriptionID, err := getTargetAzureSubscription(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tclientCloudConfig, err := getClientCloudConfig()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tcred, err := azidentity.NewDefaultAzureCredential(&azidentity.DefaultAzureCredentialOptions{\n\t\tClientOptions: azcore.ClientOptions{\n\t\t\tCloud: clientCloudConfig,\n\t\t},\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn armsql.NewClientFactory(targetSubscriptionID, cred, &arm.ClientOptions{\n\t\tClientOptions: policy.ClientOptions{\n\t\t\tCloud: clientCloudConfig,\n\t\t},\n\t})\n}\n\n// CreateDatabaseClient is a helper function that will create and setup a SQL DB client\nfunc CreateDatabaseClient(subscriptionID string) (*armsql.DatabasesClient, error) {\n\tclientFactory, err := getArmSQLClientFactory(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn clientFactory.NewDatabasesClient(), nil\n}\n\n// CreateMySQLServerClientE is a helper function that will setup a mysql server client.\nfunc CreateMySQLServerClientE(subscriptionID string) (*armmysql.ServersClient, error) {\n\tclientFactory, err := getArmMySQLClientFactory(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn clientFactory.NewServersClient(), nil\n}\n\n// getArmMySQLClientFactory gets an arm mysql client factory\nfunc getArmMySQLClientFactory(subscriptionID string) (*armmysql.ClientFactory, error) {\n\ttargetSubscriptionID, err := getTargetAzureSubscription(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tclientCloudConfig, err := getClientCloudConfig()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tcred, err := azidentity.NewDefaultAzureCredential(&azidentity.DefaultAzureCredentialOptions{\n\t\tClientOptions: azcore.ClientOptions{\n\t\t\tCloud: clientCloudConfig,\n\t\t},\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn armmysql.NewClientFactory(targetSubscriptionID, cred, &arm.ClientOptions{\n\t\tClientOptions: policy.ClientOptions{\n\t\t\tCloud: clientCloudConfig,\n\t\t},\n\t})\n}\n\n// CreateDisksClientE returns a new Disks client in the specified Azure Subscription\nfunc CreateDisksClientE(subscriptionID string) (*compute.DisksClient, error) {\n\t// Validate Azure subscription ID\n\tsubscriptionID, err := getTargetAzureSubscription(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Lookup environment URI\n\tbaseURI, err := getBaseURI()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Get the Disks client\n\tclient := compute.NewDisksClientWithBaseURI(baseURI, subscriptionID)\n\n\t// Create an authorizer\n\tauthorizer, err := NewAuthorizer()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tclient.Authorizer = *authorizer\n\n\treturn &client, nil\n}\n\nfunc CreateActionGroupClient(subscriptionID string) (*insights.ActionGroupsClient, error) {\n\tsubID, err := getTargetAzureSubscription(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Lookup environment URI\n\tbaseURI, err := getBaseURI()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tmetricAlertsClient := insights.NewActionGroupsClientWithBaseURI(baseURI, subID)\n\n\tauthorizer, err := NewAuthorizer()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tmetricAlertsClient.Authorizer = *authorizer\n\n\treturn &metricAlertsClient, nil\n}\n\n// CreateVMInsightsClientE gets a VM Insights client\nfunc CreateVMInsightsClientE(subscriptionID string) (*insights.VMInsightsClient, error) {\n\t// Validate Azure subscription ID\n\tsubscriptionID, err := getTargetAzureSubscription(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Lookup environment URI\n\tbaseURI, err := getBaseURI()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tclient := insights.NewVMInsightsClientWithBaseURI(baseURI, subscriptionID)\n\n\tauthorizer, err := NewAuthorizer()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tclient.Authorizer = *authorizer\n\n\treturn &client, nil\n}\n\n// CreateActivityLogAlertsClientE gets an Action Groups client in the specified Azure Subscription\nfunc CreateActivityLogAlertsClientE(subscriptionID string) (*insights.ActivityLogAlertsClient, error) {\n\t// Validate Azure subscription ID\n\tsubscriptionID, err := getTargetAzureSubscription(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Lookup environment URI\n\tbaseURI, err := getBaseURI()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Get the Action Groups client\n\tclient := insights.NewActivityLogAlertsClientWithBaseURI(baseURI, subscriptionID)\n\n\t// Create an authorizer\n\tauthorizer, err := NewAuthorizer()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tclient.Authorizer = *authorizer\n\n\treturn &client, nil\n}\n\n// CreateDiagnosticsSettingsClientE returns a diagnostics settings client\nfunc CreateDiagnosticsSettingsClientE(subscriptionID string) (*insights.DiagnosticSettingsClient, error) {\n\t// Validate Azure subscription ID\n\tsubscriptionID, err := getTargetAzureSubscription(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Lookup environment URI\n\tbaseURI, err := getBaseURI()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tclient := insights.NewDiagnosticSettingsClientWithBaseURI(baseURI, subscriptionID)\n\n\tauthorizer, err := NewAuthorizer()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tclient.Authorizer = *authorizer\n\n\treturn &client, nil\n}\n\n// CreateNsgDefaultRulesClientE returns an NSG default (platform) rules client instance configured with the\n// correct BaseURI depending on the Azure environment that is currently setup (or \"Public\", if none is setup).\nfunc CreateNsgDefaultRulesClientE(subscriptionID string) (*network.DefaultSecurityRulesClient, error) {\n\t// Validate Azure subscription ID\n\tsubscriptionID, err := getTargetAzureSubscription(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Lookup environment URI\n\tbaseURI, err := getEnvironmentEndpointE(ResourceManagerEndpointName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Create new client\n\tnsgClient := network.NewDefaultSecurityRulesClientWithBaseURI(baseURI, subscriptionID)\n\treturn &nsgClient, nil\n}\n\n// CreateNsgCustomRulesClientE returns an NSG custom (user) rules client instance configured with the\n// correct BaseURI depending on the Azure environment that is currently setup (or \"Public\", if none is setup).\nfunc CreateNsgCustomRulesClientE(subscriptionID string) (*network.SecurityRulesClient, error) {\n\t// Validate Azure subscription ID\n\tsubscriptionID, err := getTargetAzureSubscription(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Lookup environment URI\n\tbaseURI, err := getEnvironmentEndpointE(ResourceManagerEndpointName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Create new client\n\tnsgClient := network.NewSecurityRulesClientWithBaseURI(baseURI, subscriptionID)\n\treturn &nsgClient, nil\n}\n\n// CreateNewNetworkInterfacesClientE returns an NIC client instance configured with the\n// correct BaseURI depending on the Azure environment that is currently setup (or \"Public\", if none is setup).\nfunc CreateNewNetworkInterfacesClientE(subscriptionID string) (*network.InterfacesClient, error) {\n\t// Validate Azure subscription ID\n\tsubscriptionID, err := getTargetAzureSubscription(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Lookup environment URI\n\tbaseURI, err := getEnvironmentEndpointE(ResourceManagerEndpointName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// create client\n\tnicClient := network.NewInterfacesClientWithBaseURI(baseURI, subscriptionID)\n\treturn &nicClient, nil\n}\n\n// CreateNewNetworkInterfaceIPConfigurationClientE returns an NIC IP configuration client instance configured with the\n// correct BaseURI depending on the Azure environment that is currently setup (or \"Public\", if none is setup).\nfunc CreateNewNetworkInterfaceIPConfigurationClientE(subscriptionID string) (*network.InterfaceIPConfigurationsClient, error) {\n\t// Validate Azure subscription ID\n\tsubscriptionID, err := getTargetAzureSubscription(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Lookup environment URI\n\tbaseURI, err := getEnvironmentEndpointE(ResourceManagerEndpointName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// create client\n\tipConfigClient := network.NewInterfaceIPConfigurationsClientWithBaseURI(baseURI, subscriptionID)\n\treturn &ipConfigClient, nil\n}\n\n// CreatePublicIPAddressesClientE returns a public IP address client instance configured with the correct BaseURI depending on\n// the Azure environment that is currently setup (or \"Public\", if none is setup).\nfunc CreatePublicIPAddressesClientE(subscriptionID string) (*network.PublicIPAddressesClient, error) {\n\t// Validate Azure subscription ID\n\tsubscriptionID, err := getTargetAzureSubscription(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Lookup environment URI\n\tbaseURI, err := getEnvironmentEndpointE(ResourceManagerEndpointName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Create client\n\tclient := network.NewPublicIPAddressesClientWithBaseURI(baseURI, subscriptionID)\n\treturn &client, nil\n}\n\n// CreateLoadBalancerClientE returns a load balancer client instance configured with the correct BaseURI depending on\n// the Azure environment that is currently setup (or \"Public\", if none is setup).\nfunc CreateLoadBalancerClientE(subscriptionID string) (*network.LoadBalancersClient, error) {\n\t// Validate Azure subscription ID\n\tsubscriptionID, err := getTargetAzureSubscription(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Lookup environment URI\n\tbaseURI, err := getEnvironmentEndpointE(ResourceManagerEndpointName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// create LB client\n\tclient := network.NewLoadBalancersClientWithBaseURI(baseURI, subscriptionID)\n\treturn &client, nil\n}\n\n// CreateNewSubnetClientE returns a Subnet client instance configured with the\n// correct BaseURI depending on the Azure environment that is currently setup (or \"Public\", if none is setup).\nfunc CreateNewSubnetClientE(subscriptionID string) (*network.SubnetsClient, error) {\n\t// Validate Azure subscription ID\n\tsubscriptionID, err := getTargetAzureSubscription(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Lookup environment URI\n\tbaseURI, err := getEnvironmentEndpointE(ResourceManagerEndpointName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// create client\n\tsubnetClient := network.NewSubnetsClientWithBaseURI(baseURI, subscriptionID)\n\treturn &subnetClient, nil\n}\n\n// CreateNewVirtualNetworkClientE returns a Virtual Network client instance configured with the\n// correct BaseURI depending on the Azure environment that is currently setup (or \"Public\", if none is setup).\nfunc CreateNewVirtualNetworkClientE(subscriptionID string) (*network.VirtualNetworksClient, error) {\n\t// Validate Azure subscription ID\n\tsubscriptionID, err := getTargetAzureSubscription(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Lookup environment URI\n\tbaseURI, err := getEnvironmentEndpointE(ResourceManagerEndpointName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// create client\n\tvnetClient := network.NewVirtualNetworksClientWithBaseURI(baseURI, subscriptionID)\n\treturn &vnetClient, nil\n}\n\n// CreateAppServiceClientE returns an App service client instance configured with the\n// correct BaseURI depending on the Azure environment that is currently setup (or \"Public\", if none is setup).\nfunc CreateAppServiceClientE(subscriptionID string) (*armappservice.WebAppsClient, error) {\n\tclientFactory, err := getArmAppServiceClientFactory(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn clientFactory.NewWebAppsClient(), nil\n}\n\n// getArmAppServiceClientFactory gets an arm app service client factory\nfunc getArmAppServiceClientFactory(subscriptionID string) (*armappservice.ClientFactory, error) {\n\ttargetSubscriptionID, err := getTargetAzureSubscription(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tclientCloudConfig, err := getClientCloudConfig()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tcred, err := azidentity.NewDefaultAzureCredential(&azidentity.DefaultAzureCredentialOptions{\n\t\tClientOptions: azcore.ClientOptions{\n\t\t\tCloud: clientCloudConfig,\n\t\t},\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn armappservice.NewClientFactory(targetSubscriptionID, cred, &arm.ClientOptions{\n\t\tClientOptions: policy.ClientOptions{\n\t\t\tCloud: clientCloudConfig,\n\t\t},\n\t})\n}\n\n// CreateContainerRegistryClientE returns an ACR client instance configured with the\n// correct BaseURI depending on the Azure environment that is currently setup (or \"Public\", if none is setup).\nfunc CreateContainerRegistryClientE(subscriptionID string) (*containerregistry.RegistriesClient, error) {\n\t// Validate Azure subscription ID\n\tsubscriptionID, err := getTargetAzureSubscription(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Lookup environment URI\n\tbaseURI, err := getEnvironmentEndpointE(ResourceManagerEndpointName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// create client\n\tregistryClient := containerregistry.NewRegistriesClientWithBaseURI(baseURI, subscriptionID)\n\treturn &registryClient, nil\n}\n\n// CreateContainerInstanceClientE returns an ACI client instance configured with the\n// correct BaseURI depending on the Azure environment that is currently setup (or \"Public\", if none is setup).\nfunc CreateContainerInstanceClientE(subscriptionID string) (*containerinstance.ContainerGroupsClient, error) {\n\t// Validate Azure subscription ID\n\tsubscriptionID, err := getTargetAzureSubscription(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Lookup environment URI\n\tbaseURI, err := getEnvironmentEndpointE(ResourceManagerEndpointName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// create client\n\tinstanceClient := containerinstance.NewContainerGroupsClientWithBaseURI(baseURI, subscriptionID)\n\treturn &instanceClient, nil\n}\n\n// CreateFrontDoorClientE returns an AFD client instance configured with the\n// correct BaseURI depending on the Azure environment that is currently setup (or \"Public\", if none is setup).\nfunc CreateFrontDoorClientE(subscriptionID string) (*frontdoor.FrontDoorsClient, error) {\n\t// Validate Azure subscription ID\n\tsubscriptionID, err := getTargetAzureSubscription(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Lookup environment URI\n\tbaseURI, err := getEnvironmentEndpointE(ResourceManagerEndpointName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// create client\n\tclient := frontdoor.NewFrontDoorsClientWithBaseURI(baseURI, subscriptionID)\n\treturn &client, nil\n}\n\n// CreateFrontDoorFrontendEndpointClientE returns an AFD Frontend Endpoints client instance configured with the\n// correct BaseURI depending on the Azure environment that is currently setup (or \"Public\", if none is setup).\nfunc CreateFrontDoorFrontendEndpointClientE(subscriptionID string) (*frontdoor.FrontendEndpointsClient, error) {\n\t// Validate Azure subscription ID\n\tsubscriptionID, err := getTargetAzureSubscription(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Lookup environment URI\n\tbaseURI, err := getEnvironmentEndpointE(ResourceManagerEndpointName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// create client\n\tclient := frontdoor.NewFrontendEndpointsClientWithBaseURI(baseURI, subscriptionID)\n\treturn &client, nil\n}\n\n// CreateSynapseWorkspaceClientE is a helper function that will setup a synapse workspace client.\nfunc CreateSynapseWorkspaceClientE(subscriptionID string) (*armsynapse.WorkspacesClient, error) {\n\tclientFactory, err := getArmSynapseClientFactory(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn clientFactory.NewWorkspacesClient(), nil\n}\n\n// CreateSynapseSqlPoolClientE is a helper function that will setup a synapse sql pool client.\nfunc CreateSynapseSqlPoolClientE(subscriptionID string) (*armsynapse.SQLPoolsClient, error) {\n\tclientFactory, err := getArmSynapseClientFactory(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn clientFactory.NewSQLPoolsClient(), nil\n}\n\n// getArmSynapseClientFactory gets an arm synapse client factory\nfunc getArmSynapseClientFactory(subscriptionID string) (*armsynapse.ClientFactory, error) {\n\ttargetSubscriptionID, err := getTargetAzureSubscription(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tclientCloudConfig, err := getClientCloudConfig()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tcred, err := azidentity.NewDefaultAzureCredential(&azidentity.DefaultAzureCredentialOptions{\n\t\tClientOptions: azcore.ClientOptions{\n\t\t\tCloud: clientCloudConfig,\n\t\t},\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn armsynapse.NewClientFactory(targetSubscriptionID, cred, &arm.ClientOptions{\n\t\tClientOptions: policy.ClientOptions{\n\t\t\tCloud: clientCloudConfig,\n\t\t},\n\t})\n}\n\n// CreateDataFactoriesClientE is a helper function that will setup a data factory client.\nfunc CreateDataFactoriesClientE(subscriptionID string) (*armdatafactory.FactoriesClient, error) {\n\tclientFactory, err := getArmDataFactoryClientFactory(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn clientFactory.NewFactoriesClient(), nil\n}\n\n// CreatePrivateDnsZonesClientE is a helper function that will setup a private DNS zone client.\nfunc CreatePrivateDnsZonesClientE(subscriptionID string) (*privatedns.PrivateZonesClient, error) {\n\t// Validate Azure subscription ID\n\tsubID, err := getTargetAzureSubscription(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Lookup environment URI\n\tbaseURI, err := getBaseURI()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Create a private DNS zone client\n\tprivateZonesClient := privatedns.NewPrivateZonesClientWithBaseURI(baseURI, subID)\n\n\t// Create an authorizer\n\tauthorizer, err := NewAuthorizer()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Attach authorizer to the client\n\tprivateZonesClient.Authorizer = *authorizer\n\n\treturn &privateZonesClient, nil\n}\n\nfunc CreateManagedEnvironmentsClientE(subscriptionID string) (*armappcontainers.ManagedEnvironmentsClient, error) {\n\tclientFactory, err := getArmAppContainersClientFactory(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tclient := clientFactory.NewManagedEnvironmentsClient()\n\treturn client, nil\n}\n\nfunc CreateResourceGroupClientV2E(subscriptionID string) (*armresources.ResourceGroupsClient, error) {\n\tclientFactory, err := getArmResourcesClientFactory(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn clientFactory.NewResourceGroupsClient(), nil\n}\n\nfunc CreateContainerAppsClientE(subscriptionID string) (*armappcontainers.ContainerAppsClient, error) {\n\tclientFactory, err := getArmAppContainersClientFactory(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tclient := clientFactory.NewContainerAppsClient()\n\treturn client, nil\n}\n\nfunc CreateContainerAppJobsClientE(subscriptionID string) (*armappcontainers.JobsClient, error) {\n\tclientFactory, err := getArmAppContainersClientFactory(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tclient := clientFactory.NewJobsClient()\n\treturn client, nil\n}\n\n// GetKeyVaultURISuffixE returns the proper KeyVault URI suffix for the configured Azure environment.\n// This function would fail the test if there is an error.\nfunc GetKeyVaultURISuffixE() (string, error) {\n\tenvName := getDefaultEnvironmentName()\n\tenv, err := autorestAzure.EnvironmentFromName(envName)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn env.KeyVaultDNSSuffix, nil\n}\n\n// getDefaultEnvironmentName returns either a configured Azure environment name, or the public default\nfunc getDefaultEnvironmentName() string {\n\tenvName, exists := os.LookupEnv(AzureEnvironmentEnvName)\n\n\tif exists && len(envName) > 0 {\n\t\treturn envName\n\t}\n\n\treturn autorestAzure.PublicCloud.Name\n}\n\n// getEnvironmentEndpointE returns the endpoint identified by the endpoint name parameter.\nfunc getEnvironmentEndpointE(endpointName string) (string, error) {\n\tenvName := getDefaultEnvironmentName()\n\tenv, err := autorestAzure.EnvironmentFromName(envName)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn getFieldValue(&env, endpointName), nil\n}\n\n// getFieldValue gets the field identified by the field parameter from the passed Environment struct\nfunc getFieldValue(env *autorestAzure.Environment, field string) string {\n\tstructValue := reflect.ValueOf(env)\n\tfieldVal := reflect.Indirect(structValue).FieldByName(field)\n\treturn fieldVal.String()\n}\n\n// getBaseURI gets the base URI endpoint.\nfunc getBaseURI() (string, error) {\n\t// Lookup environment URI\n\tbaseURI, err := getEnvironmentEndpointE(ResourceManagerEndpointName)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn baseURI, nil\n}\n\n// getArmResourcesClientFactory gets an arm resources client factory\nfunc getArmResourcesClientFactory(subscriptionID string) (*armresources.ClientFactory, error) {\n\ttargetSubscriptionID, err := getTargetAzureSubscription(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tclientCloudConfig, err := getClientCloudConfig()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tcred, err := azidentity.NewDefaultAzureCredential(&azidentity.DefaultAzureCredentialOptions{\n\t\tClientOptions: azcore.ClientOptions{\n\t\t\tCloud: clientCloudConfig,\n\t\t},\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn armresources.NewClientFactory(targetSubscriptionID, cred, &arm.ClientOptions{\n\t\tClientOptions: policy.ClientOptions{\n\t\t\tCloud: clientCloudConfig,\n\t\t},\n\t})\n}\n\n// getArmAppContainersClientFactory gets an arm app containers client factory\nfunc getArmAppContainersClientFactory(subscriptionID string) (*armappcontainers.ClientFactory, error) {\n\ttargetSubscriptionID, err := getTargetAzureSubscription(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tclientCloudConfig, err := getClientCloudConfig()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tcred, err := azidentity.NewDefaultAzureCredential(&azidentity.DefaultAzureCredentialOptions{\n\t\tClientOptions: azcore.ClientOptions{\n\t\t\tCloud: clientCloudConfig,\n\t\t},\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn armappcontainers.NewClientFactory(targetSubscriptionID, cred, &arm.ClientOptions{\n\t\tClientOptions: policy.ClientOptions{\n\t\t\tCloud: clientCloudConfig,\n\t\t},\n\t})\n}\n\n// getArmDataFactoryClientFactory gets an arm data factory client factory\nfunc getArmDataFactoryClientFactory(subscriptionID string) (*armdatafactory.ClientFactory, error) {\n\ttargetSubscriptionID, err := getTargetAzureSubscription(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tclientCloudConfig, err := getClientCloudConfig()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tcred, err := azidentity.NewDefaultAzureCredential(&azidentity.DefaultAzureCredentialOptions{\n\t\tClientOptions: azcore.ClientOptions{\n\t\t\tCloud: clientCloudConfig,\n\t\t},\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn armdatafactory.NewClientFactory(targetSubscriptionID, cred, &arm.ClientOptions{\n\t\tClientOptions: policy.ClientOptions{\n\t\t\tCloud: clientCloudConfig,\n\t\t},\n\t})\n}\n\nfunc getClientCloudConfig() (cloud.Configuration, error) {\n\tenvName := getDefaultEnvironmentName()\n\tswitch strings.ToUpper(envName) {\n\tcase \"AZURECHINACLOUD\":\n\t\treturn cloud.AzureChina, nil\n\tcase \"AZUREUSGOVERNMENTCLOUD\":\n\t\treturn cloud.AzureGovernment, nil\n\tcase \"AZUREPUBLICCLOUD\":\n\t\treturn cloud.AzurePublic, nil\n\tcase \"AZURESTACKCLOUD\":\n\t\tenv, err := autorestAzure.EnvironmentFromName(envName)\n\t\tif err != nil {\n\t\t\treturn cloud.Configuration{}, err\n\t\t}\n\t\tc := cloud.Configuration{\n\t\t\tActiveDirectoryAuthorityHost: env.ActiveDirectoryEndpoint,\n\t\t\tServices: map[cloud.ServiceName]cloud.ServiceConfiguration{\n\t\t\t\tcloud.ResourceManager: {\n\t\t\t\t\tAudience: env.TokenAudience,\n\t\t\t\t\tEndpoint: env.ResourceManagerEndpoint,\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\t\treturn c, nil\n\tdefault:\n\t\treturn cloud.Configuration{},\n\t\t\tfmt.Errorf(\"no cloud environment matching the name: %s. \"+\n\t\t\t\t\"Available values are: \"+\n\t\t\t\t\"AzurePublicCloud (default), \"+\n\t\t\t\t\"AzureUSGovernmentCloud, \"+\n\t\t\t\t\"AzureChinaCloud or \"+\n\t\t\t\t\"AzureStackCloud\",\n\t\t\t\tenvName)\n\t}\n}\n"
  },
  {
    "path": "modules/azure/client_factory_test.go",
    "content": "//go:build azure\n// +build azure\n\n// This file contains unit tests for the client factory implementation(s).\n\npackage azure\n\nimport (\n\t\"os\"\n\t\"reflect\"\n\t\"testing\"\n\n\tautorest \"github.com/Azure/go-autorest/autorest/azure\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// Local consts for this file only\nconst govCloudEnvName = \"AzureUSGovernmentCloud\"\nconst publicCloudEnvName = \"AzurePublicCloud\"\nconst chinaCloudEnvName = \"AzureChinaCloud\"\nconst germanyCloudEnvName = \"AzureGermanCloud\"\n\nfunc TestDefaultEnvIsPublicWhenNotSet(t *testing.T) {\n\t// save any current env value and restore on exit\n\toriginalEnv := os.Getenv(AzureEnvironmentEnvName)\n\tdefer os.Setenv(AzureEnvironmentEnvName, originalEnv)\n\n\t// Set env var to missing value\n\tos.Setenv(AzureEnvironmentEnvName, \"\")\n\n\t// get the default\n\tenv := getDefaultEnvironmentName()\n\n\t// Make sure it's public cloud\n\tassert.Equal(t, autorest.PublicCloud.Name, env)\n}\n\nfunc TestDefaultEnvSetToGov(t *testing.T) {\n\t// save any current env value and restore on exit\n\toriginalEnv := os.Getenv(AzureEnvironmentEnvName)\n\tdefer os.Setenv(AzureEnvironmentEnvName, originalEnv)\n\n\t// Set env var to gov\n\tos.Setenv(AzureEnvironmentEnvName, govCloudEnvName)\n\n\t// get the default\n\tenv := getDefaultEnvironmentName()\n\n\t// Make sure it's public cloud\n\tassert.Equal(t, autorest.USGovernmentCloud.Name, env)\n}\n\nfunc TestSubscriptionClientBaseURISetCorrectly(t *testing.T) {\n\tvar cases = []struct {\n\t\tCaseName        string\n\t\tEnvironmentName string\n\t\tExpectedBaseURI string\n\t}{\n\t\t{\"GovCloud/SubscriptionClient\", govCloudEnvName, autorest.USGovernmentCloud.ResourceManagerEndpoint},\n\t\t{\"PublicCloud/SubscriptionClient\", publicCloudEnvName, autorest.PublicCloud.ResourceManagerEndpoint},\n\t\t{\"ChinaCloud/SubscriptionClient\", chinaCloudEnvName, autorest.ChinaCloud.ResourceManagerEndpoint},\n\t\t{\"GermanCloud/SubscriptionClient\", germanyCloudEnvName, autorest.GermanCloud.ResourceManagerEndpoint},\n\t}\n\n\t// save any current env value and restore on exit\n\tcurrentEnv := os.Getenv(AzureEnvironmentEnvName)\n\tdefer os.Setenv(AzureEnvironmentEnvName, currentEnv)\n\n\tfor _, tt := range cases {\n\t\t// The following is necessary to make sure testCase's values don't\n\t\t// get updated due to concurrency within the scope of t.Run(..) below\n\t\ttt := tt\n\t\tt.Run(tt.CaseName, func(t *testing.T) {\n\t\t\t// Override env setting\n\t\t\tos.Setenv(AzureEnvironmentEnvName, tt.EnvironmentName)\n\n\t\t\t// Get a VM client\n\t\t\tclient, err := CreateSubscriptionsClientE()\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// Check for correct ARM URI\n\t\t\tassert.Equal(t, tt.ExpectedBaseURI, client.BaseURI)\n\t\t})\n\t}\n}\n\n// snippet-tag-start::client_factory_example.UnitTest\n\nfunc TestVMClientBaseURISetCorrectly(t *testing.T) {\n\tvar cases = []struct {\n\t\tCaseName        string\n\t\tEnvironmentName string\n\t\tExpectedBaseURI string\n\t}{\n\t\t{\"GovCloud/VMClient\", govCloudEnvName, autorest.USGovernmentCloud.ResourceManagerEndpoint},\n\t\t{\"PublicCloud/VMClient\", publicCloudEnvName, autorest.PublicCloud.ResourceManagerEndpoint},\n\t\t{\"ChinaCloud/VMClient\", chinaCloudEnvName, autorest.ChinaCloud.ResourceManagerEndpoint},\n\t\t{\"GermanCloud/VMClient\", germanyCloudEnvName, autorest.GermanCloud.ResourceManagerEndpoint},\n\t}\n\n\t// save any current env value and restore on exit\n\tcurrentEnv := os.Getenv(AzureEnvironmentEnvName)\n\tdefer os.Setenv(AzureEnvironmentEnvName, currentEnv)\n\n\tfor _, tt := range cases {\n\t\t// The following is necessary to make sure testCase's values don't\n\t\t// get updated due to concurrency within the scope of t.Run(..) below\n\t\ttt := tt\n\t\tt.Run(tt.CaseName, func(t *testing.T) {\n\t\t\t// Override env setting\n\t\t\tos.Setenv(AzureEnvironmentEnvName, tt.EnvironmentName)\n\n\t\t\t// Get a VM client\n\t\t\tclient, err := CreateVirtualMachinesClientE(\"\")\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// Check for correct ARM URI\n\t\t\tassert.Equal(t, tt.ExpectedBaseURI, client.BaseURI)\n\t\t})\n\t}\n}\n\n// snippet-tag-end::client_factory_example.UnitTest\n\nfunc TestManagedClustersClientBaseURISetCorrectly(t *testing.T) {\n\tvar cases = []struct {\n\t\tCaseName        string\n\t\tEnvironmentName string\n\t\tExpectedBaseURI string\n\t}{\n\t\t{\"GovCloud/ManagedClustersClient\", govCloudEnvName, autorest.USGovernmentCloud.ResourceManagerEndpoint},\n\t\t{\"PublicCloud/ManagedClustersClient\", publicCloudEnvName, autorest.PublicCloud.ResourceManagerEndpoint},\n\t\t{\"ChinaCloud/ManagedClustersClient\", chinaCloudEnvName, autorest.ChinaCloud.ResourceManagerEndpoint},\n\t\t{\"GermanCloud/ManagedClustersClient\", germanyCloudEnvName, autorest.GermanCloud.ResourceManagerEndpoint},\n\t}\n\n\t// save any current env value and restore on exit\n\tcurrentEnv := os.Getenv(AzureEnvironmentEnvName)\n\tdefer os.Setenv(AzureEnvironmentEnvName, currentEnv)\n\n\tfor _, tt := range cases {\n\t\t// The following is necessary to make sure testCase's values don't\n\t\t// get updated due to concurrency within the scope of t.Run(..) below\n\t\ttt := tt\n\t\tt.Run(tt.CaseName, func(t *testing.T) {\n\t\t\t// Override env setting\n\t\t\tos.Setenv(AzureEnvironmentEnvName, tt.EnvironmentName)\n\n\t\t\t// Get a VM client\n\t\t\tclient, err := CreateManagedClustersClientE(\"\")\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// Check for correct ARM URI\n\t\t\tassert.Equal(t, tt.ExpectedBaseURI, client.BaseURI)\n\t\t})\n\t}\n}\n\nfunc TestCosmosDBAccountClientBaseURISetCorrectly(t *testing.T) {\n\tvar cases = []struct {\n\t\tCaseName        string\n\t\tEnvironmentName string\n\t\tExpectedBaseURI string\n\t}{\n\t\t{\"GovCloud/CosmosDBAccountClient\", govCloudEnvName, autorest.USGovernmentCloud.ResourceManagerEndpoint},\n\t\t{\"PublicCloud/CosmosDBAccountClient\", publicCloudEnvName, autorest.PublicCloud.ResourceManagerEndpoint},\n\t\t{\"ChinaCloud/CosmosDBAccountClient\", chinaCloudEnvName, autorest.ChinaCloud.ResourceManagerEndpoint},\n\t\t{\"GermanCloud/CosmosDBAccountClient\", germanyCloudEnvName, autorest.GermanCloud.ResourceManagerEndpoint},\n\t}\n\n\t// save any current env value and restore on exit\n\tcurrentEnv := os.Getenv(AzureEnvironmentEnvName)\n\tdefer os.Setenv(AzureEnvironmentEnvName, currentEnv)\n\n\tfor _, tt := range cases {\n\t\t// The following is necessary to make sure testCase's values don't\n\t\t// get updated due to concurrency within the scope of t.Run(..) below\n\t\ttt := tt\n\t\tt.Run(tt.CaseName, func(t *testing.T) {\n\t\t\t// Override env setting\n\t\t\tos.Setenv(AzureEnvironmentEnvName, tt.EnvironmentName)\n\n\t\t\t// Get a VM client\n\t\t\tclient, err := CreateCosmosDBAccountClientE(\"\")\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// Check for correct ARM URI\n\t\t\tassert.Equal(t, tt.ExpectedBaseURI, client.BaseURI)\n\t\t})\n\t}\n}\n\nfunc TestCosmosDBSQLClientBaseURISetCorrectly(t *testing.T) {\n\tvar cases = []struct {\n\t\tCaseName        string\n\t\tEnvironmentName string\n\t\tExpectedBaseURI string\n\t}{\n\t\t{\"GovCloud/CosmosDBAccountClient\", govCloudEnvName, autorest.USGovernmentCloud.ResourceManagerEndpoint},\n\t\t{\"PublicCloud/CosmosDBAccountClient\", publicCloudEnvName, autorest.PublicCloud.ResourceManagerEndpoint},\n\t\t{\"ChinaCloud/CosmosDBAccountClient\", chinaCloudEnvName, autorest.ChinaCloud.ResourceManagerEndpoint},\n\t\t{\"GermanCloud/CosmosDBAccountClient\", germanyCloudEnvName, autorest.GermanCloud.ResourceManagerEndpoint},\n\t}\n\n\t// save any current env value and restore on exit\n\tcurrentEnv := os.Getenv(AzureEnvironmentEnvName)\n\tdefer os.Setenv(AzureEnvironmentEnvName, currentEnv)\n\n\tfor _, tt := range cases {\n\t\t// The following is necessary to make sure testCase's values don't\n\t\t// get updated due to concurrency within the scope of t.Run(..) below\n\t\ttt := tt\n\t\tt.Run(tt.CaseName, func(t *testing.T) {\n\t\t\t// Override env setting\n\t\t\tos.Setenv(AzureEnvironmentEnvName, tt.EnvironmentName)\n\n\t\t\t// Get a VM client\n\t\t\tclient, err := CreateCosmosDBSQLClientE(\"\")\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// Check for correct ARM URI\n\t\t\tassert.Equal(t, tt.ExpectedBaseURI, client.BaseURI)\n\t\t})\n\t}\n}\nfunc TestPublicIPAddressesClientBaseURISetCorrectly(t *testing.T) {\n\tvar cases = []struct {\n\t\tCaseName        string\n\t\tEnvironmentName string\n\t\tExpectedBaseURI string\n\t}{\n\t\t{\"GovCloud/CosmosDBAccountClient\", govCloudEnvName, autorest.USGovernmentCloud.ResourceManagerEndpoint},\n\t\t{\"PublicCloud/CosmosDBAccountClient\", publicCloudEnvName, autorest.PublicCloud.ResourceManagerEndpoint},\n\t\t{\"ChinaCloud/CosmosDBAccountClient\", chinaCloudEnvName, autorest.ChinaCloud.ResourceManagerEndpoint},\n\t\t{\"GermanCloud/CosmosDBAccountClient\", germanyCloudEnvName, autorest.GermanCloud.ResourceManagerEndpoint},\n\t}\n\n\t// save any current env value and restore on exit\n\tcurrentEnv := os.Getenv(AzureEnvironmentEnvName)\n\tdefer os.Setenv(AzureEnvironmentEnvName, currentEnv)\n\n\tfor _, tt := range cases {\n\t\t// The following is necessary to make sure testCase's values don't\n\t\t// get updated due to concurrency within the scope of t.Run(..) below\n\t\ttt := tt\n\t\tt.Run(tt.CaseName, func(t *testing.T) {\n\t\t\t// Override env setting\n\t\t\tos.Setenv(AzureEnvironmentEnvName, tt.EnvironmentName)\n\n\t\t\t// Get a VM client\n\t\t\tclient, err := CreatePublicIPAddressesClientE(\"\")\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// Check for correct ARM URI\n\t\t\tassert.Equal(t, tt.ExpectedBaseURI, client.BaseURI)\n\t\t})\n\t}\n}\nfunc TestLoadBalancerClientBaseURISetCorrectly(t *testing.T) {\n\tvar cases = []struct {\n\t\tCaseName        string\n\t\tEnvironmentName string\n\t\tExpectedBaseURI string\n\t}{\n\t\t{\"GovCloud/CosmosDBAccountClient\", govCloudEnvName, autorest.USGovernmentCloud.ResourceManagerEndpoint},\n\t\t{\"PublicCloud/CosmosDBAccountClient\", publicCloudEnvName, autorest.PublicCloud.ResourceManagerEndpoint},\n\t\t{\"ChinaCloud/CosmosDBAccountClient\", chinaCloudEnvName, autorest.ChinaCloud.ResourceManagerEndpoint},\n\t\t{\"GermanCloud/CosmosDBAccountClient\", germanyCloudEnvName, autorest.GermanCloud.ResourceManagerEndpoint},\n\t}\n\n\t// save any current env value and restore on exit\n\tcurrentEnv := os.Getenv(AzureEnvironmentEnvName)\n\tdefer os.Setenv(AzureEnvironmentEnvName, currentEnv)\n\n\tfor _, tt := range cases {\n\t\t// The following is necessary to make sure testCase's values don't\n\t\t// get updated due to concurrency within the scope of t.Run(..) below\n\t\ttt := tt\n\t\tt.Run(tt.CaseName, func(t *testing.T) {\n\t\t\t// Override env setting\n\t\t\tos.Setenv(AzureEnvironmentEnvName, tt.EnvironmentName)\n\n\t\t\t// Get a VM client\n\t\t\tclient, err := CreateLoadBalancerClientE(\"\")\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// Check for correct ARM URI\n\t\t\tassert.Equal(t, tt.ExpectedBaseURI, client.BaseURI)\n\t\t})\n\t}\n}\n\nfunc TestFrontDoorClientBaseURISetCorrectly(t *testing.T) {\n\tvar cases = []struct {\n\t\tCaseName        string\n\t\tEnvironmentName string\n\t\tExpectedBaseURI string\n\t}{\n\t\t{\"GovCloud/FrontDoorClient\", govCloudEnvName, autorest.USGovernmentCloud.ResourceManagerEndpoint},\n\t\t{\"PublicCloud/FrontDoorClient\", publicCloudEnvName, autorest.PublicCloud.ResourceManagerEndpoint},\n\t\t{\"ChinaCloud/FrontDoorClient\", chinaCloudEnvName, autorest.ChinaCloud.ResourceManagerEndpoint},\n\t\t{\"GermanCloud/FrontDoorClient\", germanyCloudEnvName, autorest.GermanCloud.ResourceManagerEndpoint},\n\t}\n\n\t// save any current env value and restore on exit\n\tcurrentEnv := os.Getenv(AzureEnvironmentEnvName)\n\tdefer os.Setenv(AzureEnvironmentEnvName, currentEnv)\n\n\tfor _, tt := range cases {\n\t\t// The following is necessary to make sure testCase's values don't\n\t\t// get updated due to concurrency within the scope of t.Run(..) below\n\t\ttt := tt\n\t\tt.Run(tt.CaseName, func(t *testing.T) {\n\t\t\t// Override env setting\n\t\t\tos.Setenv(AzureEnvironmentEnvName, tt.EnvironmentName)\n\n\t\t\t// Get a Front Door client\n\t\t\tclient, err := CreateFrontDoorClientE(\"\")\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// Check for correct ARM URI\n\t\t\tassert.Equal(t, tt.ExpectedBaseURI, client.BaseURI)\n\t\t})\n\t}\n}\n\nfunc TestFrontDoorFrontendEndpointClientBaseURISetCorrectly(t *testing.T) {\n\tvar cases = []struct {\n\t\tCaseName        string\n\t\tEnvironmentName string\n\t\tExpectedBaseURI string\n\t}{\n\t\t{\"GovCloud/FrontDoorClient\", govCloudEnvName, autorest.USGovernmentCloud.ResourceManagerEndpoint},\n\t\t{\"PublicCloud/FrontDoorClient\", publicCloudEnvName, autorest.PublicCloud.ResourceManagerEndpoint},\n\t\t{\"ChinaCloud/FrontDoorClient\", chinaCloudEnvName, autorest.ChinaCloud.ResourceManagerEndpoint},\n\t\t{\"GermanCloud/FrontDoorClient\", germanyCloudEnvName, autorest.GermanCloud.ResourceManagerEndpoint},\n\t}\n\n\t// save any current env value and restore on exit\n\tcurrentEnv := os.Getenv(AzureEnvironmentEnvName)\n\tdefer os.Setenv(AzureEnvironmentEnvName, currentEnv)\n\n\tfor _, tt := range cases {\n\t\t// The following is necessary to make sure testCase's values don't\n\t\t// get updated due to concurrency within the scope of t.Run(..) below\n\t\ttt := tt\n\t\tt.Run(tt.CaseName, func(t *testing.T) {\n\t\t\t// Override env setting\n\t\t\tos.Setenv(AzureEnvironmentEnvName, tt.EnvironmentName)\n\n\t\t\t// Get a AFD frontend endpoint client\n\t\t\tclient, err := CreateFrontDoorFrontendEndpointClientE(\"\")\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// Check for correct ARM URI\n\t\t\tassert.Equal(t, tt.ExpectedBaseURI, client.BaseURI)\n\t\t})\n\t}\n}\n\nfunc TestCreateManagedEnvironmentsClientEEndpointURISetCorrectly(t *testing.T) {\n\tvar cases = []struct {\n\t\tCaseName        string\n\t\tEnvironmentName string\n\t\tExpectedBaseURI string\n\t\tExpectErr       bool\n\t}{\n\t\t{\"Default/ManagedEnvironmentsClient\", publicCloudEnvName, autorest.PublicCloud.ResourceManagerEndpoint, false},\n\t\t{\"PublicCloud/ManagedEnvironmentsClient\", publicCloudEnvName, autorest.PublicCloud.ResourceManagerEndpoint, false},\n\t\t{\"GovCloud/ManagedEnvironmentsClient\", govCloudEnvName, autorest.USGovernmentCloud.ResourceManagerEndpoint, false},\n\t\t{\"ChinaCloud/ManagedEnvironmentsClient\", chinaCloudEnvName, autorest.ChinaCloud.ResourceManagerEndpoint, false},\n\t\t{\"GermanCloud/ManagedEnvironmentsClient\", germanyCloudEnvName, autorest.GermanCloud.ResourceManagerEndpoint, true}, //GermanCloud is deleted as of 2021-10-21 https://learn.microsoft.com/en-us/previous-versions/azure/germany/germany-welcome\n\t}\n\n\t// save any current env value and restore on exit\n\tcurrentEnv := os.Getenv(AzureEnvironmentEnvName)\n\tdefer os.Setenv(AzureEnvironmentEnvName, currentEnv)\n\n\tfor _, tt := range cases {\n\t\t// The following is necessary to make sure testCase's values don't\n\t\t// get updated due to concurrency within the scope of t.Run(..) below\n\t\ttt := tt\n\t\tt.Run(tt.CaseName, func(t *testing.T) {\n\t\t\t// Override env setting\n\t\t\tif tt.EnvironmentName != \"\" {\n\t\t\t\tos.Setenv(AzureEnvironmentEnvName, tt.EnvironmentName)\n\t\t\t} else {\n\t\t\t\tos.Unsetenv(AzureEnvironmentEnvName)\n\t\t\t}\n\n\t\t\t// Get a ManagedEnvironmentsClient client\n\t\t\tclient, err := CreateManagedEnvironmentsClientE(\"\")\n\t\t\tif tt.ExpectErr {\n\t\t\t\trequire.Error(t, err)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, client)\n\t\t\t\t// Not ideal, but to get the base URI we need to access the internal field\n\t\t\t\tfield := reflect.ValueOf(client).Elem().FieldByName(\"internal\").Elem().FieldByName(\"ep\")\n\t\t\t\tassert.Equal(t, field.String()+\"/\", tt.ExpectedBaseURI)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestCreateContainerAppsClientEEndpointURISetCorrectly(t *testing.T) {\n\tvar cases = []struct {\n\t\tCaseName        string\n\t\tEnvironmentName string\n\t\tExpectedBaseURI string\n\t\tExpectErr       bool\n\t}{\n\t\t{\"Default/ContainerAppsClient\", \"\", autorest.PublicCloud.ResourceManagerEndpoint, false},\n\t\t{\"PublicCloud/ContainerAppsClient\", publicCloudEnvName, autorest.PublicCloud.ResourceManagerEndpoint, false},\n\t\t{\"GovCloud/ContainerAppsClient\", govCloudEnvName, autorest.USGovernmentCloud.ResourceManagerEndpoint, false},\n\t\t{\"ChinaCloud/ContainerAppsClient\", chinaCloudEnvName, autorest.ChinaCloud.ResourceManagerEndpoint, false},\n\t\t{\"GermanCloud/ContainerAppsClient\", germanyCloudEnvName, autorest.GermanCloud.ResourceManagerEndpoint, true}, //GermanCloud is deleted as of 2021-10-21 https://learn.microsoft.com/en-us/previous-versions/azure/germany/germany-welcome\n\t}\n\n\t// save any current env value and restore on exit\n\tcurrentEnv := os.Getenv(AzureEnvironmentEnvName)\n\tdefer os.Setenv(AzureEnvironmentEnvName, currentEnv)\n\n\tfor _, tt := range cases {\n\t\t// The following is necessary to make sure testCase's values don't\n\t\t// get updated due to concurrency within the scope of t.Run(..) below\n\t\ttt := tt\n\t\tt.Run(tt.CaseName, func(t *testing.T) {\n\t\t\t// Override env setting\n\t\t\tif tt.EnvironmentName != \"\" {\n\t\t\t\tos.Setenv(AzureEnvironmentEnvName, tt.EnvironmentName)\n\t\t\t} else {\n\t\t\t\tos.Unsetenv(AzureEnvironmentEnvName)\n\t\t\t}\n\n\t\t\t// Get a ManagedEnvironmentsClient client\n\t\t\tclient, err := CreateContainerAppsClientE(\"\")\n\t\t\tif tt.ExpectErr {\n\t\t\t\trequire.Error(t, err)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, client)\n\t\t\t\t// Not ideal, but to get the base URI we need to access the internal field\n\t\t\t\tfield := reflect.ValueOf(client).Elem().FieldByName(\"internal\").Elem().FieldByName(\"ep\")\n\t\t\t\tassert.Equal(t, field.String()+\"/\", tt.ExpectedBaseURI)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestCreateContainerAppJobsClientEEndpointURISetCorrectly(t *testing.T) {\n\tvar cases = []struct {\n\t\tCaseName        string\n\t\tEnvironmentName string\n\t\tExpectedBaseURI string\n\t\tExpectErr       bool\n\t}{\n\t\t{\"Default/ContainerAppsClient\", \"\", autorest.PublicCloud.ResourceManagerEndpoint, false},\n\t\t{\"PublicCloud/ContainerAppsClient\", publicCloudEnvName, autorest.PublicCloud.ResourceManagerEndpoint, false},\n\t\t{\"GovCloud/ContainerAppsClient\", govCloudEnvName, autorest.USGovernmentCloud.ResourceManagerEndpoint, false},\n\t\t{\"ChinaCloud/ContainerAppsClient\", chinaCloudEnvName, autorest.ChinaCloud.ResourceManagerEndpoint, false},\n\t\t{\"GermanCloud/ContainerAppsClient\", germanyCloudEnvName, autorest.GermanCloud.ResourceManagerEndpoint, true}, //GermanCloud is deleted as of 2021-10-21 https://learn.microsoft.com/en-us/previous-versions/azure/germany/germany-welcome\n\t}\n\n\t// save any current env value and restore on exit\n\tcurrentEnv := os.Getenv(AzureEnvironmentEnvName)\n\tdefer os.Setenv(AzureEnvironmentEnvName, currentEnv)\n\n\tfor _, tt := range cases {\n\t\t// The following is necessary to make sure testCase's values don't\n\t\t// get updated due to concurrency within the scope of t.Run(..) below\n\t\ttt := tt\n\t\tt.Run(tt.CaseName, func(t *testing.T) {\n\t\t\t// Override env setting\n\t\t\tif tt.EnvironmentName != \"\" {\n\t\t\t\tos.Setenv(AzureEnvironmentEnvName, tt.EnvironmentName)\n\t\t\t} else {\n\t\t\t\tos.Unsetenv(AzureEnvironmentEnvName)\n\t\t\t}\n\n\t\t\t// Get a ManagedEnvironmentsClient client\n\t\t\tclient, err := CreateContainerAppJobsClientE(\"\")\n\t\t\tif tt.ExpectErr {\n\t\t\t\trequire.Error(t, err)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, client)\n\t\t\t\t// Not ideal, but to get the base URI we need to access the internal field\n\t\t\t\tfield := reflect.ValueOf(client).Elem().FieldByName(\"internal\").Elem().FieldByName(\"ep\")\n\t\t\t\tassert.Equal(t, field.String()+\"/\", tt.ExpectedBaseURI)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "modules/azure/common.go",
    "content": "package azure\n\nimport (\n\t\"os\"\n)\n\nconst (\n\t// AzureSubscriptionID is an optional env variable supported by the `azurerm` Terraform provider to\n\t// designate a target Azure subscription ID\n\tAzureSubscriptionID = \"ARM_SUBSCRIPTION_ID\"\n\n\t// AzureResGroupName is an optional env variable custom to Terratest to designate a target Azure resource group\n\tAzureResGroupName = \"AZURE_RES_GROUP_NAME\"\n)\n\n// GetTargetAzureSubscription is a helper function to find the correct target Azure Subscription ID,\n// with provided arguments taking precedence over environment variables\nfunc GetTargetAzureSubscription(subscriptionID string) (string, error) {\n\treturn getTargetAzureSubscription(subscriptionID)\n}\n\nfunc getTargetAzureSubscription(subscriptionID string) (string, error) {\n\tif subscriptionID == \"\" {\n\t\tif id, exists := os.LookupEnv(AzureSubscriptionID); exists {\n\t\t\treturn id, nil\n\t\t}\n\n\t\treturn \"\", SubscriptionIDNotFound{}\n\t}\n\n\treturn subscriptionID, nil\n}\n\n// GetTargetAzureResourceGroupName is a helper function to find the correct target Azure Resource Group name,\n// with provided arguments taking precedence over environment variables\nfunc GetTargetAzureResourceGroupName(resourceGroupName string) (string, error) {\n\treturn getTargetAzureResourceGroupName(resourceGroupName)\n}\n\nfunc getTargetAzureResourceGroupName(resourceGroupName string) (string, error) {\n\tif resourceGroupName == \"\" {\n\t\tif name, exists := os.LookupEnv(AzureResGroupName); exists {\n\t\t\treturn name, nil\n\t\t}\n\n\t\treturn \"\", ResourceGroupNameNotFound{}\n\t}\n\n\treturn resourceGroupName, nil\n}\n\n// safePtrToString converts a string pointer to a non-pointer string value, or to \"\" if the pointer is nil.\nfunc safePtrToString(raw *string) string {\n\tif raw == nil {\n\t\treturn \"\"\n\t}\n\treturn *raw\n}\n\n// safePtrToInt32 converts a int32 pointer to a non-pointer int32 value, or to 0 if the pointer is nil.\nfunc safePtrToInt32(raw *int32) int32 {\n\tif raw == nil {\n\t\treturn 0\n\t}\n\treturn *raw\n}\n\n// safePtrToList converts a []string pointer to a non-pointer []string value, or to initialization of an empty slice if the pointer is nil.\nfunc safePtrToList(raw *[]string) []string {\n\tif raw == nil {\n\t\treturn []string{}\n\t}\n\treturn *raw\n}\n"
  },
  {
    "path": "modules/azure/common_test.go",
    "content": "//go:build azure\n// +build azure\n\n// NOTE: We use build tags to differentiate azure testing because we currently do not have azure access setup for\n// CircleCI.\n\npackage azure\n\nimport (\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestGetTargetAzureSubscription(t *testing.T) {\n\tt.Parallel()\n\n\t//Check that ARM_SUBSCRIPTION_ID env variable is set, CI requires this value to run all test.\n\trequire.NotEmpty(t, os.Getenv(AzureSubscriptionID), \"ARM_SUBSCRIPTION_ID environment variable not set.\")\n\n\ttype args struct {\n\t\tsubID string\n\t}\n\n\ttests := []struct {\n\t\tname    string\n\t\targs    args\n\t\twant    string\n\t\twantErr bool\n\t}{\n\t\t{name: \"subIDProvidedAsArg\", args: args{subID: \"test\"}, want: \"test\", wantErr: false},\n\t\t{name: \"subIDNotProvidedFallbackToEnv\", args: args{subID: \"\"}, want: os.Getenv(AzureSubscriptionID), wantErr: false},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := GetTargetAzureSubscription(tt.args.subID)\n\n\t\t\tif tt.wantErr {\n\t\t\t\trequire.Error(t, err)\n\t\t\t} else {\n\t\t\t\trequire.Equal(t, tt.want, got)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestGetTargetAzureResourceGroupName(t *testing.T) {\n\tt.Parallel()\n\n\ttype args struct {\n\t\trgName string\n\t}\n\n\ttests := []struct {\n\t\tname    string\n\t\targs    args\n\t\twant    string\n\t\twantErr bool\n\t}{\n\t\t{name: \"rgNameProvidedAsArg\", args: args{rgName: \"test\"}, want: \"test\", wantErr: false},\n\t\t{name: \"rgNameNotProvided\", args: args{rgName: \"\"}, want: \"\", wantErr: true},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := GetTargetAzureResourceGroupName(tt.args.rgName)\n\n\t\t\tif tt.wantErr {\n\t\t\t\trequire.Error(t, err)\n\t\t\t} else {\n\t\t\t\trequire.Equal(t, tt.want, got)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSafePtrToString(t *testing.T) {\n\t// When given a nil, should always return an empty string\n\tvar nilPtr *string = nil\n\tnilResult := safePtrToString(nilPtr)\n\tassert.Equal(t, \"\", nilResult)\n\n\t// When given a string, should just de-ref and return\n\tstringPtr := \"Test\"\n\tstringResult := safePtrToString(&stringPtr)\n\tassert.Equal(t, \"Test\", stringResult)\n}\n\nfunc TestSafePtrToInt32(t *testing.T) {\n\t// When given a nil, should always return an zero value int32\n\tvar nilPtr *int32 = nil\n\tnilResult := safePtrToInt32(nilPtr)\n\tassert.Equal(t, int32(0), nilResult)\n\n\t// When given a string, should just de-ref and return\n\tintPtr := int32(42)\n\tintResult := safePtrToInt32(&intPtr)\n\tassert.Equal(t, int32(42), intResult)\n}\n"
  },
  {
    "path": "modules/azure/compute.go",
    "content": "package azure\n\nimport (\n\t\"context\"\n\n\t\"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2019-07-01/compute\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// GetVirtualMachineClient is a helper function that will setup an Azure Virtual Machine client on your behalf.\nfunc GetVirtualMachineClient(t testing.TestingT, subscriptionID string) *compute.VirtualMachinesClient {\n\tvmClient, err := GetVirtualMachineClientE(subscriptionID)\n\trequire.NoError(t, err)\n\treturn vmClient\n}\n\n// GetVirtualMachineClientE is a helper function that will setup an Azure Virtual Machine client on your behalf.\nfunc GetVirtualMachineClientE(subscriptionID string) (*compute.VirtualMachinesClient, error) {\n\n\t// snippet-tag-start::client_factory_example.helper\n\t// Create a VM client\n\tvmClient, err := CreateVirtualMachinesClientE(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\t// snippet-tag-end::client_factory_example.helper\n\n\t// Create an authorizer\n\tauthorizer, err := NewAuthorizer()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Attach authorizer to the client\n\tvmClient.Authorizer = *authorizer\n\treturn vmClient, nil\n}\n\n// VirtualMachineExists indicates whether the specified Azure Virtual Machine exists.\n// This function would fail the test if there is an error.\nfunc VirtualMachineExists(t testing.TestingT, vmName string, resGroupName string, subscriptionID string) bool {\n\texists, err := VirtualMachineExistsE(vmName, resGroupName, subscriptionID)\n\trequire.NoError(t, err)\n\treturn exists\n}\n\n// VirtualMachineExistsE indicates whether the specified Azure Virtual Machine exists.\nfunc VirtualMachineExistsE(vmName string, resGroupName string, subscriptionID string) (bool, error) {\n\t// Get VM Object\n\t_, err := GetVirtualMachineE(vmName, resGroupName, subscriptionID)\n\tif err != nil {\n\t\tif ResourceNotFoundErrorExists(err) {\n\t\t\treturn false, nil\n\t\t}\n\t\treturn false, err\n\t}\n\treturn true, nil\n}\n\n// GetVirtualMachineNics gets a list of Network Interface names for a specified Azure Virtual Machine.\n// This function would fail the test if there is an error.\nfunc GetVirtualMachineNics(t testing.TestingT, vmName string, resGroupName string, subscriptionID string) []string {\n\tnicList, err := GetVirtualMachineNicsE(vmName, resGroupName, subscriptionID)\n\trequire.NoError(t, err)\n\n\treturn nicList\n}\n\n// GetVirtualMachineNicsE gets a list of Network Interface names for a specified Azure Virtual Machine.\nfunc GetVirtualMachineNicsE(vmName string, resGroupName string, subscriptionID string) ([]string, error) {\n\n\t// Get VM Object\n\tvm, err := GetVirtualMachineE(vmName, resGroupName, subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Get VM NIC(s); value always present, no nil checks needed.\n\tvmNICs := *vm.NetworkProfile.NetworkInterfaces\n\n\tnics := make([]string, len(vmNICs))\n\tfor i, nic := range vmNICs {\n\t\t// Get ID from resource string.\n\t\tnicName, err := GetNameFromResourceIDE(*nic.ID)\n\t\tif err == nil {\n\t\t\tnics[i] = nicName\n\t\t}\n\t}\n\treturn nics, nil\n}\n\n// GetVirtualMachineManagedDisks gets the list of Managed Disk names of the specified Azure Virtual Machine.\n// This function would fail the test if there is an error.\nfunc GetVirtualMachineManagedDisks(t testing.TestingT, vmName string, resGroupName string, subscriptionID string) []string {\n\tdiskNames, err := GetVirtualMachineManagedDisksE(vmName, resGroupName, subscriptionID)\n\trequire.NoError(t, err)\n\n\treturn diskNames\n}\n\n// GetVirtualMachineManagedDisksE gets the list of Managed Disk names of the specified Azure Virtual Machine.\nfunc GetVirtualMachineManagedDisksE(vmName string, resGroupName string, subscriptionID string) ([]string, error) {\n\n\t// Get VM Object\n\tvm, err := GetVirtualMachineE(vmName, resGroupName, subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Get VM attached Disks; value always present even if no disks attached, no nil check needed.\n\tvmDisks := *vm.StorageProfile.DataDisks\n\n\t// Get the Names of the attached Managed Disks\n\tdiskNames := make([]string, len(vmDisks))\n\tfor i, v := range vmDisks {\n\t\t// Disk names are required, no nil check needed.\n\t\tdiskNames[i] = *v.Name\n\t}\n\n\treturn diskNames, nil\n}\n\n// GetVirtualMachineOSDiskName gets the OS Disk name of the specified Azure Virtual Machine.\n// This function would fail the test if there is an error.\nfunc GetVirtualMachineOSDiskName(t testing.TestingT, vmName string, resGroupName string, subscriptionID string) string {\n\tosDiskName, err := GetVirtualMachineOSDiskNameE(vmName, resGroupName, subscriptionID)\n\trequire.NoError(t, err)\n\n\treturn osDiskName\n}\n\n// GetVirtualMachineOSDiskNameE gets the OS Disk name of the specified Azure Virtual Machine.\nfunc GetVirtualMachineOSDiskNameE(vmName string, resGroupName string, subscriptionID string) (string, error) {\n\t// Get VM Object\n\tvm, err := GetVirtualMachineE(vmName, resGroupName, subscriptionID)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn *vm.StorageProfile.OsDisk.Name, nil\n}\n\n// GetVirtualMachineAvailabilitySetID gets the Availability Set ID of the specified Azure Virtual Machine.\n// This function would fail the test if there is an error.\nfunc GetVirtualMachineAvailabilitySetID(t testing.TestingT, vmName string, resGroupName string, subscriptionID string) string {\n\tavsID, err := GetVirtualMachineAvailabilitySetIDE(vmName, resGroupName, subscriptionID)\n\trequire.NoError(t, err)\n\n\treturn avsID\n}\n\n// GetVirtualMachineAvailabilitySetIDE gets the Availability Set ID of the specified Azure Virtual Machine.\nfunc GetVirtualMachineAvailabilitySetIDE(vmName string, resGroupName string, subscriptionID string) (string, error) {\n\t// Get VM Object\n\tvm, err := GetVirtualMachineE(vmName, resGroupName, subscriptionID)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// Virtual Machine has no associated Availability Set\n\tif vm.AvailabilitySet == nil {\n\t\treturn \"\", nil\n\t}\n\n\t// Get ID from resource string\n\tavs, err := GetNameFromResourceIDE(*vm.AvailabilitySet.ID)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn avs, nil\n}\n\n// VMImage represents the storage image for the specified Azure Virtual Machine.\ntype VMImage struct {\n\tPublisher string\n\tOffer     string\n\tSKU       string\n\tVersion   string\n}\n\n// GetVirtualMachineImage gets the Image of the specified Azure Virtual Machine.\n// This function would fail the test if there is an error.\nfunc GetVirtualMachineImage(t testing.TestingT, vmName string, resGroupName string, subscriptionID string) VMImage {\n\tvmImage, err := GetVirtualMachineImageE(vmName, resGroupName, subscriptionID)\n\trequire.NoError(t, err)\n\n\treturn vmImage\n}\n\n// GetVirtualMachineImageE gets the Image of the specified Azure Virtual Machine.\nfunc GetVirtualMachineImageE(vmName string, resGroupName string, subscriptionID string) (VMImage, error) {\n\tvar vmImage VMImage\n\n\t// Get VM Object\n\tvm, err := GetVirtualMachineE(vmName, resGroupName, subscriptionID)\n\tif err != nil {\n\t\treturn vmImage, err\n\t}\n\n\t// Populate VM Image; values always present, no nil checks needed\n\tvmImage.Publisher = *vm.StorageProfile.ImageReference.Publisher\n\tvmImage.Offer = *vm.StorageProfile.ImageReference.Offer\n\tvmImage.SKU = *vm.StorageProfile.ImageReference.Sku\n\tvmImage.Version = *vm.StorageProfile.ImageReference.Version\n\n\treturn vmImage, nil\n}\n\n// GetSizeOfVirtualMachine gets the Size Type of the specified Azure Virtual Machine.\n// This function would fail the test if there is an error.\nfunc GetSizeOfVirtualMachine(t testing.TestingT, vmName string, resGroupName string, subscriptionID string) compute.VirtualMachineSizeTypes {\n\tsize, err := GetSizeOfVirtualMachineE(vmName, resGroupName, subscriptionID)\n\trequire.NoError(t, err)\n\n\treturn size\n}\n\n// GetSizeOfVirtualMachineE gets the Size Type of the specified Azure Virtual Machine.\nfunc GetSizeOfVirtualMachineE(vmName string, resGroupName string, subscriptionID string) (compute.VirtualMachineSizeTypes, error) {\n\t// Get VM Object\n\tvm, err := GetVirtualMachineE(vmName, resGroupName, subscriptionID)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn vm.VirtualMachineProperties.HardwareProfile.VMSize, nil\n}\n\n// GetVirtualMachineTags gets the Tags of the specified Virtual Machine as a map.\n// This function would fail the test if there is an error.\nfunc GetVirtualMachineTags(t testing.TestingT, vmName string, resGroupName string, subscriptionID string) map[string]string {\n\ttags, err := GetVirtualMachineTagsE(vmName, resGroupName, subscriptionID)\n\trequire.NoError(t, err)\n\n\treturn tags\n}\n\n// GetVirtualMachineTagsE gets the Tags of the specified Virtual Machine as a map.\nfunc GetVirtualMachineTagsE(vmName string, resGroupName string, subscriptionID string) (map[string]string, error) {\n\t// Setup a blank map to populate and return\n\ttags := make(map[string]string)\n\n\t// Get VM Object\n\tvm, err := GetVirtualMachineE(vmName, resGroupName, subscriptionID)\n\tif err != nil {\n\t\treturn tags, err\n\t}\n\n\t// Range through existing tags and populate above map accordingly\n\tfor k, v := range vm.Tags {\n\t\ttags[k] = *v\n\t}\n\n\treturn tags, nil\n}\n\n// ***************************************************** //\n// Get multiple Virtual Machines from a Resource Group\n// ***************************************************** //\n\n// ListVirtualMachinesForResourceGroup gets a list of all Virtual Machine names in the specified Resource Group.\n// This function would fail the test if there is an error.\nfunc ListVirtualMachinesForResourceGroup(t testing.TestingT, resGroupName string, subscriptionID string) []string {\n\tvms, err := ListVirtualMachinesForResourceGroupE(resGroupName, subscriptionID)\n\trequire.NoError(t, err)\n\treturn vms\n}\n\n// ListVirtualMachinesForResourceGroupE gets a list of all Virtual Machine names in the specified Resource Group.\nfunc ListVirtualMachinesForResourceGroupE(resourceGroupName string, subscriptionID string) ([]string, error) {\n\tvar vmDetails []string\n\n\tvmClient, err := GetVirtualMachineClientE(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvms, err := vmClient.List(context.Background(), resourceGroupName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfor _, v := range vms.Values() {\n\t\tvmDetails = append(vmDetails, *v.Name)\n\t}\n\treturn vmDetails, nil\n}\n\n// GetVirtualMachinesForResourceGroup gets all Virtual Machine objects in the specified Resource Group. Each\n// VM Object represents the entire set of VM compute properties accessible by using the VM name as the map key.\n// This function would fail the test if there is an error.\nfunc GetVirtualMachinesForResourceGroup(t testing.TestingT, resGroupName string, subscriptionID string) map[string]compute.VirtualMachineProperties {\n\tvms, err := GetVirtualMachinesForResourceGroupE(resGroupName, subscriptionID)\n\trequire.NoError(t, err)\n\treturn vms\n}\n\n// GetVirtualMachinesForResourceGroupE gets all Virtual Machine objects in the specified Resource Group. Each\n// VM Object represents the entire set of VM compute properties accessible by using the VM name as the map key.\nfunc GetVirtualMachinesForResourceGroupE(resourceGroupName string, subscriptionID string) (map[string]compute.VirtualMachineProperties, error) {\n\t// Create VM Client\n\tvmClient, err := GetVirtualMachineClientE(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Get the list of VMs in the Resource Group\n\tvms, err := vmClient.List(context.Background(), resourceGroupName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Get the VMs in the Resource Group.\n\tvmDetails := make(map[string]compute.VirtualMachineProperties, len(vms.Values()))\n\tfor _, v := range vms.Values() {\n\t\t// VM name and machine properties are required for each VM, no nil check required.\n\t\tvmDetails[*v.Name] = *v.VirtualMachineProperties\n\t}\n\treturn vmDetails, nil\n}\n\n// ******************************************************************** //\n// Get VM using Instance and Instance property get, reducing SKD calls\n// ******************************************************************** //\n\n// Instance of the VM\ntype Instance struct {\n\t*compute.VirtualMachine\n}\n\n// GetVirtualMachineInstanceSize gets the size of the Virtual Machine.\nfunc (vm *Instance) GetVirtualMachineInstanceSize() compute.VirtualMachineSizeTypes {\n\treturn vm.VirtualMachineProperties.HardwareProfile.VMSize\n}\n\n// *********************** //\n// Get the base VM Object\n// *********************** //\n\n// GetVirtualMachine gets a Virtual Machine in the specified Azure Resource Group.\n// This function would fail the test if there is an error.\nfunc GetVirtualMachine(t testing.TestingT, vmName string, resGroupName string, subscriptionID string) *compute.VirtualMachine {\n\tvm, err := GetVirtualMachineE(vmName, resGroupName, subscriptionID)\n\trequire.NoError(t, err)\n\treturn vm\n}\n\n// GetVirtualMachineE gets a Virtual Machine in the specified Azure Resource Group.\nfunc GetVirtualMachineE(vmName string, resGroupName string, subscriptionID string) (*compute.VirtualMachine, error) {\n\t// Validate resource group name and subscription ID\n\tresGroupName, err := getTargetAzureResourceGroupName(resGroupName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Get the client reference\n\tclient, err := GetVirtualMachineClientE(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvm, err := client.Get(context.Background(), resGroupName, vmName, compute.InstanceView)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &vm, nil\n}\n"
  },
  {
    "path": "modules/azure/compute_test.go",
    "content": "//go:build azure\n// +build azure\n\n// NOTE: We use build tags to differentiate azure testing because we currently do not have azure access setup for\n// CircleCI.\n\npackage azure\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\n/*\nThe below tests are currently stubbed out, with the expectation that they will throw errors.\nIf/when CRUD methods are introduced for Azure Virtual Machines, these tests can be extended.\n*/\n\nfunc TestGetVirtualMachineE(t *testing.T) {\n\tt.Parallel()\n\n\tvmName := \"\"\n\trgName := \"\"\n\tsubID := \"\"\n\n\t_, err := GetVirtualMachineE(vmName, rgName, subID)\n\n\trequire.Error(t, err)\n}\n\nfunc TestListVirtualMachinesForResourceGroupE(t *testing.T) {\n\tt.Parallel()\n\n\trgName := \"\"\n\tsubID := \"\"\n\n\t_, err := ListVirtualMachinesForResourceGroupE(rgName, subID)\n\n\trequire.Error(t, err)\n}\n\nfunc TestGetVirtualMachinesForResourceGroupE(t *testing.T) {\n\tt.Parallel()\n\n\trgName := \"\"\n\tsubID := \"\"\n\n\t_, err := GetVirtualMachinesForResourceGroupE(rgName, subID)\n\n\trequire.Error(t, err)\n}\n\nfunc TestGetVirtualMachineTagsE(t *testing.T) {\n\tt.Parallel()\n\n\tvmName := \"\"\n\trgName := \"\"\n\tsubID := \"\"\n\n\t_, err := GetVirtualMachineTagsE(vmName, rgName, subID)\n\n\trequire.Error(t, err)\n}\n\nfunc TestGetSizeOfVirtualMachineE(t *testing.T) {\n\tt.Parallel()\n\n\tvmName := \"\"\n\trgName := \"\"\n\tsubID := \"\"\n\n\t_, err := GetSizeOfVirtualMachineE(vmName, rgName, subID)\n\n\trequire.Error(t, err)\n}\n\nfunc TestGetVirtualMachineImageE(t *testing.T) {\n\tt.Parallel()\n\n\tvmName := \"\"\n\trgName := \"\"\n\tsubID := \"\"\n\n\t_, err := GetVirtualMachineImageE(vmName, rgName, subID)\n\n\trequire.Error(t, err)\n}\n\nfunc TestGetVirtualMachineAvailabilitySetIDE(t *testing.T) {\n\tt.Parallel()\n\n\tvmName := \"\"\n\trgName := \"\"\n\tsubID := \"\"\n\n\t_, err := GetVirtualMachineAvailabilitySetIDE(vmName, rgName, subID)\n\n\trequire.Error(t, err)\n}\n\nfunc TestGetVirtualMachineOSDiskNameE(t *testing.T) {\n\tt.Parallel()\n\n\tvmName := \"\"\n\trgName := \"\"\n\tsubID := \"\"\n\n\t_, err := GetVirtualMachineOSDiskNameE(vmName, rgName, subID)\n\n\trequire.Error(t, err)\n}\n\nfunc TestGetVirtualMachineManagedDisksE(t *testing.T) {\n\tt.Parallel()\n\n\tvmName := \"\"\n\trgName := \"\"\n\tsubID := \"\"\n\n\t_, err := GetVirtualMachineManagedDisksE(vmName, rgName, subID)\n\n\trequire.Error(t, err)\n}\n\nfunc TestGetVirtualMachineNicsE(t *testing.T) {\n\tt.Parallel()\n\n\tvmName := \"\"\n\trgName := \"\"\n\tsubID := \"\"\n\n\t_, err := GetVirtualMachineNicsE(vmName, rgName, subID)\n\n\trequire.Error(t, err)\n}\n\nfunc TestVirtualMachineExistsE(t *testing.T) {\n\tt.Parallel()\n\n\tvmName := \"\"\n\trgName := \"\"\n\tsubID := \"\"\n\n\t_, err := VirtualMachineExistsE(vmName, rgName, subID)\n\n\trequire.Error(t, err)\n}\n"
  },
  {
    "path": "modules/azure/container_apps.go",
    "content": "package azure\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/appcontainers/armappcontainers/v3\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// ManagedEnvironmentExists indicates whether the specified Managed Environment exists.\n// This function would fail the test if there is an error.\nfunc ManagedEnvironmentExists(t *testing.T, environmentName string, resourceGroupName string, subscriptionID string) bool {\n\texists, err := ManagedEnvironmentExistsE(environmentName, resourceGroupName, subscriptionID)\n\trequire.NoError(t, err)\n\treturn exists\n}\n\n// ManagedEnvironmentExistsE indicates whether the specified Managed Environment exists.\nfunc ManagedEnvironmentExistsE(environmentName string, resourceGroupName string, subscriptionID string) (bool, error) {\n\tclient, err := CreateManagedEnvironmentsClientE(subscriptionID)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\t_, err = client.Get(context.Background(), resourceGroupName, environmentName, nil)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\treturn true, nil\n}\n\n// GetManagedEnvironment gets the Managed Environment object\n// This function would fail the test if there is an error.\nfunc GetManagedEnvironment(t *testing.T, environmentName string, resourceGroupName string, subscriptionID string) *armappcontainers.ManagedEnvironment {\n\tenv, err := GetManagedEnvironmentE(environmentName, resourceGroupName, subscriptionID)\n\trequire.NoError(t, err)\n\treturn env\n}\n\n// GetManagedEnvironmentE gets the Managed Environment object\nfunc GetManagedEnvironmentE(environmentName string, resourceGroupName string, subscriptionID string) (*armappcontainers.ManagedEnvironment, error) {\n\tclient, err := CreateManagedEnvironmentsClientE(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tenv, err := client.Get(context.Background(), resourceGroupName, environmentName, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &env.ManagedEnvironment, nil\n}\n\n// ContainerAppExists indicates whether the Container App exists for the subscription.\n// This function would fail the test if there is an error.\nfunc ContainerAppExists(t *testing.T, containerAppName string, resourceGroupName string, subscriptionID string) bool {\n\texists, err := ContainerAppExistsE(containerAppName, resourceGroupName, subscriptionID)\n\trequire.NoError(t, err)\n\treturn exists\n}\n\n// ContainerAppExistsE indicates whether the Container App exists for the subscription.\nfunc ContainerAppExistsE(containerAppName string, resourceGroupName string, subscriptionID string) (bool, error) {\n\tclient, err := CreateContainerAppsClientE(subscriptionID)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\t_, err = client.Get(context.Background(), resourceGroupName, containerAppName, nil)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\treturn true, nil\n}\n\n// GetContainerApp gets the Container App object\n// This function would fail the test if there is an error.\nfunc GetContainerApp(t *testing.T, containerAppName string, resourceGroupName string, subscriptionID string) *armappcontainers.ContainerApp {\n\tapp, err := GetContainerAppE(containerAppName, resourceGroupName, subscriptionID)\n\trequire.NoError(t, err)\n\treturn app\n}\n\n// GetContainerAppE gets the Container App object\nfunc GetContainerAppE(environmentName string, resourceGroupName string, subscriptionID string) (*armappcontainers.ContainerApp, error) {\n\tclient, err := CreateContainerAppsClientE(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tapp, err := client.Get(context.Background(), resourceGroupName, environmentName, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &app.ContainerApp, nil\n}\n\n// ContainerAppJobExists indicates whether the Container App Job exists for the subscription.\n// This function would fail the test if there is an error.\nfunc ContainerAppJobExists(t *testing.T, containerAppName string, resourceGroupName string, subscriptionID string) bool {\n\texists, err := ContainerAppJobExistsE(containerAppName, resourceGroupName, subscriptionID)\n\trequire.NoError(t, err)\n\treturn exists\n}\n\n// ContainerAppJobExistsE indicates whether the Container App Job exists for the subscription.\nfunc ContainerAppJobExistsE(containerAppName string, resourceGroupName string, subscriptionID string) (bool, error) {\n\tclient, err := CreateContainerAppJobsClientE(subscriptionID)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\t_, err = client.Get(context.Background(), resourceGroupName, containerAppName, nil)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\treturn true, nil\n}\n\n// GetContainerAppJob gets the Container App Job object\n// This function would fail the test if there is an error.\nfunc GetContainerAppJob(t *testing.T, containerAppName string, resourceGroupName string, subscriptionID string) *armappcontainers.Job {\n\tapp, err := GetContainerAppJobE(containerAppName, resourceGroupName, subscriptionID)\n\trequire.NoError(t, err)\n\treturn app\n}\n\n// GetContainerAppJobE gets the Container App Job object\nfunc GetContainerAppJobE(environmentName string, resourceGroupName string, subscriptionID string) (*armappcontainers.Job, error) {\n\tclient, err := CreateContainerAppJobsClientE(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tapp, err := client.Get(context.Background(), resourceGroupName, environmentName, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &app.Job, nil\n}\n"
  },
  {
    "path": "modules/azure/container_apps_test.go",
    "content": "//go:build azure\n// +build azure\n\n// NOTE: We use build tags to differentiate azure testing because we currently do not have azure access setup for\n// CircleCI.\n\npackage azure\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\n/*\nThe below tests are currently stubbed out, with the expectation that they will throw errors.\nIf/when CRUD methods are introduced for Azure Virtual Machines, these tests can be extended.\n*/\n\nfunc TestManagedEnvironmentExists(t *testing.T) {\n\tt.Parallel()\n\n\tenvironmentName := \"\"\n\tresourceGroupName := \"\"\n\tsubscriptionID := \"\"\n\n\t_, err := ManagedEnvironmentExistsE(environmentName, resourceGroupName, subscriptionID)\n\trequire.Error(t, err)\n}\n\nfunc TestGetManagedEnvironmentE(t *testing.T) {\n\tt.Parallel()\n\n\tenvironmentName := \"\"\n\tresourceGroupName := \"\"\n\tsubscriptionID := \"\"\n\n\t_, err := GetManagedEnvironmentE(environmentName, resourceGroupName, subscriptionID)\n\trequire.Error(t, err)\n}\n\nfunc TestContainerAppExists(t *testing.T) {\n\tt.Parallel()\n\n\tenvironmentName := \"\"\n\tresourceGroupName := \"\"\n\tsubscriptionID := \"\"\n\n\t_, err := ContainerAppExistsE(environmentName, resourceGroupName, subscriptionID)\n\trequire.Error(t, err)\n}\n\nfunc TestGetContainerAppE(t *testing.T) {\n\tt.Parallel()\n\n\tenvironmentName := \"\"\n\tresourceGroupName := \"\"\n\tsubscriptionID := \"\"\n\n\t_, err := GetContainerAppE(environmentName, resourceGroupName, subscriptionID)\n\trequire.Error(t, err)\n}\n\nfunc TestContainerAppJobExists(t *testing.T) {\n\tt.Parallel()\n\n\tenvironmentName := \"\"\n\tresourceGroupName := \"\"\n\tsubscriptionID := \"\"\n\n\t_, err := ContainerAppJobExistsE(environmentName, resourceGroupName, subscriptionID)\n\trequire.Error(t, err)\n}\n\nfunc TestGetContainerJobAppE(t *testing.T) {\n\tt.Parallel()\n\n\tenvironmentName := \"\"\n\tresourceGroupName := \"\"\n\tsubscriptionID := \"\"\n\n\t_, err := GetContainerAppJobE(environmentName, resourceGroupName, subscriptionID)\n\trequire.Error(t, err)\n}\n"
  },
  {
    "path": "modules/azure/containers.go",
    "content": "package azure\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/Azure/azure-sdk-for-go/services/containerinstance/mgmt/2018-10-01/containerinstance\"\n\t\"github.com/Azure/azure-sdk-for-go/services/containerregistry/mgmt/2019-05-01/containerregistry\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\n// ContainerRegistryExists indicates whether the specified container registry exists.\n// This function would fail the test if there is an error.\nfunc ContainerRegistryExists(t *testing.T, registryName string, resourceGroupName string, subscriptionID string) bool {\n\texists, err := ContainerRegistryExistsE(registryName, resourceGroupName, subscriptionID)\n\trequire.NoError(t, err)\n\n\treturn exists\n}\n\n// ContainerRegistryExistsE indicates whether the specified container registry exists.\nfunc ContainerRegistryExistsE(registryName string, resourceGroupName string, subscriptionID string) (bool, error) {\n\t_, err := GetContainerRegistryE(registryName, resourceGroupName, subscriptionID)\n\tif err != nil {\n\t\tif ResourceNotFoundErrorExists(err) {\n\t\t\treturn false, nil\n\t\t}\n\t\treturn false, err\n\t}\n\treturn true, nil\n}\n\n// GetContainerRegistry gets the container registry object\n// This function would fail the test if there is an error.\nfunc GetContainerRegistry(t *testing.T, registryName string, resGroupName string, subscriptionID string) *containerregistry.Registry {\n\tresource, err := GetContainerRegistryE(registryName, resGroupName, subscriptionID)\n\n\trequire.NoError(t, err)\n\n\treturn resource\n}\n\n// GetContainerRegistryE gets the container registry object\nfunc GetContainerRegistryE(registryName string, resGroupName string, subscriptionID string) (*containerregistry.Registry, error) {\n\trgName, err := getTargetAzureResourceGroupName(resGroupName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tclient, err := GetContainerRegistryClientE(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresource, err := client.Get(context.Background(), rgName, registryName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &resource, nil\n}\n\n// GetContainerRegistryClientE is a helper function that will setup an Azure Container Registry client on your behalf\nfunc GetContainerRegistryClientE(subscriptionID string) (*containerregistry.RegistriesClient, error) {\n\t// Create an ACR client\n\tregistryClient, err := CreateContainerRegistryClientE(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Create an authorizer\n\tauthorizer, err := NewAuthorizer()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Attach authorizer to the client\n\tregistryClient.Authorizer = *authorizer\n\treturn registryClient, nil\n}\n\n// ContainerInstanceExists indicates whether the specified container instance exists.\n// This function would fail the test if there is an error.\nfunc ContainerInstanceExists(t *testing.T, instanceName string, resourceGroupName string, subscriptionID string) bool {\n\texists, err := ContainerInstanceExistsE(instanceName, resourceGroupName, subscriptionID)\n\trequire.NoError(t, err)\n\n\treturn exists\n}\n\n// ContainerInstanceExistsE indicates whether the specified container instance exists.\nfunc ContainerInstanceExistsE(instanceName string, resourceGroupName string, subscriptionID string) (bool, error) {\n\t_, err := GetContainerInstanceE(instanceName, resourceGroupName, subscriptionID)\n\tif err != nil {\n\t\tif ResourceNotFoundErrorExists(err) {\n\t\t\treturn false, nil\n\t\t}\n\t\treturn false, err\n\t}\n\treturn true, nil\n}\n\n// GetContainerInstance gets the container instance object\n// This function would fail the test if there is an error.\nfunc GetContainerInstance(t *testing.T, instanceName string, resGroupName string, subscriptionID string) *containerinstance.ContainerGroup {\n\tinstance, err := GetContainerInstanceE(instanceName, resGroupName, subscriptionID)\n\n\trequire.NoError(t, err)\n\n\treturn instance\n}\n\n// GetContainerInstanceE gets the container instance object\nfunc GetContainerInstanceE(instanceName string, resGroupName string, subscriptionID string) (*containerinstance.ContainerGroup, error) {\n\trgName, err := getTargetAzureResourceGroupName(resGroupName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tclient, err := GetContainerInstanceClientE(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tinstance, err := client.Get(context.Background(), rgName, instanceName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &instance, nil\n}\n\n// GetContainerInstanceClientE is a helper function that will setup an Azure Container Instance client on your behalf\nfunc GetContainerInstanceClientE(subscriptionID string) (*containerinstance.ContainerGroupsClient, error) {\n\t// Create an ACI client\n\tinstanceClient, err := CreateContainerInstanceClientE(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Create an authorizer\n\tauthorizer, err := NewAuthorizer()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Attach authorizer to the client\n\tinstanceClient.Authorizer = *authorizer\n\treturn instanceClient, nil\n}\n"
  },
  {
    "path": "modules/azure/containers_test.go",
    "content": "//go:build azure\n// +build azure\n\n// NOTE: We use build tags to differentiate azure testing because we currently do not have azure access setup for\n// CircleCI.\n\npackage azure\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\n/*\nThe below tests are currently stubbed out, with the expectation that they will throw errors.\nIf/when CRUD methods are introduced for Azure MySQL server and database, these tests can be extended\n*/\n\nfunc TestContainerRegistryExistsE(t *testing.T) {\n\tt.Parallel()\n\n\tresGroupName := \"\"\n\tregistryName := \"\"\n\tsubscriptionID := \"\"\n\n\t_, err := ContainerRegistryExistsE(registryName, resGroupName, subscriptionID)\n\trequire.Error(t, err)\n}\n\nfunc TestGetContainerRegistryE(t *testing.T) {\n\tt.Parallel()\n\n\tresGroupName := \"\"\n\tregistryName := \"\"\n\tsubscriptionID := \"\"\n\n\t_, err := GetContainerRegistryE(registryName, resGroupName, subscriptionID)\n\trequire.Error(t, err)\n}\n\nfunc TestGetContainerRegistryClientE(t *testing.T) {\n\tt.Parallel()\n\n\tsubscriptionID := \"\"\n\n\t_, err := GetContainerRegistryClientE(subscriptionID)\n\trequire.NoError(t, err)\n}\n\nfunc TestContainerInstanceExistsE(t *testing.T) {\n\tt.Parallel()\n\n\tresGroupName := \"\"\n\tinstanceName := \"\"\n\tsubscriptionID := \"\"\n\n\t_, err := ContainerInstanceExistsE(instanceName, resGroupName, subscriptionID)\n\trequire.Error(t, err)\n}\n\nfunc TestGetContainerInstanceE(t *testing.T) {\n\tt.Parallel()\n\n\tresGroupName := \"\"\n\tinstanceName := \"\"\n\tsubscriptionID := \"\"\n\n\t_, err := GetContainerInstanceE(instanceName, resGroupName, subscriptionID)\n\trequire.Error(t, err)\n}\n\nfunc TestGetContainerInstanceClientE(t *testing.T) {\n\tt.Parallel()\n\n\tsubscriptionID := \"\"\n\n\t_, err := GetContainerInstanceClientE(subscriptionID)\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "modules/azure/cosmosdb.go",
    "content": "package azure\n\nimport (\n\t\"context\"\n\n\t\"github.com/Azure/azure-sdk-for-go/profiles/preview/cosmos-db/mgmt/documentdb\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// GetCosmosDBAccountClientE is a helper function that will setup a CosmosDB account client.\nfunc GetCosmosDBAccountClientE(subscriptionID string) (*documentdb.DatabaseAccountsClient, error) {\n\n\t// Create a CosmosDB client\n\tcosmosClient, err := CreateCosmosDBAccountClientE(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Create an authorizer\n\tauthorizer, err := NewAuthorizer()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Attach authorizer to the client\n\tcosmosClient.Authorizer = *authorizer\n\n\treturn cosmosClient, nil\n}\n\n// GetCosmosDBAccountClient is a helper function that will setup a CosmosDB account client. This function would fail the test if there is an error.\nfunc GetCosmosDBAccountClient(t testing.TestingT, subscriptionID string) *documentdb.DatabaseAccountsClient {\n\tcosmosDBAccount, err := GetCosmosDBAccountClientE(subscriptionID)\n\trequire.NoError(t, err)\n\n\treturn cosmosDBAccount\n}\n\n// GetCosmosDBAccount is a helper function that gets the database account. This function would fail the test if there is an error.\nfunc GetCosmosDBAccount(t testing.TestingT, subscriptionID string, resourceGroupName string, accountName string) *documentdb.DatabaseAccountGetResults {\n\tcosmosDBAccount, err := GetCosmosDBAccountE(t, subscriptionID, resourceGroupName, accountName)\n\trequire.NoError(t, err)\n\n\treturn cosmosDBAccount\n}\n\n// GetCosmosDBAccountE is a helper function that gets the database account.\nfunc GetCosmosDBAccountE(t testing.TestingT, subscriptionID string, resourceGroupName string, accountName string) (*documentdb.DatabaseAccountGetResults, error) {\n\t// Create a CosmosDB client\n\tcosmosClient, err := GetCosmosDBAccountClientE(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Get the corresponding database account\n\tcosmosDBAccount, err := cosmosClient.Get(context.Background(), resourceGroupName, accountName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t//Return DB\n\treturn &cosmosDBAccount, nil\n}\n\n// GetCosmosDBSQLClientE is a helper function that will setup a CosmosDB SQL client.\nfunc GetCosmosDBSQLClientE(subscriptionID string) (*documentdb.SQLResourcesClient, error) {\n\n\t// Create a CosmosDB client\n\tcosmosClient, err := CreateCosmosDBSQLClientE(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Create an authorizer\n\tauthorizer, err := NewAuthorizer()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Attach authorizer to the client\n\tcosmosClient.Authorizer = *authorizer\n\n\treturn cosmosClient, nil\n}\n\n// GetCosmosDBSQLClient is a helper function that will setup a CosmosDB SQL client. This function would fail the test if there is an error.\nfunc GetCosmosDBSQLClient(t testing.TestingT, subscriptionID string) *documentdb.SQLResourcesClient {\n\tcosmosClient, err := GetCosmosDBSQLClientE(subscriptionID)\n\trequire.NoError(t, err)\n\n\treturn cosmosClient\n}\n\n// GetCosmosDBSQLDatabase is a helper function that gets a SQL database. This function would fail the test if there is an error.\nfunc GetCosmosDBSQLDatabase(t testing.TestingT, subscriptionID string, resourceGroupName string, accountName string, databaseName string) *documentdb.SQLDatabaseGetResults {\n\tcosmosSQLDB, err := GetCosmosDBSQLDatabaseE(t, subscriptionID, resourceGroupName, accountName, databaseName)\n\trequire.NoError(t, err)\n\n\treturn cosmosSQLDB\n}\n\n// GetCosmosDBSQLDatabaseE is a helper function that gets a SQL database.\nfunc GetCosmosDBSQLDatabaseE(t testing.TestingT, subscriptionID string, resourceGroupName string, accountName string, databaseName string) (*documentdb.SQLDatabaseGetResults, error) {\n\t// Create a CosmosDB client\n\tcosmosClient, err := GetCosmosDBSQLClientE(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Get the corresponding database\n\tcosmosSQLDB, err := cosmosClient.GetSQLDatabase(context.Background(), resourceGroupName, accountName, databaseName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t//Return DB\n\treturn &cosmosSQLDB, nil\n}\n\n// GetCosmosDBSQLContainer is a helper function that gets a SQL container. This function would fail the test if there is an error.\nfunc GetCosmosDBSQLContainer(t testing.TestingT, subscriptionID string, resourceGroupName string, accountName string, databaseName string, containerName string) *documentdb.SQLContainerGetResults {\n\tcosmosSQLContainer, err := GetCosmosDBSQLContainerE(t, subscriptionID, resourceGroupName, accountName, databaseName, containerName)\n\trequire.NoError(t, err)\n\n\treturn cosmosSQLContainer\n}\n\n// GetCosmosDBSQLContainerE is a helper function that gets a SQL container.\nfunc GetCosmosDBSQLContainerE(t testing.TestingT, subscriptionID string, resourceGroupName string, accountName string, databaseName string, containerName string) (*documentdb.SQLContainerGetResults, error) {\n\t// Create a CosmosDB client\n\tcosmosClient, err := GetCosmosDBSQLClientE(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Get the corresponding SQL container\n\tcosmosSQLContainer, err := cosmosClient.GetSQLContainer(context.Background(), resourceGroupName, accountName, databaseName, containerName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t//Return container\n\treturn &cosmosSQLContainer, nil\n}\n\n// GetCosmosDBSQLDatabaseThroughput is a helper function that gets a SQL database throughput configuration. This function would fail the test if there is an error.\nfunc GetCosmosDBSQLDatabaseThroughput(t testing.TestingT, subscriptionID string, resourceGroupName string, accountName string, databaseName string) *documentdb.ThroughputSettingsGetResults {\n\tcosmosSQLDBThroughput, err := GetCosmosDBSQLDatabaseThroughputE(t, subscriptionID, resourceGroupName, accountName, databaseName)\n\trequire.NoError(t, err)\n\n\treturn cosmosSQLDBThroughput\n}\n\n// GetCosmosDBSQLDatabaseThroughputE is a helper function that gets a SQL database throughput configuration.\nfunc GetCosmosDBSQLDatabaseThroughputE(t testing.TestingT, subscriptionID string, resourceGroupName string, accountName string, databaseName string) (*documentdb.ThroughputSettingsGetResults, error) {\n\t// Create a CosmosDB client\n\tcosmosClient, err := GetCosmosDBSQLClientE(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Get the corresponding database throughput config\n\tcosmosSQLDBThroughput, err := cosmosClient.GetSQLDatabaseThroughput(context.Background(), resourceGroupName, accountName, databaseName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t//Return throughput config\n\treturn &cosmosSQLDBThroughput, nil\n}\n\n// GetCosmosDBSQLContainerThroughput is a helper function that gets a SQL container throughput configuration. This function would fail the test if there is an error.\nfunc GetCosmosDBSQLContainerThroughput(t testing.TestingT, subscriptionID string, resourceGroupName string, accountName string, databaseName string, containerName string) *documentdb.ThroughputSettingsGetResults {\n\tcosmosSQLCtrThroughput, err := GetCosmosDBSQLContainerThroughputE(t, subscriptionID, resourceGroupName, accountName, databaseName, containerName)\n\trequire.NoError(t, err)\n\n\treturn cosmosSQLCtrThroughput\n}\n\n// GetCosmosDBSQLContainerThroughputE is a helper function that gets a SQL container throughput configuration.\nfunc GetCosmosDBSQLContainerThroughputE(t testing.TestingT, subscriptionID string, resourceGroupName string, accountName string, databaseName string, containerName string) (*documentdb.ThroughputSettingsGetResults, error) {\n\t// Create a CosmosDB client\n\tcosmosClient, err := GetCosmosDBSQLClientE(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Get the corresponding container throughput config\n\tcosmosSQLCtrThroughput, err := cosmosClient.GetSQLContainerThroughput(context.Background(), resourceGroupName, accountName, databaseName, containerName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t//Return throughput config\n\treturn &cosmosSQLCtrThroughput, nil\n}\n"
  },
  {
    "path": "modules/azure/datafactory.go",
    "content": "package azure\n\nimport (\n\t\"context\"\n\n\t\"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/datafactory/armdatafactory/v9\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// DataFactoryExists indicates whether the Data Factory exists for the subscription.\n// This function would fail the test if there is an error.\nfunc DataFactoryExists(t testing.TestingT, dataFactoryName string, resourceGroupName string, subscriptionID string) bool {\n\texists, err := DataFactoryExistsE(dataFactoryName, resourceGroupName, subscriptionID)\n\trequire.NoError(t, err)\n\treturn exists\n}\n\n// DataFactoryExistsE indicates whether the specified Data Factory exists and may return an error.\nfunc DataFactoryExistsE(dataFactoryName string, resourceGroupName string, subscriptionID string) (bool, error) {\n\t_, err := GetDataFactoryE(subscriptionID, resourceGroupName, dataFactoryName)\n\tif err != nil {\n\t\tif ResourceNotFoundErrorExists(err) {\n\t\t\treturn false, nil\n\t\t}\n\t\treturn false, err\n\t}\n\treturn true, nil\n}\n\n// GetDataFactory is a helper function that gets the data factory.\n// This function would fail the test if there is an error.\nfunc GetDataFactory(t testing.TestingT, resGroupName string, factoryName string, subscriptionID string) *armdatafactory.Factory {\n\tfactory, err := GetDataFactoryE(subscriptionID, resGroupName, factoryName)\n\trequire.NoError(t, err)\n\n\treturn factory\n}\n\n// GetDataFactoryE is a helper function that gets the data factory.\nfunc GetDataFactoryE(subscriptionID string, resGroupName string, factoryName string) (*armdatafactory.Factory, error) {\n\t// Create a datafactory client\n\tdatafactoryClient, err := CreateDataFactoriesClientE(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Get the corresponding data factory\n\tresp, err := datafactoryClient.Get(context.Background(), resGroupName, factoryName, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Return data factory\n\treturn &resp.Factory, nil\n}\n"
  },
  {
    "path": "modules/azure/datafactory_test.go",
    "content": "package azure\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\n/*\nThe below tests are currently stubbed out, with the expectation that they will throw errors.\nIf/when CRUD methods are introduced for Azure Synapse, these tests can be extended\n*/\nfunc TestDataFactoryExists(t *testing.T) {\n\tt.Parallel()\n\n\tdataFactoryName := \"\"\n\tresourceGroupName := \"\"\n\tsubscriptionID := \"\"\n\n\texists, err := DataFactoryExistsE(dataFactoryName, resourceGroupName, subscriptionID)\n\n\trequire.False(t, exists)\n\trequire.Error(t, err)\n}\n\nfunc TestGetDataFactoryE(t *testing.T) {\n\tt.Parallel()\n\n\tresGroupName := \"\"\n\tsubscriptionID := \"\"\n\tdataFactoryName := \"\"\n\n\t_, err := GetDataFactoryE(subscriptionID, resGroupName, dataFactoryName)\n\trequire.Error(t, err)\n}\n"
  },
  {
    "path": "modules/azure/disk.go",
    "content": "package azure\n\nimport (\n\t\"context\"\n\n\t\"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2019-07-01/compute\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// DiskExists indicates whether the specified Azure Managed Disk exists\n// This function would fail the test if there is an error.\nfunc DiskExists(t testing.TestingT, diskName string, resGroupName string, subscriptionID string) bool {\n\texists, err := DiskExistsE(diskName, resGroupName, subscriptionID)\n\trequire.NoError(t, err)\n\treturn exists\n}\n\n// DiskExistsE indicates whether the specified Azure Managed Disk exists in the specified Azure Resource Group\nfunc DiskExistsE(diskName string, resGroupName string, subscriptionID string) (bool, error) {\n\t// Get the Disk object\n\t_, err := GetDiskE(diskName, resGroupName, subscriptionID)\n\tif err != nil {\n\t\tif ResourceNotFoundErrorExists(err) {\n\t\t\treturn false, nil\n\t\t}\n\t\treturn false, err\n\t}\n\treturn true, nil\n}\n\n// GetDisk returns a Disk in the specified Azure Resource Group\n// This function would fail the test if there is an error.\nfunc GetDisk(t testing.TestingT, diskName string, resGroupName string, subscriptionID string) *compute.Disk {\n\tdisk, err := GetDiskE(diskName, resGroupName, subscriptionID)\n\trequire.NoError(t, err)\n\treturn disk\n}\n\n// GetDiskE returns a Disk in the specified Azure Resource Group\nfunc GetDiskE(diskName string, resGroupName string, subscriptionID string) (*compute.Disk, error) {\n\t// Validate resource group name and subscription ID\n\tresGroupName, err := getTargetAzureResourceGroupName(resGroupName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Get the client reference\n\tclient, err := CreateDisksClientE(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Get the Disk\n\tdisk, err := client.Get(context.Background(), resGroupName, diskName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &disk, nil\n}\n\n// GetDiskClientE returns a new Disk client in the specified Azure Subscription\n// TODO: remove in next major/minor version\nfunc GetDiskClientE(subscriptionID string) (*compute.DisksClient, error) {\n\t// Validate Azure subscription ID\n\tsubscriptionID, err := getTargetAzureSubscription(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Get the Disk client\n\tclient := compute.NewDisksClient(subscriptionID)\n\n\t// Create an authorizer\n\tauthorizer, err := NewAuthorizer()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tclient.Authorizer = *authorizer\n\n\treturn &client, nil\n}\n"
  },
  {
    "path": "modules/azure/disk_test.go",
    "content": "//go:build azure\n// +build azure\n\n// NOTE: We use build tags to differentiate azure testing because we currently do not have azure access setup for\n// CircleCI.\n\npackage azure\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestGetDiskE(t *testing.T) {\n\tt.Parallel()\n\n\tdiskName := \"\"\n\trgName := \"\"\n\tsubID := \"\"\n\n\t_, err := GetDiskE(diskName, rgName, subID)\n\n\trequire.Error(t, err)\n}\n"
  },
  {
    "path": "modules/azure/enums.go",
    "content": "package azure\n\n// LoadBalancerIPType enumerator for types Public, Private or No IP.\ntype LoadBalancerIPType string\n\n// LoadBalancerIPType values\nconst (\n\tPublicIP  LoadBalancerIPType = \"PublicIP\"\n\tPrivateIP LoadBalancerIPType = \"PrivateIP\"\n\tNoIP      LoadBalancerIPType = \"NoIP\"\n)\n"
  },
  {
    "path": "modules/azure/errors.go",
    "content": "package azure\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/Azure/go-autorest/autorest\"\n\t\"github.com/Azure/go-autorest/autorest/azure\"\n)\n\n// SubscriptionIDNotFound is an error that occurs when the Azure Subscription ID could not be found or was not provided\ntype SubscriptionIDNotFound struct{}\n\nfunc (err SubscriptionIDNotFound) Error() string {\n\treturn fmt.Sprintf(\"Could not find an Azure Subscription ID in expected environment variable %s and one was not provided for this test.\", AzureSubscriptionID)\n}\n\n// ResourceGroupNameNotFound is an error that occurs when the target Azure Resource Group name could not be found or was not provided\ntype ResourceGroupNameNotFound struct{}\n\nfunc (err ResourceGroupNameNotFound) Error() string {\n\treturn fmt.Sprintf(\"Could not find an Azure Resource Group name in expected environment variable %s and one was not provided for this test.\", AzureResGroupName)\n}\n\n// FailedToParseError is returned when an object cannot be parsed\ntype FailedToParseError struct {\n\tobjectType string\n\tobjectID   string\n}\n\nfunc (err FailedToParseError) Error() string {\n\treturn fmt.Sprintf(\"Failed to parse %s with ID %s\", err.objectType, err.objectID)\n}\n\n// NewFailedToParseError creates a new not found error when an expected object is not found in the search space\nfunc NewFailedToParseError(objectType string, objectID string) FailedToParseError {\n\treturn FailedToParseError{objectType, objectID}\n}\n\n// NotFoundError is returned when an expected object is not found in the search space\ntype NotFoundError struct {\n\tobjectType  string\n\tobjectID    string\n\tsearchSpace string\n}\n\nfunc (err NotFoundError) Error() string {\n\tvar objIDMsg string\n\n\tif err.objectID != \"Any\" {\n\t\tobjIDMsg = fmt.Sprintf(\" with id %s\", err.objectID)\n\t}\n\n\treturn fmt.Sprintf(\"Object of type %s%s not found in %s\", err.objectType, objIDMsg, err.searchSpace)\n}\n\n// NewNotFoundError creates a new not found error when an expected object is not found in the search space\nfunc NewNotFoundError(objectType string, objectID string, region string) NotFoundError {\n\treturn NotFoundError{objectType, objectID, region}\n}\n\n// ResourceNotFoundErrorExists checks the Service Error Code for the 'Resource Not Found' error\nfunc ResourceNotFoundErrorExists(err error) bool {\n\tif err != nil {\n\t\tif autorestError, ok := err.(autorest.DetailedError); ok {\n\t\t\tif requestError, ok := autorestError.Original.(*azure.RequestError); ok {\n\t\t\t\treturn (requestError.ServiceError.Code == \"ResourceNotFound\")\n\t\t\t}\n\t\t}\n\t}\n\treturn false\n}\n"
  },
  {
    "path": "modules/azure/frontdoor.go",
    "content": "package azure\n\nimport (\n\t\"context\"\n\n\t\"github.com/Azure/azure-sdk-for-go/profiles/latest/frontdoor/mgmt/frontdoor\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// FrontDoorExists indicates whether the Front Door exists for the subscription.\n// This function would fail the test if there is an error.\nfunc FrontDoorExists(t testing.TestingT, frontDoorName string, resourceGroupName string, subscriptionID string) bool {\n\texists, err := FrontDoorExistsE(frontDoorName, resourceGroupName, subscriptionID)\n\trequire.NoError(t, err)\n\treturn exists\n}\n\n// GetFrontDoor gets a Front Door by name if it exists for the subscription.\n// This function would fail the test if there is an error.\nfunc GetFrontDoor(t testing.TestingT, frontDoorName string, resourceGroupName string, subscriptionID string) *frontdoor.FrontDoor {\n\tfd, err := GetFrontDoorE(frontDoorName, resourceGroupName, subscriptionID)\n\trequire.NoError(t, err)\n\treturn fd\n}\n\n// FrontDoorFrontendEndpointExists indicates whether the frontend endpoint exists for the provided Front Door.\n// This function would fail the test if there is an error.\nfunc FrontDoorFrontendEndpointExists(t testing.TestingT, endpointName string, frontDoorName string, resourceGroupName string, subscriptionID string) bool {\n\texists, err := FrontDoorFrontendEndpointExistsE(endpointName, frontDoorName, resourceGroupName, subscriptionID)\n\trequire.NoError(t, err)\n\treturn exists\n}\n\n// GetFrontDoorFrontendEndpoint gets a frontend endpoint by name for the provided Front Door if it exists for the subscription.\n// This function would fail the test if there is an error.\nfunc GetFrontDoorFrontendEndpoint(t testing.TestingT, endpointName string, frontDoorName string, resourceGroupName string, subscriptionID string) *frontdoor.FrontendEndpoint {\n\tep, err := GetFrontDoorFrontendEndpointE(endpointName, frontDoorName, resourceGroupName, subscriptionID)\n\trequire.NoError(t, err)\n\treturn ep\n}\n\n// FrontDoorExistsE indicates whether the specified Front Door exists and may return an error.\nfunc FrontDoorExistsE(frontDoorName string, resourceGroupName string, subscriptionID string) (bool, error) {\n\t_, err := GetFrontDoorE(frontDoorName, resourceGroupName, subscriptionID)\n\tif err != nil {\n\t\tif ResourceNotFoundErrorExists(err) {\n\t\t\treturn false, nil\n\t\t}\n\t\treturn false, err\n\t}\n\treturn true, nil\n}\n\n// FrontDoorFrontendEndpointExistsE indicates whether the specified endpoint exists for the provided Front Door and may return an error.\nfunc FrontDoorFrontendEndpointExistsE(endpointName string, frontDoorName string, resourceGroupName string, subscriptionID string) (bool, error) {\n\t_, err := GetFrontDoorFrontendEndpointE(endpointName, frontDoorName, resourceGroupName, subscriptionID)\n\tif err != nil {\n\t\tif ResourceNotFoundErrorExists(err) {\n\t\t\treturn false, nil\n\t\t}\n\t\treturn false, err\n\t}\n\treturn true, nil\n}\n\n// GetFrontDoorE gets the specified Front Door if it exists and may return an error.\nfunc GetFrontDoorE(frontDoorName, resourceGroupName, subscriptionID string) (*frontdoor.FrontDoor, error) {\n\tclient, err := GetFrontDoorClientE(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfd, err := client.Get(context.Background(), resourceGroupName, frontDoorName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &fd, nil\n}\n\n// GetFrontDoorFrontendEndpointE gets the specified Frontend Endpoint for the provided Front Door if it exists and may return an error.\nfunc GetFrontDoorFrontendEndpointE(endpointName, frontDoorName, resourceGroupName, subscriptionID string) (*frontdoor.FrontendEndpoint, error) {\n\tclient, err := GetFrontDoorFrontendEndpointClientE(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tendpoint, err := client.Get(context.Background(), resourceGroupName, frontDoorName, endpointName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &endpoint, nil\n}\n\n// GetFrontDoorClientE return a front door client; otherwise error.\nfunc GetFrontDoorClientE(subscriptionID string) (*frontdoor.FrontDoorsClient, error) {\n\tclient, err := CreateFrontDoorClientE(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tauthorizer, err := NewAuthorizer()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tclient.Authorizer = *authorizer\n\treturn client, nil\n}\n\n// GetFrontDoorFrontendEndpointClientE returns a front door frontend endpoints client; otherwise error.\nfunc GetFrontDoorFrontendEndpointClientE(subscriptionID string) (*frontdoor.FrontendEndpointsClient, error) {\n\tclient, err := CreateFrontDoorFrontendEndpointClientE(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tauthorizer, err := NewAuthorizer()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tclient.Authorizer = *authorizer\n\treturn client, nil\n}\n"
  },
  {
    "path": "modules/azure/frontdoor_test.go",
    "content": "//go:build azure\n// +build azure\n\n// NOTE: We use build tags to differentiate azure testing because we currently do not have azure access setup for\n// CircleCI.\n\npackage azure\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\n/*\nThe below tests are currently stubbed out, with the expectation that they will throw errors.\nIf/when methods to create and delete front door are added, these tests can be extended.\n*/\n\nfunc TestFrontDoorExists(t *testing.T) {\n\tt.Parallel()\n\n\tfrontDoorName := \"TestFrontDoor\"\n\tresourceGroupName := \"TestResourceGroup\"\n\tsubscriptionID := \"\"\n\n\texists, err := FrontDoorExistsE(frontDoorName, resourceGroupName, subscriptionID)\n\n\trequire.False(t, exists)\n\trequire.Error(t, err)\n}\n\nfunc TestGetFrontDoor(t *testing.T) {\n\tt.Parallel()\n\n\tfrontDoorName := \"TestFrontDoor\"\n\tresourceGroupName := \"TestResourceGroup\"\n\tsubscriptionID := \"\"\n\n\tinstance, err := GetFrontDoorE(frontDoorName, resourceGroupName, subscriptionID)\n\n\trequire.Nil(t, instance)\n\trequire.Error(t, err)\n}\n\nfunc TestFrontDoorFrontendEndpointExists(t *testing.T) {\n\tt.Parallel()\n\n\tendpointName := \"TestFrontendEndpoint\"\n\tfrontDoorName := \"TestFrontDoor\"\n\tresourceGroupName := \"TestResourceGroup\"\n\tsubscriptionID := \"\"\n\n\tendpoint, err := FrontDoorFrontendEndpointExistsE(endpointName, frontDoorName, resourceGroupName, subscriptionID)\n\n\trequire.False(t, endpoint)\n\trequire.Error(t, err)\n}\n\nfunc TestGetFrontDoorFrontendEndpoint(t *testing.T) {\n\tt.Parallel()\n\n\tendpointName := \"TestFrontendEndpoint\"\n\tfrontDoorName := \"TestFrontDoor\"\n\tresourceGroupName := \"TestResourceGroup\"\n\tsubscriptionID := \"\"\n\n\tendpoint, err := GetFrontDoorFrontendEndpointE(endpointName, frontDoorName, resourceGroupName, subscriptionID)\n\n\trequire.Nil(t, endpoint)\n\trequire.Error(t, err)\n}\n"
  },
  {
    "path": "modules/azure/keyvault.go",
    "content": "package azure\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/Azure/azure-sdk-for-go/sdk/azidentity\"\n\t\"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault\"\n\t\"github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azcertificates\"\n\t\"github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys\"\n\t\"github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// NewAzureCredentialE creates a new Azure credential using DefaultAzureCredential.\nfunc NewAzureCredentialE() (*azidentity.DefaultAzureCredential, error) {\n\treturn azidentity.NewDefaultAzureCredential(nil)\n}\n\n// KeyVaultSecretExists indicates whether a key vault secret exists; otherwise false\n// This function would fail the test if there is an error.\nfunc KeyVaultSecretExists(t *testing.T, keyVaultName string, secretName string) bool {\n\tresult, err := KeyVaultSecretExistsE(keyVaultName, secretName)\n\trequire.NoError(t, err)\n\treturn result\n}\n\n// KeyVaultKeyExists indicates whether a key vault key exists; otherwise false.\n// This function would fail the test if there is an error.\nfunc KeyVaultKeyExists(t *testing.T, keyVaultName string, keyName string) bool {\n\tresult, err := KeyVaultKeyExistsE(keyVaultName, keyName)\n\trequire.NoError(t, err)\n\treturn result\n}\n\n// KeyVaultCertificateExists indicates whether a key vault certificate exists; otherwise false.\n// This function would fail the test if there is an error.\nfunc KeyVaultCertificateExists(t *testing.T, keyVaultName string, certificateName string) bool {\n\tresult, err := KeyVaultCertificateExistsE(keyVaultName, certificateName)\n\trequire.NoError(t, err)\n\treturn result\n}\n\n// KeyVaultCertificateExistsE indicates whether a certificate exists in key vault; otherwise false.\nfunc KeyVaultCertificateExistsE(keyVaultName, certificateName string) (bool, error) {\n\tclient, err := GetKeyVaultCertificatesClientE(keyVaultName)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tpager := client.NewListCertificatePropertiesVersionsPager(certificateName, nil)\n\tif pager.More() {\n\t\t_, err := pager.NextPage(context.Background())\n\t\tif err != nil {\n\t\t\tif ResourceNotFoundErrorExists(err) {\n\t\t\t\treturn false, nil\n\t\t\t}\n\t\t\treturn false, err\n\t\t}\n\t\treturn true, nil\n\t}\n\treturn false, nil\n}\n\n// KeyVaultKeyExistsE indicates whether a key exists in the key vault; otherwise false.\nfunc KeyVaultKeyExistsE(keyVaultName, keyName string) (bool, error) {\n\tclient, err := GetKeyVaultKeysClientE(keyVaultName)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tpager := client.NewListKeyPropertiesVersionsPager(keyName, nil)\n\tif pager.More() {\n\t\t_, err := pager.NextPage(context.Background())\n\t\tif err != nil {\n\t\t\tif ResourceNotFoundErrorExists(err) {\n\t\t\t\treturn false, nil\n\t\t\t}\n\t\t\treturn false, err\n\t\t}\n\t\treturn true, nil\n\t}\n\treturn false, nil\n}\n\n// KeyVaultSecretExistsE indicates whether a secret exists in the key vault; otherwise false.\nfunc KeyVaultSecretExistsE(keyVaultName, secretName string) (bool, error) {\n\tclient, err := GetKeyVaultSecretsClientE(keyVaultName)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tpager := client.NewListSecretPropertiesVersionsPager(secretName, nil)\n\tif pager.More() {\n\t\t_, err := pager.NextPage(context.Background())\n\t\tif err != nil {\n\t\t\tif ResourceNotFoundErrorExists(err) {\n\t\t\t\treturn false, nil\n\t\t\t}\n\t\t\treturn false, err\n\t\t}\n\t\treturn true, nil\n\t}\n\treturn false, nil\n}\n\n// GetKeyVaultSecretsClientE creates a KeyVault secrets client.\nfunc GetKeyVaultSecretsClientE(keyVaultName string) (*azsecrets.Client, error) {\n\tkeyVaultSuffix, err := GetKeyVaultURISuffixE()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvaultURL := fmt.Sprintf(\"https://%s.%s\", keyVaultName, keyVaultSuffix)\n\n\tcred, err := NewAzureCredentialE()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn azsecrets.NewClient(vaultURL, cred, nil)\n}\n\n// GetKeyVaultKeysClientE creates a KeyVault keys client.\nfunc GetKeyVaultKeysClientE(keyVaultName string) (*azkeys.Client, error) {\n\tkeyVaultSuffix, err := GetKeyVaultURISuffixE()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvaultURL := fmt.Sprintf(\"https://%s.%s\", keyVaultName, keyVaultSuffix)\n\n\tcred, err := NewAzureCredentialE()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn azkeys.NewClient(vaultURL, cred, nil)\n}\n\n// GetKeyVaultCertificatesClientE creates a KeyVault certificates client.\nfunc GetKeyVaultCertificatesClientE(keyVaultName string) (*azcertificates.Client, error) {\n\tkeyVaultSuffix, err := GetKeyVaultURISuffixE()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvaultURL := fmt.Sprintf(\"https://%s.%s\", keyVaultName, keyVaultSuffix)\n\n\tcred, err := NewAzureCredentialE()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn azcertificates.NewClient(vaultURL, cred, nil)\n}\n\n// GetKeyVault is a helper function that gets the keyvault management object.\n// This function would fail the test if there is an error.\nfunc GetKeyVault(t *testing.T, resGroupName string, keyVaultName string, subscriptionID string) *armkeyvault.Vault {\n\tkeyVault, err := GetKeyVaultE(t, resGroupName, keyVaultName, subscriptionID)\n\trequire.NoError(t, err)\n\n\treturn keyVault\n}\n\n// GetKeyVaultE is a helper function that gets the keyvault management object.\nfunc GetKeyVaultE(t *testing.T, resGroupName string, keyVaultName string, subscriptionID string) (*armkeyvault.Vault, error) {\n\t// Create a key vault management client\n\tvaultClient, err := GetKeyVaultManagementClientE(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Get the corresponding vault\n\tresp, err := vaultClient.Get(context.Background(), resGroupName, keyVaultName, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &resp.Vault, nil\n}\n\n// GetKeyVaultManagementClientE is a helper function that will setup a key vault management client\nfunc GetKeyVaultManagementClientE(subscriptionID string) (*armkeyvault.VaultsClient, error) {\n\tclientFactory, err := getArmKeyVaultClientFactory(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn clientFactory.NewVaultsClient(), nil\n}\n"
  },
  {
    "path": "modules/azure/keyvault_test.go",
    "content": "package azure\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\n/*\nThe below tests are currently stubbed out, with the expectation that they will throw errors.\nIf/when methods to create and delete key vault resources are added, these tests can be extended.\n*/\n\nfunc TestKeyVaultSecretExists(t *testing.T) {\n\tt.Parallel()\n\n\ttestKeyVaultName := \"fakeKeyVault\"\n\ttestKeyVaultSecretName := \"fakeSecretName\"\n\t_, err := KeyVaultSecretExistsE(testKeyVaultName, testKeyVaultSecretName)\n\trequire.Error(t, err)\n}\n\nfunc TestKeyVaultKeyExists(t *testing.T) {\n\tt.Parallel()\n\n\ttestKeyVaultName := \"fakeKeyVault\"\n\ttestKeyVaultKeyName := \"fakeKeyName\"\n\t_, err := KeyVaultKeyExistsE(testKeyVaultName, testKeyVaultKeyName)\n\trequire.Error(t, err)\n}\n\nfunc TestKeyVaultCertificateExists(t *testing.T) {\n\tt.Parallel()\n\n\ttestKeyVaultName := \"fakeKeyVault\"\n\ttestKeyVaultCertName := \"fakeCertName\"\n\t_, err := KeyVaultCertificateExistsE(testKeyVaultName, testKeyVaultCertName)\n\trequire.Error(t, err)\n}\n\nfunc TestGetKeyVault(t *testing.T) {\n\tt.Parallel()\n\n\tresGroupName := \"\"\n\tkeyVaultName := \"\"\n\tsubscriptionID := \"\"\n\n\t_, err := GetKeyVaultE(t, resGroupName, keyVaultName, subscriptionID)\n\trequire.Error(t, err)\n}\n"
  },
  {
    "path": "modules/azure/loadbalancer.go",
    "content": "package azure\n\nimport (\n\t\"context\"\n\n\t\"github.com/Azure/azure-sdk-for-go/services/network/mgmt/2019-09-01/network\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// LoadBalancerExists indicates whether the specified Load Balancer exists.\n// This function would fail the test if there is an error.\nfunc LoadBalancerExists(t testing.TestingT, loadBalancerName string, resourceGroupName string, subscriptionID string) bool {\n\texists, err := LoadBalancerExistsE(loadBalancerName, resourceGroupName, subscriptionID)\n\trequire.NoError(t, err)\n\treturn exists\n}\n\n// LoadBalancerExistsE indicates whether the specified Load Balancer exists.\nfunc LoadBalancerExistsE(loadBalancerName string, resourceGroupName string, subscriptionID string) (bool, error) {\n\t_, err := GetLoadBalancerE(loadBalancerName, resourceGroupName, subscriptionID)\n\tif err != nil {\n\t\tif ResourceNotFoundErrorExists(err) {\n\t\t\treturn false, nil\n\t\t}\n\t\treturn false, err\n\t}\n\treturn true, nil\n}\n\n// GetLoadBalancerFrontendIPConfigNames gets a list of the Frontend IP Configuration Names for the Load Balancer.\n// This function would fail the test if there is an error.\nfunc GetLoadBalancerFrontendIPConfigNames(t testing.TestingT, loadBalancerName string, resourceGroupName string, subscriptionID string) []string {\n\tconfigName, err := GetLoadBalancerFrontendIPConfigNamesE(loadBalancerName, resourceGroupName, subscriptionID)\n\trequire.NoError(t, err)\n\treturn configName\n}\n\n// GetLoadBalancerFrontendIPConfigNamesE gets a list of the Frontend IP Configuration Names for the Load Balancer.\nfunc GetLoadBalancerFrontendIPConfigNamesE(loadBalancerName string, resourceGroupName string, subscriptionID string) ([]string, error) {\n\tlb, err := GetLoadBalancerE(loadBalancerName, resourceGroupName, subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Get the Frontend IP Configurations\n\tlbProps := lb.LoadBalancerPropertiesFormat\n\tfeConfigs := *lbProps.FrontendIPConfigurations\n\tif len(feConfigs) == 0 {\n\t\t// No Frontend IP Configuration present\n\t\treturn nil, nil\n\t}\n\n\t// Get the names of the Frontend IP Configurations present\n\tconfigNames := make([]string, len(feConfigs))\n\tfor i, config := range feConfigs {\n\t\tconfigNames[i] = *config.Name\n\t}\n\n\treturn configNames, nil\n}\n\n// GetIPOfLoadBalancerFrontendIPConfig gets the IP and LoadBalancerIPType for the specified Load Balancer Frontend IP Configuration.\n// This function would fail the test if there is an error.\nfunc GetIPOfLoadBalancerFrontendIPConfig(t testing.TestingT, feConfigName string, loadBalancerName string, resourceGroupName string, subscriptionID string) (ipAddress string, publicOrPrivate LoadBalancerIPType) {\n\tipAddress, ipType, err := GetIPOfLoadBalancerFrontendIPConfigE(feConfigName, loadBalancerName, resourceGroupName, subscriptionID)\n\trequire.NoError(t, err)\n\treturn ipAddress, ipType\n}\n\n// GetIPOfLoadBalancerFrontendIPConfigE gets the IP and LoadBalancerIPType for the specified Load Balancer Frontend IP Configuration.\nfunc GetIPOfLoadBalancerFrontendIPConfigE(feConfigName string, loadBalancerName string, resourceGroupName string, subscriptionID string) (ipAddress string, publicOrPrivate LoadBalancerIPType, err1 error) {\n\t// Get the specified Load Balancer Frontend Config\n\tfeConfig, err := GetLoadBalancerFrontendIPConfigE(feConfigName, loadBalancerName, resourceGroupName, subscriptionID)\n\tif err != nil {\n\t\treturn \"\", NoIP, err\n\t}\n\n\t// Get the Properties of the Frontend Configuration\n\tfeProps := *feConfig.FrontendIPConfigurationPropertiesFormat\n\n\t// Check for the Public Type Frontend Config\n\tif feProps.PublicIPAddress != nil {\n\t\t// Get PublicIPAddress resource name from the Load Balancer Frontend Configuration\n\t\tpipName := GetNameFromResourceID(*feProps.PublicIPAddress.ID)\n\n\t\t// Get the Public IP of the PublicIPAddress\n\t\tipValue, err := GetIPOfPublicIPAddressByNameE(pipName, resourceGroupName, subscriptionID)\n\t\tif err != nil {\n\t\t\treturn \"\", NoIP, err\n\t\t}\n\n\t\treturn ipValue, PublicIP, nil\n\t}\n\n\t// Return the Private IP as there are no other option available\n\treturn *feProps.PrivateIPAddress, PrivateIP, nil\n\n}\n\n// GetLoadBalancerFrontendIPConfig gets the specified Load Balancer Frontend IP Configuration network resource.\n// This function would fail the test if there is an error.\nfunc GetLoadBalancerFrontendIPConfig(t testing.TestingT, feConfigName string, loadBalancerName string, resourceGroupName string, subscriptionID string) *network.FrontendIPConfiguration {\n\tlbFEConfig, err := GetLoadBalancerFrontendIPConfigE(feConfigName, loadBalancerName, resourceGroupName, subscriptionID)\n\trequire.NoError(t, err)\n\treturn lbFEConfig\n}\n\n// GetLoadBalancerFrontendIPConfigE gets the specified Load Balancer Frontend IP Configuration network resource.\nfunc GetLoadBalancerFrontendIPConfigE(feConfigName string, loadBalancerName string, resourceGroupName string, subscriptionID string) (*network.FrontendIPConfiguration, error) {\n\t// Validate Azure Resource Group Name\n\tresourceGroupName, err := getTargetAzureResourceGroupName(resourceGroupName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Get the client reference\n\tclient, err := GetLoadBalancerFrontendIPConfigClientE(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Get the Load Balancer Frontend IP Configuration\n\tlbc, err := client.Get(context.Background(), resourceGroupName, loadBalancerName, feConfigName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &lbc, nil\n}\n\n// GetLoadBalancerFrontendIPConfigClientE gets a new Load Balancer Frontend IP Configuration client in the specified Azure Subscription.\nfunc GetLoadBalancerFrontendIPConfigClientE(subscriptionID string) (*network.LoadBalancerFrontendIPConfigurationsClient, error) {\n\t// Validate Azure subscription ID\n\tsubscriptionID, err := getTargetAzureSubscription(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Get the Load Balancer Frontend Configuration client\n\tclient := network.NewLoadBalancerFrontendIPConfigurationsClient(subscriptionID)\n\n\t// Create an authorizer\n\tauthorizer, err := NewAuthorizer()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tclient.Authorizer = *authorizer\n\n\treturn &client, nil\n}\n\n// GetLoadBalancer gets a Load Balancer network resource in the specified Azure Resource Group.\n// This function would fail the test if there is an error.\nfunc GetLoadBalancer(t testing.TestingT, loadBalancerName string, resourceGroupName string, subscriptionID string) *network.LoadBalancer {\n\tlb, err := GetLoadBalancerE(loadBalancerName, resourceGroupName, subscriptionID)\n\trequire.NoError(t, err)\n\treturn lb\n}\n\n// GetLoadBalancerE gets a Load Balancer network resource in the specified Azure Resource Group.\nfunc GetLoadBalancerE(loadBalancerName string, resourceGroupName string, subscriptionID string) (*network.LoadBalancer, error) {\n\t// Validate Azure Resource Group Name\n\tresourceGroupName, err := getTargetAzureResourceGroupName(resourceGroupName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Get the client reference\n\tclient, err := GetLoadBalancerClientE(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Get the Load Balancer\n\tlb, err := client.Get(context.Background(), resourceGroupName, loadBalancerName, \"\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &lb, nil\n}\n\n// GetLoadBalancerClientE gets a new Load Balancer client in the specified Azure Subscription.\nfunc GetLoadBalancerClientE(subscriptionID string) (*network.LoadBalancersClient, error) {\n\t// Get the Load Balancer client\n\tclient, err := CreateLoadBalancerClientE(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Create an authorizer\n\tauthorizer, err := NewAuthorizer()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tclient.Authorizer = *authorizer\n\n\treturn client, nil\n}\n"
  },
  {
    "path": "modules/azure/loadbalancer_test.go",
    "content": "//go:build azure\n// +build azure\n\n// NOTE: We use build tags to differentiate azure testing because we currently do not have azure access setup for\n// CircleCI.\n\npackage azure\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\n/*\nThe below tests are currently stubbed out, with the expectation that they will throw errors.\nIf/when methods can be mocked or Create/Delete APIs are added, these tests can be extended.\n*/\n\nfunc TestLoadBalancerExistsE(t *testing.T) {\n\tt.Parallel()\n\n\tloadBalancerName := \"\"\n\tresourceGroupName := \"\"\n\tsubscriptionID := \"\"\n\n\t_, err := LoadBalancerExistsE(loadBalancerName, resourceGroupName, subscriptionID)\n\n\trequire.Error(t, err)\n}\n\nfunc TestGetLoadBalancerE(t *testing.T) {\n\tt.Parallel()\n\n\tloadBalancerName := \"\"\n\tresourceGroupName := \"\"\n\tsubscriptionID := \"\"\n\n\t_, err := GetLoadBalancerE(loadBalancerName, resourceGroupName, subscriptionID)\n\n\trequire.Error(t, err)\n}\n"
  },
  {
    "path": "modules/azure/loganalytics.go",
    "content": "package azure\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/Azure/azure-sdk-for-go/services/preview/operationalinsights/mgmt/2020-03-01-preview/operationalinsights\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// LogAnalyticsWorkspaceExists indicates whether the operatonal insights workspaces exists.\n// This function would fail the test if there is an error.\nfunc LogAnalyticsWorkspaceExists(t testing.TestingT, workspaceName string, resourceGroupName string, subscriptionID string) bool {\n\texists, err := LogAnalyticsWorkspaceExistsE(workspaceName, resourceGroupName, subscriptionID)\n\trequire.NoError(t, err)\n\treturn exists\n}\n\n// GetLogAnalyticsWorkspace gets an operational insights workspace if it exists in a subscription.\n// This function would fail the test if there is an error.\nfunc GetLogAnalyticsWorkspace(t testing.TestingT, workspaceName string, resourceGroupName string, subscriptionID string) *operationalinsights.Workspace {\n\tws, err := GetLogAnalyticsWorkspaceE(workspaceName, resourceGroupName, subscriptionID)\n\trequire.NoError(t, err)\n\n\treturn ws\n}\n\n// GetLogAnalyticsWorkspaceE gets an operational insights workspace if it exists in a subscription.\nfunc GetLogAnalyticsWorkspaceE(workspaceName, resoureGroupName, subscriptionID string) (*operationalinsights.Workspace, error) {\n\tclient, err := GetLogAnalyticsWorkspacesClientE(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tws, err := client.Get(context.Background(), resoureGroupName, workspaceName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &ws, nil\n}\n\n// LogAnalyticsWorkspaceExistsE indicates whether the operatonal insights workspaces exists and may return an error.\nfunc LogAnalyticsWorkspaceExistsE(workspaceName string, resourceGroupName string, subscriptionID string) (bool, error) {\n\t_, err := GetLogAnalyticsWorkspaceE(workspaceName, resourceGroupName, subscriptionID)\n\tif err != nil {\n\t\tif ResourceNotFoundErrorExists(err) {\n\t\t\treturn false, nil\n\t\t}\n\t\treturn false, err\n\t}\n\treturn true, nil\n}\n\n// GetLogAnalyticsWorkspacesClientE return workspaces client; otherwise error.\nfunc GetLogAnalyticsWorkspacesClientE(subscriptionID string) (*operationalinsights.WorkspacesClient, error) {\n\tsubscriptionID, err := getTargetAzureSubscription(subscriptionID)\n\tif err != nil {\n\t\tfmt.Println(\"Workspace client error getting subscription\")\n\t\treturn nil, err\n\t}\n\n\tclient := operationalinsights.NewWorkspacesClient(subscriptionID)\n\tauthorizer, err := NewAuthorizer()\n\tif err != nil {\n\t\tfmt.Println(\"authorizer error\")\n\t\treturn nil, err\n\t}\n\n\tclient.Authorizer = *authorizer\n\treturn &client, nil\n}\n"
  },
  {
    "path": "modules/azure/loganalytics_test.go",
    "content": "package azure\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n/*\nThe below tests are currently stubbed out, with the expectation that they will throw errors.\nIf/when methods to create and delete log analytics resources are added, these tests can be extended.\n*/\n\nfunc TestLogAnalyticsWorkspace(t *testing.T) {\n\tt.Parallel()\n\n\t_, err := LogAnalyticsWorkspaceExistsE(\"fake\", \"\", \"\")\n\tassert.Error(t, err, \"Workspace\")\n}\n\nfunc TestGetLogAnalyticsWorkspaceE(t *testing.T) {\n\tt.Parallel()\n\tworkspaceName := \"\"\n\tresourceGroupName := \"\"\n\tsubscriptionID := \"\"\n\n\t_, err := GetLogAnalyticsWorkspaceE(workspaceName, resourceGroupName, subscriptionID)\n\trequire.Error(t, err)\n}\n"
  },
  {
    "path": "modules/azure/monitor.go",
    "content": "package azure\n\nimport (\n\t\"context\"\n\n\t\"github.com/Azure/azure-sdk-for-go/profiles/preview/preview/monitor/mgmt/insights\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// DiagnosticSettingsResourceExists indicates whether the diagnostic settings resource exists\n// This function would fail the test if there is an error.\nfunc DiagnosticSettingsResourceExists(t testing.TestingT, diagnosticSettingsResourceName string, resourceURI string, subscriptionID string) bool {\n\texists, err := DiagnosticSettingsResourceExistsE(diagnosticSettingsResourceName, resourceURI, subscriptionID)\n\trequire.NoError(t, err)\n\n\treturn exists\n}\n\n// DiagnosticSettingsResourceExistsE indicates whether the diagnostic settings resource exists\nfunc DiagnosticSettingsResourceExistsE(diagnosticSettingsResourceName string, resourceURI string, subscriptionID string) (bool, error) {\n\t_, err := GetDiagnosticsSettingsResourceE(diagnosticSettingsResourceName, resourceURI, subscriptionID)\n\tif err != nil {\n\t\tif ResourceNotFoundErrorExists(err) {\n\t\t\treturn false, nil\n\t\t}\n\t\treturn false, err\n\t}\n\n\treturn true, nil\n}\n\n// GetDiagnosticsSettingsResource gets the diagnostics settings for a specified resource\n// This function would fail the test if there is an error.\nfunc GetDiagnosticsSettingsResource(t testing.TestingT, name string, resourceURI string, subscriptionID string) *insights.DiagnosticSettingsResource {\n\tresource, err := GetDiagnosticsSettingsResourceE(name, resourceURI, subscriptionID)\n\trequire.NoError(t, err)\n\treturn resource\n}\n\n// GetDiagnosticsSettingsResourceE gets the diagnostics settings for a specified resource\nfunc GetDiagnosticsSettingsResourceE(name string, resourceURI string, subscriptionID string) (*insights.DiagnosticSettingsResource, error) {\n\t// Validate Azure subscription ID\n\tsubscriptionID, err := getTargetAzureSubscription(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tclient, err := CreateDiagnosticsSettingsClientE(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tsettings, err := client.Get(context.Background(), resourceURI, name)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &settings, nil\n}\n\n// GetDiagnosticsSettingsClientE returns a diagnostics settings client\n// TODO: delete in next version\nfunc GetDiagnosticsSettingsClientE(subscriptionID string) (*insights.DiagnosticSettingsClient, error) {\n\t// Validate Azure subscription ID\n\tsubscriptionID, err := getTargetAzureSubscription(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tclient := insights.NewDiagnosticSettingsClient(subscriptionID)\n\tauthorizer, err := NewAuthorizer()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tclient.Authorizer = *authorizer\n\n\treturn &client, nil\n}\n\n// GetVMInsightsOnboardingStatus get diagnostics VM onboarding status\n// This function would fail the test if there is an error.\nfunc GetVMInsightsOnboardingStatus(t testing.TestingT, resourceURI string, subscriptionID string) *insights.VMInsightsOnboardingStatus {\n\tstatus, err := GetVMInsightsOnboardingStatusE(t, resourceURI, subscriptionID)\n\trequire.NoError(t, err)\n\n\treturn status\n}\n\n// GetVMInsightsOnboardingStatusE get diagnostics VM onboarding status\nfunc GetVMInsightsOnboardingStatusE(t testing.TestingT, resourceURI string, subscriptionID string) (*insights.VMInsightsOnboardingStatus, error) {\n\tclient, err := CreateVMInsightsClientE(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tstatus, err := client.GetOnboardingStatus(context.Background(), resourceURI)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &status, nil\n}\n\n// GetVMInsightsClientE gets a VM Insights client\n// TODO: delete in next version\nfunc GetVMInsightsClientE(t testing.TestingT, subscriptionID string) (*insights.VMInsightsClient, error) {\n\t// Validate Azure subscription ID\n\tsubscriptionID, err := getTargetAzureSubscription(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tclient := insights.NewVMInsightsClient(subscriptionID)\n\n\tauthorizer, err := NewAuthorizer()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tclient.Authorizer = *authorizer\n\n\treturn &client, nil\n}\n\n// GetActivityLogAlertResource gets a Action Group in the specified Azure Resource Group\n// This function would fail the test if there is an error.\nfunc GetActivityLogAlertResource(t testing.TestingT, activityLogAlertName string, resGroupName string, subscriptionID string) *insights.ActivityLogAlertResource {\n\tactivityLogAlertResource, err := GetActivityLogAlertResourceE(activityLogAlertName, resGroupName, subscriptionID)\n\trequire.NoError(t, err)\n\n\treturn activityLogAlertResource\n}\n\n// GetActivityLogAlertResourceE gets a Action Group in the specified Azure Resource Group\nfunc GetActivityLogAlertResourceE(activityLogAlertName string, resGroupName string, subscriptionID string) (*insights.ActivityLogAlertResource, error) {\n\t// Validate resource group name and subscription ID\n\t_, err := getTargetAzureResourceGroupName(resGroupName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Get the client reference\n\tclient, err := CreateActivityLogAlertsClientE(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Get the Action Group\n\tactivityLogAlertResource, err := client.Get(context.Background(), resGroupName, activityLogAlertName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &activityLogAlertResource, nil\n}\n\n// GetActivityLogAlertsClientE gets an Action Groups client in the specified Azure Subscription\n// TODO: delete in next version\nfunc GetActivityLogAlertsClientE(subscriptionID string) (*insights.ActivityLogAlertsClient, error) {\n\t// Validate Azure subscription ID\n\tsubscriptionID, err := getTargetAzureSubscription(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Get the Action Groups client\n\tclient := insights.NewActivityLogAlertsClient(subscriptionID)\n\n\t// Create an authorizer\n\tauthorizer, err := NewAuthorizer()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tclient.Authorizer = *authorizer\n\n\treturn &client, nil\n}\n"
  },
  {
    "path": "modules/azure/monitor_test.go",
    "content": "//go:build azure\n// +build azure\n\npackage azure\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestDiagnosticsSettingsResourceExists(t *testing.T) {\n\tt.Parallel()\n\n\tdiagnosticsSettingResourceName := \"fakename\"\n\tresGroupName := \"fakeresgroup\"\n\tsubscriptionID := \"fakesubid\"\n\n\t_, err := DiagnosticSettingsResourceExistsE(diagnosticsSettingResourceName, resGroupName, subscriptionID)\n\trequire.Error(t, err)\n}\n"
  },
  {
    "path": "modules/azure/mysql.go",
    "content": "package azure\n\nimport (\n\t\"context\"\n\n\t\"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/mysql/armmysql\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// GetMYSQLServerClientE is a helper function that will setup a mysql server client.\nfunc GetMYSQLServerClientE(subscriptionID string) (*armmysql.ServersClient, error) {\n\tclientFactory, err := getArmMySQLClientFactory(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn clientFactory.NewServersClient(), nil\n}\n\n// GetMYSQLServer is a helper function that gets the server.\n// This function would fail the test if there is an error.\nfunc GetMYSQLServer(t testing.TestingT, resGroupName string, serverName string, subscriptionID string) *armmysql.Server {\n\tmysqlServer, err := GetMYSQLServerE(t, subscriptionID, resGroupName, serverName)\n\trequire.NoError(t, err)\n\n\treturn mysqlServer\n}\n\n// GetMYSQLServerE is a helper function that gets the server.\nfunc GetMYSQLServerE(t testing.TestingT, subscriptionID string, resGroupName string, serverName string) (*armmysql.Server, error) {\n\t// Create a MySQL Server client\n\tmysqlClient, err := CreateMySQLServerClientE(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Get the corresponding server\n\tresp, err := mysqlClient.Get(context.Background(), resGroupName, serverName, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &resp.Server, nil\n}\n\n// GetMYSQLDBClientE is a helper function that will setup a mysql DB client.\nfunc GetMYSQLDBClientE(subscriptionID string) (*armmysql.DatabasesClient, error) {\n\tclientFactory, err := getArmMySQLClientFactory(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn clientFactory.NewDatabasesClient(), nil\n}\n\n// GetMYSQLDB is a helper function that gets the database.\n// This function would fail the test if there is an error.\nfunc GetMYSQLDB(t testing.TestingT, resGroupName string, serverName string, dbName string, subscriptionID string) *armmysql.Database {\n\tdatabase, err := GetMYSQLDBE(t, subscriptionID, resGroupName, serverName, dbName)\n\trequire.NoError(t, err)\n\n\treturn database\n}\n\n// GetMYSQLDBE is a helper function that gets the database.\nfunc GetMYSQLDBE(t testing.TestingT, subscriptionID string, resGroupName string, serverName string, dbName string) (*armmysql.Database, error) {\n\t// Create a MySQL db client\n\tmysqldbClient, err := GetMYSQLDBClientE(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Get the corresponding db\n\tresp, err := mysqldbClient.Get(context.Background(), resGroupName, serverName, dbName, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &resp.Database, nil\n}\n\n// ListMySQLDB is a helper function that gets all databases per server.\nfunc ListMySQLDB(t testing.TestingT, resGroupName string, serverName string, subscriptionID string) []*armmysql.Database {\n\tdblist, err := ListMySQLDBE(t, subscriptionID, resGroupName, serverName)\n\trequire.NoError(t, err)\n\n\treturn dblist\n}\n\n// ListMySQLDBE is a helper function that gets all databases per server.\nfunc ListMySQLDBE(t testing.TestingT, subscriptionID string, resGroupName string, serverName string) ([]*armmysql.Database, error) {\n\t// Create a MySQL db client\n\tmysqldbClient, err := GetMYSQLDBClientE(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Get the databases using pager\n\tpager := mysqldbClient.NewListByServerPager(resGroupName, serverName, nil)\n\tvar databases []*armmysql.Database\n\tfor pager.More() {\n\t\tpage, err := pager.NextPage(context.Background())\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tdatabases = append(databases, page.Value...)\n\t}\n\n\treturn databases, nil\n}\n"
  },
  {
    "path": "modules/azure/mysql_test.go",
    "content": "//go:build azure\n// +build azure\n\n// NOTE: We use build tags to differentiate azure testing because we currently do not have azure access setup for\n// CircleCI.\n\npackage azure\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\n/*\nThe below tests are currently stubbed out, with the expectation that they will throw errors.\nIf/when CRUD methods are introduced for Azure MySQL server and database, these tests can be extended\n*/\n\nfunc TestGetMYSQLServerE(t *testing.T) {\n\tt.Parallel()\n\n\tresGroupName := \"\"\n\tserverName := \"\"\n\tsubscriptionID := \"\"\n\n\t_, err := GetMYSQLServerE(t, subscriptionID, resGroupName, serverName)\n\trequire.Error(t, err)\n}\n\nfunc TestGetMYSQLDBE(t *testing.T) {\n\tt.Parallel()\n\n\tresGroupName := \"\"\n\tserverName := \"\"\n\tsubscriptionID := \"\"\n\tdbName := \"\"\n\n\t_, err := GetMYSQLDBE(t, subscriptionID, resGroupName, serverName, dbName)\n\trequire.Error(t, err)\n}\n"
  },
  {
    "path": "modules/azure/networkinterface.go",
    "content": "package azure\n\nimport (\n\t\"context\"\n\n\t\"github.com/Azure/azure-sdk-for-go/services/network/mgmt/2019-09-01/network\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// NetworkInterfaceExists indicates whether the specified Azure Network Interface exists.\n// This function would fail the test if there is an error.\nfunc NetworkInterfaceExists(t testing.TestingT, nicName string, resGroupName string, subscriptionID string) bool {\n\texists, err := NetworkInterfaceExistsE(nicName, resGroupName, subscriptionID)\n\trequire.NoError(t, err)\n\treturn exists\n}\n\n// NetworkInterfaceExistsE indicates whether the specified Azure Network Interface exists.\nfunc NetworkInterfaceExistsE(nicName string, resGroupName string, subscriptionID string) (bool, error) {\n\t// Get the Network Interface\n\t_, err := GetNetworkInterfaceE(nicName, resGroupName, subscriptionID)\n\tif err != nil {\n\t\tif ResourceNotFoundErrorExists(err) {\n\t\t\treturn false, nil\n\t\t}\n\t\treturn false, err\n\t}\n\treturn true, nil\n}\n\n// GetNetworkInterfacePrivateIPs gets a list of the Private IPs of a Network Interface configs.\n// This function would fail the test if there is an error.\nfunc GetNetworkInterfacePrivateIPs(t testing.TestingT, nicName string, resGroupName string, subscriptionID string) []string {\n\tIPs, err := GetNetworkInterfacePrivateIPsE(nicName, resGroupName, subscriptionID)\n\trequire.NoError(t, err)\n\n\treturn IPs\n}\n\n// GetNetworkInterfacePrivateIPsE gets a list of the Private IPs of a Network Interface configs.\nfunc GetNetworkInterfacePrivateIPsE(nicName string, resGroupName string, subscriptionID string) ([]string, error) {\n\tvar privateIPs []string\n\n\t// Get the Network Interface client\n\tnic, err := GetNetworkInterfaceE(nicName, resGroupName, subscriptionID)\n\tif err != nil {\n\t\treturn privateIPs, err\n\t}\n\n\t// Get the Private IPs from each configuration\n\tfor _, IPConfiguration := range *nic.IPConfigurations {\n\t\tprivateIPs = append(privateIPs, *IPConfiguration.PrivateIPAddress)\n\t}\n\n\treturn privateIPs, nil\n}\n\n// GetNetworkInterfacePublicIPs returns a list of all the Public IPs found in the Network Interface configurations.\n// This function would fail the test if there is an error.\nfunc GetNetworkInterfacePublicIPs(t testing.TestingT, nicName string, resGroupName string, subscriptionID string) []string {\n\tIPs, err := GetNetworkInterfacePublicIPsE(nicName, resGroupName, subscriptionID)\n\trequire.NoError(t, err)\n\treturn IPs\n}\n\n// GetNetworkInterfacePublicIPsE returns a list of all the Public IPs found in the Network Interface configurations.\nfunc GetNetworkInterfacePublicIPsE(nicName string, resGroupName string, subscriptionID string) ([]string, error) {\n\tvar publicIPs []string\n\n\t// Get the Network Interface client\n\tnic, err := GetNetworkInterfaceE(nicName, resGroupName, subscriptionID)\n\tif err != nil {\n\t\treturn publicIPs, err\n\t}\n\n\t// Get the Public IPs from each configuration available\n\tfor _, IPConfiguration := range *nic.IPConfigurations {\n\t\t// Iterate each config, for successful configurations check for a Public Address reference.\n\t\t// Not failing on errors as this is an optimistic accumulator.\n\t\tnicConfig, err := GetNetworkInterfaceConfigurationE(nicName, *IPConfiguration.Name, resGroupName, subscriptionID)\n\t\tif err == nil {\n\t\t\tif nicConfig.PublicIPAddress != nil {\n\t\t\t\tpublicAddressID := GetNameFromResourceID(*nicConfig.PublicIPAddress.ID)\n\t\t\t\tpublicIP, err := GetIPOfPublicIPAddressByNameE(publicAddressID, resGroupName, subscriptionID)\n\t\t\t\tif err == nil {\n\t\t\t\t\tpublicIPs = append(publicIPs, publicIP)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn publicIPs, nil\n}\n\n// GetNetworkInterfaceConfigurationE gets a Network Interface Configuration in the specified Azure Resource Group.\nfunc GetNetworkInterfaceConfigurationE(nicName string, nicConfigName string, resGroupName string, subscriptionID string) (*network.InterfaceIPConfiguration, error) {\n\t// Validate Azure Resource Group\n\tresGroupName, err := getTargetAzureResourceGroupName(resGroupName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Get the client reference\n\tclient, err := GetNetworkInterfaceConfigurationClientE(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Get the Network Interface\n\tnicConfig, err := client.Get(context.Background(), resGroupName, nicName, nicConfigName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &nicConfig, nil\n}\n\n// GetNetworkInterfaceConfigurationClientE creates a new Network Interface Configuration client in the specified Azure Subscription.\nfunc GetNetworkInterfaceConfigurationClientE(subscriptionID string) (*network.InterfaceIPConfigurationsClient, error) {\n\t// Create a new client from client factory\n\tclient, err := CreateNewNetworkInterfaceIPConfigurationClientE(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Create an authorizer\n\tauthorizer, err := NewAuthorizer()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tclient.Authorizer = *authorizer\n\n\treturn client, nil\n}\n\n// GetNetworkInterfaceE gets a Network Interface in the specified Azure Resource Group.\nfunc GetNetworkInterfaceE(nicName string, resGroupName string, subscriptionID string) (*network.Interface, error) {\n\t// Validate Azure Resource Group\n\tresGroupName, err := getTargetAzureResourceGroupName(resGroupName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Get the client reference\n\tclient, err := GetNetworkInterfaceClientE(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Get the Network Interface\n\tnic, err := client.Get(context.Background(), resGroupName, nicName, \"\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &nic, nil\n}\n\n// GetNetworkInterfaceClientE creates a new Network Interface client in the specified Azure Subscription.\nfunc GetNetworkInterfaceClientE(subscriptionID string) (*network.InterfacesClient, error) {\n\t// Create new NIC client from client factory\n\tclient, err := CreateNewNetworkInterfacesClientE(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Create an authorizer\n\tauthorizer, err := NewAuthorizer()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tclient.Authorizer = *authorizer\n\n\treturn client, nil\n}\n"
  },
  {
    "path": "modules/azure/networkinterface_test.go",
    "content": "//go:build azure || (azureslim && network)\n// +build azure azureslim,network\n\n// NOTE: We use build tags to differentiate azure testing because we currently do not have azure access setup for\n// CircleCI.\n\npackage azure\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\n/*\nThe below tests are currently stubbed out, with the expectation that they will throw errors.\nIf/when methods can be mocked or Create/Delete APIs are added, these tests can be extended.\n*/\n\nfunc TestGetNetworkInterfaceE(t *testing.T) {\n\tt.Parallel()\n\n\tnicName := \"\"\n\trgName := \"\"\n\tsubID := \"\"\n\n\t_, err := GetNetworkInterfaceE(nicName, rgName, subID)\n\n\trequire.Error(t, err)\n}\n\nfunc TestGetNetworkInterfacePrivateIPsE(t *testing.T) {\n\tt.Parallel()\n\n\tnicName := \"\"\n\trgName := \"\"\n\tsubID := \"\"\n\n\t_, err := GetNetworkInterfacePrivateIPsE(nicName, rgName, subID)\n\n\trequire.Error(t, err)\n}\n\nfunc TestGetNetworkInterfacePublicIPsE(t *testing.T) {\n\tt.Parallel()\n\n\tnicName := \"\"\n\trgName := \"\"\n\tsubID := \"\"\n\n\t_, err := GetNetworkInterfacePublicIPsE(nicName, rgName, subID)\n\n\trequire.Error(t, err)\n}\n\nfunc TestNetworkInterfaceExistsE(t *testing.T) {\n\tt.Parallel()\n\n\tnicName := \"\"\n\trgName := \"\"\n\tsubID := \"\"\n\n\t_, err := NetworkInterfaceExistsE(nicName, rgName, subID)\n\n\trequire.Error(t, err)\n}\n"
  },
  {
    "path": "modules/azure/nsg.go",
    "content": "package azure\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/Azure/azure-sdk-for-go/services/network/mgmt/2019-09-01/network\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// NsgRuleSummaryList holds a collection of NsgRuleSummary rules\ntype NsgRuleSummaryList struct {\n\tSummarizedRules []NsgRuleSummary\n}\n\n// NsgRuleSummary is a string-based (non-pointer) summary of an NSG rule with several helper methods attached\n// to help with verification of rule configuration.\ntype NsgRuleSummary struct {\n\tName                       string\n\tDescription                string\n\tProtocol                   string\n\tSourcePortRange            string\n\tSourcePortRanges           []string\n\tDestinationPortRange       string\n\tDestinationPortRanges      []string\n\tSourceAddressPrefix        string\n\tSourceAdresssPrefixes      []string\n\tDestinationAddressPrefix   string\n\tDestinationAddressPrefixes []string\n\tAccess                     string\n\tPriority                   int32\n\tDirection                  string\n}\n\n// GetDefaultNsgRulesClient returns a rules client which can be used to read the list of *default* security rules\n// defined on an network security group. Note that the \"default\" rules are those provided implicitly\n// by the Azure platform.\n// This function would fail the test if there is an error.\nfunc GetDefaultNsgRulesClient(t *testing.T, subscriptionID string) network.DefaultSecurityRulesClient {\n\tclient, err := GetDefaultNsgRulesClientE(subscriptionID)\n\trequire.NoError(t, err)\n\treturn client\n}\n\n// GetDefaultNsgRulesClientE returns a rules client which can be used to read the list of *default* security rules\n// defined on an network security group. Note that the \"default\" rules are those provided implicitly\n// by the Azure platform.\nfunc GetDefaultNsgRulesClientE(subscriptionID string) (network.DefaultSecurityRulesClient, error) {\n\t// Get new default client from client factory\n\tnsgClient, err := CreateNsgDefaultRulesClientE(subscriptionID)\n\tif err != nil {\n\t\treturn network.DefaultSecurityRulesClient{}, err\n\t}\n\n\t// Get an authorizer\n\tauth, err := NewAuthorizer()\n\tif err != nil {\n\t\treturn network.DefaultSecurityRulesClient{}, err\n\t}\n\n\tnsgClient.Authorizer = *auth\n\treturn *nsgClient, nil\n}\n\n// GetCustomNsgRulesClient returns a rules client which can be used to read the list of *custom* security rules\n// defined on an network security group. Note that the \"custom\" rules are those defined by\n// end users.\n// This function would fail the test if there is an error.\nfunc GetCustomNsgRulesClient(t *testing.T, subscriptionID string) network.SecurityRulesClient {\n\tclient, err := GetCustomNsgRulesClientE(subscriptionID)\n\trequire.NoError(t, err)\n\treturn client\n}\n\n// GetCustomNsgRulesClientE returns a rules client which can be used to read the list of *custom* security rules\n// defined on an network security group. Note that the \"custom\" rules are those defined by\n// end users.\nfunc GetCustomNsgRulesClientE(subscriptionID string) (network.SecurityRulesClient, error) {\n\t// Get new custom rules client from client factory\n\tnsgClient, err := CreateNsgCustomRulesClientE(subscriptionID)\n\tif err != nil {\n\t\treturn network.SecurityRulesClient{}, err\n\t}\n\n\t// Get an authorizer\n\tauth, err := NewAuthorizer()\n\tif err != nil {\n\t\treturn network.SecurityRulesClient{}, err\n\t}\n\n\tnsgClient.Authorizer = *auth\n\treturn *nsgClient, nil\n}\n\n// GetAllNSGRules returns an NsgRuleSummaryList instance containing the combined \"default\" and \"custom\" rules from a network\n// security group.\n// This function would fail the test if there is an error.\nfunc GetAllNSGRules(t *testing.T, resourceGroupName, nsgName, subscriptionID string) NsgRuleSummaryList {\n\tresults, err := GetAllNSGRulesE(resourceGroupName, nsgName, subscriptionID)\n\trequire.NoError(t, err)\n\treturn results\n}\n\n// GetAllNSGRulesE returns an NsgRuleSummaryList instance containing the combined \"default\" and \"custom\" rules from a network\n// security group.\nfunc GetAllNSGRulesE(resourceGroupName, nsgName, subscriptionID string) (NsgRuleSummaryList, error) {\n\tdefaultRulesClient, err := GetDefaultNsgRulesClientE(subscriptionID)\n\tif err != nil {\n\t\treturn NsgRuleSummaryList{}, err\n\t}\n\n\t// Get a client instance\n\tcustomRulesClient, err := GetCustomNsgRulesClientE(subscriptionID)\n\tif err != nil {\n\t\treturn NsgRuleSummaryList{}, err\n\t}\n\n\t// Read all default (platform) rules.\n\tdefaultRuleList, err := defaultRulesClient.ListComplete(context.Background(), resourceGroupName, nsgName)\n\tif err != nil {\n\t\treturn NsgRuleSummaryList{}, err\n\t}\n\n\t// Read any custom (user provided) rules\n\tcustomRuleList, err := customRulesClient.ListComplete(context.Background(), resourceGroupName, nsgName)\n\tif err != nil {\n\t\treturn NsgRuleSummaryList{}, err\n\t}\n\n\t// Convert the default list to our summary type\n\tboundDefaultRules, err := bindRuleList(defaultRuleList)\n\tif err != nil {\n\t\treturn NsgRuleSummaryList{}, err\n\t}\n\n\t// Convert the custom list to our summary type\n\tboundCustomRules, err := bindRuleList(customRuleList)\n\tif err != nil {\n\t\treturn NsgRuleSummaryList{}, err\n\t}\n\n\t// Join the summarized lists and wrap in NsgRuleSummaryList struct\n\tallRules := append(boundDefaultRules, boundCustomRules...)\n\truleList := NsgRuleSummaryList{}\n\truleList.SummarizedRules = allRules\n\treturn ruleList, nil\n}\n\n// bindRuleList takes a raw list of security rules from the SDK and converts them into a string-based\n// summary struct.\nfunc bindRuleList(source network.SecurityRuleListResultIterator) ([]NsgRuleSummary, error) {\n\trules := make([]NsgRuleSummary, 0)\n\tfor source.NotDone() {\n\t\tv := source.Value()\n\t\trules = append(rules, convertToNsgRuleSummary(v.Name, v.SecurityRulePropertiesFormat))\n\t\terr := source.NextWithContext(context.Background())\n\t\tif err != nil {\n\t\t\treturn []NsgRuleSummary{}, err\n\t\t}\n\t}\n\treturn rules, nil\n}\n\n// convertToNsgRuleSummary converts the raw SDK security rule type into a summarized struct, flattening the\n// rules properties and name into a single, string-based struct.\nfunc convertToNsgRuleSummary(name *string, rule *network.SecurityRulePropertiesFormat) NsgRuleSummary {\n\tsummary := NsgRuleSummary{}\n\tsummary.Description = safePtrToString(rule.Description)\n\tsummary.Name = safePtrToString(name)\n\tsummary.Protocol = string(rule.Protocol)\n\tsummary.SourcePortRange = safePtrToString(rule.SourcePortRange)\n\tsummary.SourcePortRanges = safePtrToList(rule.SourcePortRanges)\n\tsummary.DestinationPortRange = safePtrToString(rule.DestinationPortRange)\n\tsummary.DestinationPortRanges = safePtrToList(rule.DestinationPortRanges)\n\tsummary.SourceAddressPrefix = safePtrToString(rule.SourceAddressPrefix)\n\tsummary.SourceAdresssPrefixes = safePtrToList(rule.SourceAddressPrefixes)\n\tsummary.DestinationAddressPrefix = safePtrToString(rule.DestinationAddressPrefix)\n\tsummary.DestinationAddressPrefixes = safePtrToList(rule.DestinationAddressPrefixes)\n\tsummary.Access = string(rule.Access)\n\tsummary.Priority = safePtrToInt32(rule.Priority)\n\tsummary.Direction = string(rule.Direction)\n\treturn summary\n}\n\n// FindRuleByName looks for a matching rule by name within the current collection of rules.\nfunc (summarizedRules *NsgRuleSummaryList) FindRuleByName(name string) NsgRuleSummary {\n\tfor _, r := range summarizedRules.SummarizedRules {\n\t\tif r.Name == name {\n\t\t\treturn r\n\t\t}\n\t}\n\n\treturn NsgRuleSummary{}\n}\n\n// AllowsDestinationPort checks to see if the rule allows a specific destination port. This is helpful when verifying\n// that a given rule is configured properly for a given port.\nfunc (summarizedRule *NsgRuleSummary) AllowsDestinationPort(t *testing.T, port string) bool {\n\tallowed, err := portRangeAllowsPort(summarizedRule.DestinationPortRange, port)\n\tassert.NoError(t, err)\n\treturn allowed && (summarizedRule.Access == \"Allow\")\n}\n\n// AllowsSourcePort checks to see if the rule allows a specific source port. This is helpful when verifying\n// that a given rule is configured properly for a given port.\nfunc (summarizedRule *NsgRuleSummary) AllowsSourcePort(t *testing.T, port string) bool {\n\tallowed, err := portRangeAllowsPort(summarizedRule.SourcePortRange, port)\n\tassert.NoError(t, err)\n\treturn allowed && (summarizedRule.Access == \"Allow\")\n}\n\n// portRangeAllowsPort is the internal implementation of AllowsSourcePort and AllowsDestinationPort.\nfunc portRangeAllowsPort(portRange string, port string) (bool, error) {\n\tif portRange == \"*\" {\n\t\treturn true, nil\n\t}\n\n\t// Decode the provided port range\n\tlow, high, parseErr := parsePortRangeString(portRange)\n\tif parseErr != nil {\n\t\treturn false, parseErr\n\t}\n\n\t// Decode user-provided port\n\tportAsInt, parseErr := strconv.ParseInt(port, 10, 16)\n\tif (parseErr != nil) && (port != \"*\") {\n\t\treturn false, parseErr\n\t}\n\n\t// If the user wants to check \"all\", make sure we parsed input range to include all ports.\n\tif (port == \"*\") && (low == 0) && (high == 65535) {\n\t\treturn true, nil\n\t}\n\n\t// Evaluate and return\n\treturn ((uint16(portAsInt) >= low) && (uint16(portAsInt) <= high)), nil\n}\n\n// parsePortRangeString decodes a range string (\"2-100\") or a single digit (\"22\") and returns\n// a tuple in [low, hi] form. Note that if a single digit is supplied, both members of the\n// return tuple will be the same value (e.g., \"22\" returns (22, 22))\nfunc parsePortRangeString(rangeString string) (uint16, uint16, error) {\n\t// An asterisk means all ports\n\tif rangeString == \"*\" {\n\t\treturn uint16(0), uint16(65535), nil\n\t}\n\n\t// Check for range string that contains hyphen separator\n\tif !strings.Contains(rangeString, \"-\") {\n\t\tval, parseErr := strconv.ParseInt(rangeString, 10, 16)\n\t\tif parseErr != nil {\n\t\t\treturn 0, 0, parseErr\n\t\t}\n\t\treturn uint16(val), uint16(val), nil\n\t}\n\n\t// Split the range into parts and validate\n\tparts := strings.Split(rangeString, \"-\")\n\tif len(parts) != 2 {\n\t\treturn 0, 0, fmt.Errorf(\"Invalid port range specified; must be of the format '{low port}-{high port}'\")\n\t}\n\n\t// Assume the low port is listed first; parse it\n\tlowVal, parseErr := strconv.ParseInt(parts[0], 10, 16)\n\tif parseErr != nil {\n\t\treturn 0, 0, parseErr\n\t}\n\n\t// Assume the hi port is listed first; parse it\n\thighVal, parseErr := strconv.ParseInt(parts[1], 10, 16)\n\tif parseErr != nil {\n\t\treturn 0, 0, parseErr\n\t}\n\n\t// Normalize ordering in the case that low and hi were reversed.\n\t// This should _never_ happen, as the Azure API's won't allow it, but\n\t// we shouldn't fail if it's the case.\n\tif lowVal > highVal {\n\t\ttemp := lowVal\n\t\tlowVal = highVal\n\t\thighVal = temp\n\t}\n\n\t// Return values\n\treturn uint16(lowVal), uint16(highVal), nil\n}\n"
  },
  {
    "path": "modules/azure/nsg_test.go",
    "content": "//go:build azure\n// +build azure\n\n// NOTE: We use build tags to differentiate azure testing because we currently do not have azure access setup for\n// CircleCI.\n\npackage azure\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/Azure/azure-sdk-for-go/services/network/mgmt/2019-09-01/network\"\n)\n\nfunc TestPortRangeParsing(t *testing.T) {\n\tvar cases = []struct {\n\t\tportRange    string\n\t\texpectedLo   int\n\t\texpectedHi   int\n\t\texpectsError bool\n\t}{\n\t\t{\"22\", 22, 22, false},\n\t\t{\"22-80\", 22, 80, false},\n\t\t{\"*\", 0, 65535, false},\n\t\t{\"*-*\", 0, 0, true},\n\t\t{\"22-\", 0, 0, true},\n\t\t{\"-80\", 0, 0, true},\n\t\t{\"-\", 0, 0, true},\n\t\t{\"80-22\", 22, 80, false},\n\t}\n\n\tfor _, tt := range cases {\n\t\tt.Run(tt.portRange, func(t *testing.T) {\n\t\t\tlo, hi, err := parsePortRangeString(tt.portRange)\n\t\t\tif !tt.expectsError {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\t\t\tassert.Equal(t, tt.expectedLo, int(lo))\n\t\t\tassert.Equal(t, tt.expectedHi, int(hi))\n\t\t})\n\t}\n}\n\nfunc TestNsgRuleSummaryConversion(t *testing.T) {\n\t// Quick test to make sure the safe nil handling is working\n\tname := \"test name\"\n\tsdkStruct := network.SecurityRulePropertiesFormat{}\n\n\t// Verify the nil values were correctly defaulted to \"\" without a panic\n\tresult := convertToNsgRuleSummary(&name, &sdkStruct)\n\tassert.Equal(t, \"\", result.Description)\n\tassert.Equal(t, \"\", result.SourcePortRange)\n\tassert.Equal(t, \"\", result.DestinationPortRange)\n\tassert.Equal(t, \"\", result.SourceAddressPrefix)\n\tassert.Equal(t, \"\", result.DestinationAddressPrefix)\n\tassert.Equal(t, int32(0), result.Priority)\n}\n\nfunc TestAllowSourcePort(t *testing.T) {\n\tvar cases = []struct {\n\t\tCaseName        string\n\t\tSourcePortRange string\n\t\tAccess          string\n\t\tTestPort        string\n\t\tResult          bool\n\t}{\n\t\t{\"22 allowed\", \"22\", \"Allow\", \"22\", true},\n\t\t{\"22 denied\", \"22\", \"Deny\", \"22\", false},\n\t\t{\"22 doesn't allow 80\", \"22\", \"Allow\", \"80\", false},\n\t\t{\"Any allows any\", \"*\", \"Allow\", \"*\", true},\n\t\t{\"Allows a range of ports\", \"80-90\", \"Allow\", \"80\", true},\n\t\t{\"Allows a range of ports\", \"80-90\", \"Allow\", \"85\", true},\n\t\t{\"Allows a range of ports\", \"80-90\", \"Allow\", \"90\", true},\n\t\t{\"Blocks a range of ports\", \"80-90\", \"Deny\", \"80\", false},\n\t\t{\"Blocks a range of ports\", \"80-90\", \"Deny\", \"85\", false},\n\t\t{\"Blocks a range of ports\", \"80-90\", \"Deny\", \"90\", false},\n\t}\n\n\tfor _, tt := range cases {\n\t\tt.Run(tt.CaseName, func(t *testing.T) {\n\t\t\tsummary := NsgRuleSummary{}\n\t\t\tsummary.SourcePortRange = tt.SourcePortRange\n\t\t\tsummary.Access = tt.Access\n\t\t\tresult := summary.AllowsSourcePort(t, tt.TestPort)\n\t\t\tassert.Equal(t, tt.Result, result)\n\t\t})\n\t}\n}\n\nfunc TestAllowDestinationPort(t *testing.T) {\n\tvar cases = []struct {\n\t\tCaseName        string\n\t\tSourcePortRange string\n\t\tAccess          string\n\t\tTestPort        string\n\t\tResult          bool\n\t}{\n\t\t{\"22 allowed\", \"22\", \"Allow\", \"22\", true},\n\t\t{\"22 denied\", \"22\", \"Deny\", \"22\", false},\n\t\t{\"22 doesn't allow 80\", \"22\", \"Allow\", \"80\", false},\n\t\t{\"Any allows any\", \"*\", \"Allow\", \"*\", true},\n\t\t{\"Allows a range of ports\", \"80-90\", \"Allow\", \"80\", true},\n\t\t{\"Allows a range of ports\", \"80-90\", \"Allow\", \"85\", true},\n\t\t{\"Allows a range of ports\", \"80-90\", \"Allow\", \"90\", true},\n\t\t{\"Blocks a range of ports\", \"80-90\", \"Deny\", \"80\", false},\n\t\t{\"Blocks a range of ports\", \"80-90\", \"Deny\", \"85\", false},\n\t\t{\"Blocks a range of ports\", \"80-90\", \"Deny\", \"90\", false},\n\t}\n\n\tfor _, tt := range cases {\n\t\tt.Run(tt.CaseName, func(t *testing.T) {\n\t\t\tsummary := NsgRuleSummary{}\n\t\t\tsummary.DestinationPortRange = tt.SourcePortRange\n\t\t\tsummary.Access = tt.Access\n\t\t\tresult := summary.AllowsDestinationPort(t, tt.TestPort)\n\t\t\tassert.Equal(t, tt.Result, result)\n\t\t})\n\t}\n}\n\nfunc TestFindSummarizedRule(t *testing.T) {\n\tvar cases = []struct {\n\t\tSearchString string\n\t\tResult       bool\n\t}{\n\t\t{\"rule_1\", true},\n\t\t{\"rule_2\", true},\n\t\t{\"rule_3\", true},\n\t\t{\"rule_4\", true},\n\t\t{\"rule_5\", true},\n\t\t{\"rule_6\", false},\n\t\t{\"\", false},\n\t\t{\"foo\", false},\n\t}\n\n\truleList := NsgRuleSummaryList{}\n\trules := make([]NsgRuleSummary, 0)\n\n\t// Create some base rules\n\tfor i := 1; i <= 5; i++ {\n\t\trule := NsgRuleSummary{}\n\t\trule.Name = fmt.Sprintf(\"rule_%d\", i)\n\t\trules = append(rules, rule)\n\t}\n\truleList.SummarizedRules = rules\n\n\tfor _, tt := range cases {\n\t\tt.Run(tt.SearchString, func(t *testing.T) {\n\t\t\tmatch := ruleList.FindRuleByName(tt.SearchString)\n\t\t\tif tt.Result {\n\t\t\t\tassert.Equal(t, tt.SearchString, match.Name)\n\t\t\t} else {\n\t\t\t\tassert.Equal(t, match, NsgRuleSummary{})\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "modules/azure/postgresql.go",
    "content": "package azure\n\nimport (\n\t\"context\"\n\n\t\"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/postgresql/armpostgresql\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// GetPostgreSQLServerClientE is a helper function that will setup a postgresql server client.\nfunc GetPostgreSQLServerClientE(subscriptionID string) (*armpostgresql.ServersClient, error) {\n\tclientFactory, err := getArmPostgreSQLClientFactory(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn clientFactory.NewServersClient(), nil\n}\n\n// GetPostgreSQLServer is a helper function that gets the server.\n// This function would fail the test if there is an error.\nfunc GetPostgreSQLServer(t testing.TestingT, resGroupName string, serverName string, subscriptionID string) *armpostgresql.Server {\n\tpostgresqlServer, err := GetPostgreSQLServerE(t, subscriptionID, resGroupName, serverName)\n\trequire.NoError(t, err)\n\n\treturn postgresqlServer\n}\n\n// GetPostgreSQLServerE is a helper function that gets the server.\nfunc GetPostgreSQLServerE(t testing.TestingT, subscriptionID string, resGroupName string, serverName string) (*armpostgresql.Server, error) {\n\t// Create a postgresql Server client\n\tpostgresqlClient, err := GetPostgreSQLServerClientE(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Get the corresponding server\n\tresp, err := postgresqlClient.Get(context.Background(), resGroupName, serverName, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &resp.Server, nil\n}\n\n// GetPostgreSQLDBClientE is a helper function that will setup a postgresql DB client.\nfunc GetPostgreSQLDBClientE(subscriptionID string) (*armpostgresql.DatabasesClient, error) {\n\tclientFactory, err := getArmPostgreSQLClientFactory(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn clientFactory.NewDatabasesClient(), nil\n}\n\n// GetPostgreSQLDB is a helper function that gets the database.\n// This function would fail the test if there is an error.\nfunc GetPostgreSQLDB(t testing.TestingT, resGroupName string, serverName string, dbName string, subscriptionID string) *armpostgresql.Database {\n\tdatabase, err := GetPostgreSQLDBE(t, subscriptionID, resGroupName, serverName, dbName)\n\trequire.NoError(t, err)\n\n\treturn database\n}\n\n// GetPostgreSQLDBE is a helper function that gets the database.\nfunc GetPostgreSQLDBE(t testing.TestingT, subscriptionID string, resGroupName string, serverName string, dbName string) (*armpostgresql.Database, error) {\n\t// Create a postgresql db client\n\tpostgresqldbClient, err := GetPostgreSQLDBClientE(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Get the corresponding db\n\tresp, err := postgresqldbClient.Get(context.Background(), resGroupName, serverName, dbName, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &resp.Database, nil\n}\n\n// ListPostgreSQLDB is a helper function that gets all databases per server.\nfunc ListPostgreSQLDB(t testing.TestingT, subscriptionID string, resGroupName string, serverName string) []*armpostgresql.Database {\n\tdblist, err := ListPostgreSQLDBE(t, subscriptionID, resGroupName, serverName)\n\trequire.NoError(t, err)\n\n\treturn dblist\n}\n\n// ListPostgreSQLDBE is a helper function that gets all databases per server.\nfunc ListPostgreSQLDBE(t testing.TestingT, subscriptionID string, resGroupName string, serverName string) ([]*armpostgresql.Database, error) {\n\t// Create a postgresql db client\n\tpostgresqldbClient, err := GetPostgreSQLDBClientE(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Get the databases using pager\n\tpager := postgresqldbClient.NewListByServerPager(resGroupName, serverName, nil)\n\tvar databases []*armpostgresql.Database\n\tfor pager.More() {\n\t\tpage, err := pager.NextPage(context.Background())\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tdatabases = append(databases, page.Value...)\n\t}\n\n\treturn databases, nil\n}\n"
  },
  {
    "path": "modules/azure/postgresql_test.go",
    "content": "//go:build azure\n// +build azure\n\n// NOTE: We use build tags to differentiate azure testing because we currently do not have azure access setup for\n// CircleCI.\n\npackage azure\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\n/*\nThe below tests are currently stubbed out, with the expectation that they will throw errors.\nIf/when CRUD methods are introduced for Azure PostgreSQL server and database, these tests can be extended\n*/\n\nfunc TestGetPostgreSQLServerE(t *testing.T) {\n\tt.Parallel()\n\n\tresGroupName := \"\"\n\tserverName := \"\"\n\tsubscriptionID := \"\"\n\n\t_, err := GetPostgreSQLServerE(t, subscriptionID, resGroupName, serverName)\n\trequire.Error(t, err)\n}\n\nfunc TestGetPostgreSQLDBE(t *testing.T) {\n\tt.Parallel()\n\n\tresGroupName := \"\"\n\tserverName := \"\"\n\tsubscriptionID := \"\"\n\tdbName := \"\"\n\n\t_, err := GetPostgreSQLDBE(t, subscriptionID, resGroupName, serverName, dbName)\n\trequire.Error(t, err)\n}\n"
  },
  {
    "path": "modules/azure/privatednszone.go",
    "content": "package azure\n\nimport (\n\t\"context\"\n\n\t\"github.com/Azure/azure-sdk-for-go/profiles/latest/privatedns/mgmt/privatedns\"\n)\n\n// PrivateDNSZoneExistsE indicates whether the specified private DNS zone exists.\nfunc PrivateDNSZoneExistsE(zoneName string, resourceGroupName string, subscriptionID string) (bool, error) {\n\t_, err := GetPrivateDNSZoneE(zoneName, resourceGroupName, subscriptionID)\n\tif err != nil {\n\t\tif ResourceNotFoundErrorExists(err) {\n\t\t\treturn false, nil\n\t\t}\n\t\treturn false, err\n\t}\n\treturn true, nil\n}\n\n// GetPrivateDNSZoneE gets the private DNS zone object\nfunc GetPrivateDNSZoneE(zoneName string, resGroupName string, subscriptionID string) (*privatedns.PrivateZone, error) {\n\trgName, err := getTargetAzureResourceGroupName(resGroupName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tclient, err := CreatePrivateDnsZonesClientE(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tzone, err := client.Get(context.Background(), rgName, zoneName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &zone, nil\n}\n"
  },
  {
    "path": "modules/azure/privatednszone_test.go",
    "content": "package azure\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\n/*\nThe below tests are currently stubbed out, with the expectation that they will throw errors.\nIf/when CRUD methods are introduced for Azure Synapse, these tests can be extended\n*/\nfunc TestPrivateDNSZoneExists(t *testing.T) {\n\tt.Parallel()\n\n\tzoneName := \"\"\n\tresourceGroupName := \"\"\n\tsubscriptionID := \"\"\n\n\texists, err := PrivateDNSZoneExistsE(zoneName, resourceGroupName, subscriptionID)\n\n\trequire.False(t, exists)\n\trequire.Error(t, err)\n}\n\nfunc TestPrivateDNSZoneExistsE(t *testing.T) {\n\tt.Parallel()\n\n\tresGroupName := \"\"\n\tsubscriptionID := \"\"\n\tzoneName := \"\"\n\n\t_, err := GetPrivateDNSZoneE(subscriptionID, resGroupName, zoneName)\n\trequire.Error(t, err)\n}\n"
  },
  {
    "path": "modules/azure/publicaddress.go",
    "content": "package azure\n\nimport (\n\t\"context\"\n\n\t\"github.com/Azure/azure-sdk-for-go/services/network/mgmt/2019-09-01/network\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// PublicAddressExists indicates whether the specified AzurePublic Address exists.\n// This function would fail the test if there is an error.\nfunc PublicAddressExists(t testing.TestingT, publicAddressName string, resGroupName string, subscriptionID string) bool {\n\texists, err := PublicAddressExistsE(publicAddressName, resGroupName, subscriptionID)\n\trequire.NoError(t, err)\n\treturn exists\n}\n\n// PublicAddressExistsE indicates whether the specified AzurePublic Address exists.\nfunc PublicAddressExistsE(publicAddressName string, resGroupName string, subscriptionID string) (bool, error) {\n\t// Get the Public Address\n\t_, err := GetPublicIPAddressE(publicAddressName, resGroupName, subscriptionID)\n\tif err != nil {\n\t\tif ResourceNotFoundErrorExists(err) {\n\t\t\treturn false, nil\n\t\t}\n\t\treturn false, err\n\t}\n\treturn true, nil\n}\n\n// GetIPOfPublicIPAddressByName gets the Public IP of the Public IP Address specified.\n// This function would fail the test if there is an error.\nfunc GetIPOfPublicIPAddressByName(t testing.TestingT, publicAddressName string, resGroupName string, subscriptionID string) string {\n\tIP, err := GetIPOfPublicIPAddressByNameE(publicAddressName, resGroupName, subscriptionID)\n\trequire.NoError(t, err)\n\treturn IP\n}\n\n// GetIPOfPublicIPAddressByNameE gets the Public IP of the Public IP Address specified.\nfunc GetIPOfPublicIPAddressByNameE(publicAddressName string, resGroupName string, subscriptionID string) (string, error) {\n\t// Create a NIC client\n\tpip, err := GetPublicIPAddressE(publicAddressName, resGroupName, subscriptionID)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn *pip.IPAddress, nil\n}\n\n// CheckPublicDNSNameAvailability checks whether a Domain Name in the cloudapp.azure.com zone\n// is available for use. This function would fail the test if there is an error.\nfunc CheckPublicDNSNameAvailability(t testing.TestingT, location string, domainNameLabel string, subscriptionID string) bool {\n\tavailable, err := CheckPublicDNSNameAvailabilityE(location, domainNameLabel, subscriptionID)\n\tif err != nil {\n\t\treturn false\n\t}\n\treturn available\n}\n\n// CheckPublicDNSNameAvailabilityE checks whether a Domain Name in the cloudapp.azure.com zone is available for use.\nfunc CheckPublicDNSNameAvailabilityE(location string, domainNameLabel string, subscriptionID string) (bool, error) {\n\tclient, err := GetPublicIPAddressClientE(subscriptionID)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tres, err := client.CheckDNSNameAvailability(context.Background(), location, domainNameLabel)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\treturn *res.Available, nil\n}\n\n// GetPublicIPAddressE gets a Public IP Addresses in the specified Azure Resource Group.\nfunc GetPublicIPAddressE(publicIPAddressName string, resGroupName string, subscriptionID string) (*network.PublicIPAddress, error) {\n\t// Validate resource group name and subscription ID\n\tresGroupName, err := getTargetAzureResourceGroupName(resGroupName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Get the client reference\n\tclient, err := GetPublicIPAddressClientE(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Get the Public IP Address\n\tpip, err := client.Get(context.Background(), resGroupName, publicIPAddressName, \"\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &pip, nil\n}\n\n// GetPublicIPAddressClientE creates a Public IP Addresses client in the specified Azure Subscription.\nfunc GetPublicIPAddressClientE(subscriptionID string) (*network.PublicIPAddressesClient, error) {\n\t// Get the Public IP Address client from clientfactory\n\tclient, err := CreatePublicIPAddressesClientE(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Create an authorizer\n\tauthorizer, err := NewAuthorizer()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tclient.Authorizer = *authorizer\n\n\treturn client, nil\n}\n"
  },
  {
    "path": "modules/azure/publicaddress_test.go",
    "content": "//go:build azure || (azureslim && network)\n// +build azure azureslim,network\n\n// NOTE: We use build tags to differentiate azure testing because we currently do not have azure access setup for\n// CircleCI.\n\npackage azure\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\n/*\nThe below tests are currently stubbed out, with the expectation that they will throw errors.\nIf/when methods can be mocked or Create/Delete APIs are added, these tests can be extended.\n*/\n\nfunc TestGetPublicIPAddressE(t *testing.T) {\n\tt.Parallel()\n\n\tpaName := \"\"\n\trgName := \"\"\n\tsubID := \"\"\n\n\t_, err := GetPublicIPAddressE(paName, rgName, subID)\n\n\trequire.Error(t, err)\n}\n\nfunc TestCheckPublicDNSNameAvailabilityE(t *testing.T) {\n\tt.Parallel()\n\n\tlocation := \"\"\n\tdomain := \"\"\n\tsubID := \"\"\n\n\t_, err := CheckPublicDNSNameAvailabilityE(location, domain, subID)\n\n\trequire.Error(t, err)\n}\n\nfunc TestGetIPOfPublicIPAddressByNameE(t *testing.T) {\n\tt.Parallel()\n\n\tpaName := \"\"\n\trgName := \"\"\n\tsubID := \"\"\n\n\t_, err := GetIPOfPublicIPAddressByNameE(paName, rgName, subID)\n\n\trequire.Error(t, err)\n}\n\nfunc TestPublicAddressExistsE(t *testing.T) {\n\tt.Parallel()\n\n\tpaName := \"\"\n\trgName := \"\"\n\tsubID := \"\"\n\n\t_, err := PublicAddressExistsE(paName, rgName, subID)\n\n\trequire.Error(t, err)\n}\n"
  },
  {
    "path": "modules/azure/recoveryservices.go",
    "content": "package azure\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/Azure/azure-sdk-for-go/services/recoveryservices/mgmt/2016-06-01/recoveryservices\"\n\t\"github.com/Azure/azure-sdk-for-go/services/recoveryservices/mgmt/2020-02-02/backup\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// RecoveryServicesVaultExists indicates whether a recovery services vault exists; otherwise false.\n// This function would fail the test if there is an error.\nfunc RecoveryServicesVaultExists(t *testing.T, vaultName, resourceGroupName, subscriptionID string) bool {\n\texists, err := RecoveryServicesVaultExistsE(vaultName, resourceGroupName, subscriptionID)\n\trequire.NoError(t, err)\n\treturn exists\n}\n\n// GetRecoveryServicesVaultBackupPolicyList returns a list of backup policies for the given vault.\n// This function would fail the test if there is an error.\nfunc GetRecoveryServicesVaultBackupPolicyList(t *testing.T, vaultName, resourceGroupName, subscriptionID string) map[string]backup.ProtectionPolicyResource {\n\tlist, err := GetRecoveryServicesVaultBackupPolicyListE(vaultName, resourceGroupName, subscriptionID)\n\trequire.NoError(t, err)\n\treturn list\n}\n\n// GetRecoveryServicesVaultBackupProtectedVMList returns a list of protected VM's on the given vault/policy.\n// This function would fail the test if there is an error.\nfunc GetRecoveryServicesVaultBackupProtectedVMList(t *testing.T, policyName, vaultName, resourceGroupName, subscriptionID string) map[string]backup.AzureIaaSComputeVMProtectedItem {\n\tlist, err := GetRecoveryServicesVaultBackupProtectedVMListE(policyName, vaultName, resourceGroupName, subscriptionID)\n\trequire.NoError(t, err)\n\treturn list\n}\n\n// RecoveryServicesVaultExistsE indicates whether a recovery services vault exists; otherwise false or error.\nfunc RecoveryServicesVaultExistsE(vaultName, resourceGroupName, subscriptionID string) (bool, error) {\n\t_, err := GetRecoveryServicesVaultE(vaultName, resourceGroupName, subscriptionID)\n\tif err != nil {\n\t\tif ResourceNotFoundErrorExists(err) {\n\t\t\treturn false, nil\n\t\t}\n\t\treturn false, err\n\t}\n\treturn true, nil\n}\n\n// GetRecoveryServicesVaultE returns a vault instance.\nfunc GetRecoveryServicesVaultE(vaultName, resourceGroupName, subscriptionID string) (*recoveryservices.Vault, error) {\n\tsubscriptionID, err := getTargetAzureSubscription(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresourceGroupName, err2 := getTargetAzureResourceGroupName((resourceGroupName))\n\tif err2 != nil {\n\t\treturn nil, err2\n\t}\n\n\tclient := recoveryservices.NewVaultsClient(subscriptionID)\n\t// setup auth and create request params\n\tauthorizer, err := NewAuthorizer()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tclient.Authorizer = *authorizer\n\tvault, err := client.Get(context.Background(), resourceGroupName, vaultName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &vault, nil\n}\n\n// GetRecoveryServicesVaultBackupPolicyListE returns a list of backup policies for the given vault.\nfunc GetRecoveryServicesVaultBackupPolicyListE(vaultName, resourceGroupName, subscriptionID string) (map[string]backup.ProtectionPolicyResource, error) {\n\tsubscriptionID, err := getTargetAzureSubscription(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresourceGroupName, err2 := getTargetAzureResourceGroupName(resourceGroupName)\n\tif err2 != nil {\n\t\treturn nil, err2\n\t}\n\n\tclient := backup.NewPoliciesClient(subscriptionID)\n\t// setup authorizer\n\tauthorizer, err := NewAuthorizer()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tclient.Authorizer = *authorizer\n\tlistIter, err := client.ListComplete(context.Background(), vaultName, resourceGroupName, \"\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tpolicyMap := make(map[string]backup.ProtectionPolicyResource)\n\tfor listIter.NotDone() {\n\t\tv := listIter.Value()\n\t\tpolicyMap[*v.Name] = v\n\t\terr := listIter.NextWithContext(context.Background())\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t}\n\treturn policyMap, nil\n}\n\n// GetRecoveryServicesVaultBackupProtectedVMListE returns a list of protected VM's on the given vault/policy.\nfunc GetRecoveryServicesVaultBackupProtectedVMListE(policyName, vaultName, resourceGroupName, subscriptionID string) (map[string]backup.AzureIaaSComputeVMProtectedItem, error) {\n\tsubscriptionID, err := getTargetAzureSubscription(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresourceGroupName, err = getTargetAzureResourceGroupName(resourceGroupName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tclient := backup.NewProtectedItemsGroupClient(subscriptionID)\n\t// setup authorizer\n\tauthorizer, err := NewAuthorizer()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tclient.Authorizer = *authorizer\n\t// Build a filter string to narrow down results to just VM's\n\tfilter := fmt.Sprintf(\"backupManagementType eq 'AzureIaasVM' and itemType eq 'VM' and policyName eq '%s'\", policyName)\n\tlistIter, err := client.ListComplete(context.Background(), vaultName, resourceGroupName, filter, \"\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\t// Prep the return container\n\tvmList := make(map[string]backup.AzureIaaSComputeVMProtectedItem)\n\t// First iterator check\n\tfor listIter.NotDone() {\n\t\tcurrentVM, _ := listIter.Value().Properties.AsAzureIaaSComputeVMProtectedItem()\n\t\tvmList[*currentVM.FriendlyName] = *currentVM\n\t\terr := listIter.NextWithContext(context.Background())\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\treturn vmList, nil\n}\n"
  },
  {
    "path": "modules/azure/recoveryservices_test.go",
    "content": "package azure\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\n/*\nThe below tests are currently stubbed out, with the expectation that they will throw errors.\nIf/when methods to create and delete recovery services resources are added, these tests can be extended.\n*/\n\nfunc TestRecoveryServicesVaultName(t *testing.T) {\n\t_, err := GetRecoveryServicesVaultE(\"\", \"\", \"\")\n\trequire.Error(t, err, \"vault\")\n}\n\nfunc TestRecoveryServicesVaultExists(t *testing.T) {\n\t_, err := RecoveryServicesVaultExistsE(\"\", \"\", \"\")\n\trequire.Error(t, err, \"vault exists\")\n}\n\nfunc TestRecoveryServicesVaultBackupPolicyList(t *testing.T) {\n\t_, err := GetRecoveryServicesVaultBackupPolicyListE(\"\", \"\", \"\")\n\trequire.Error(t, err, \"Backup policy list not faulted\")\n}\n\nfunc TestRecoveryServicesVaultBackupProtectedVMList(t *testing.T) {\n\t_, err := GetRecoveryServicesVaultBackupProtectedVMListE(\"\", \"\", \"\", \"\")\n\trequire.Error(t, err, \"Backup policy protected vm list not faulted\")\n}\n"
  },
  {
    "path": "modules/azure/region.go",
    "content": "package azure\n\nimport (\n\t\"context\"\n\n\t\"github.com/gruntwork-io/terratest/modules/collections\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n)\n\n// Reference for region list: https://azure.microsoft.com/en-us/global-infrastructure/locations/\nvar stableRegions = []string{\n\t// Americas\n\t\"centralus\",\n\t\"eastus\",\n\t\"eastus2\",\n\t\"northcentralus\",\n\t\"southcentralus\",\n\t\"westcentralus\",\n\t\"westus\",\n\t\"westus2\",\n\t\"canadacentral\",\n\t\"canadaeast\",\n\t\"brazilsouth\",\n\n\t// Europe\n\t\"northeurope\",\n\t\"westeurope\",\n\t\"francecentral\",\n\t\"francesouth\",\n\t\"uksouth\",\n\t\"ukwest\",\n\t// \"germanycentral\", // Shows as active on Azure website, but not from API\n\t// \"germanynortheast\", // Shows as active on Azure website, but not from API\n\n\t// Asia Pacific\n\t\"eastasia\",\n\t\"southeastasia\",\n\t\"australiacentral\",\n\t\"australiacentral2\",\n\t\"australiaeast\",\n\t\"australiasoutheast\",\n\t\"chinaeast\",\n\t\"chinaeast2\",\n\t\"chinanorth\",\n\t\"chinanorth2\",\n\t\"centralindia\",\n\t\"southindia\",\n\t\"westindia\",\n\t\"japaneast\",\n\t\"japanwest\",\n\t\"koreacentral\",\n\t\"koreasouth\",\n\n\t// Middle East and Africa\n\t\"southafricanorth\",\n\t\"southafricawest\",\n\t\"uaecentral\",\n\t\"uaenorth\",\n}\n\n// GetRandomStableRegion gets a randomly chosen Azure region that is considered stable. Like GetRandomRegion, you can\n// further restrict the stable region list using approvedRegions and forbiddenRegions. We consider stable regions to be\n// those that have been around for at least 1 year.\n// Note that regions in the approvedRegions list that are not considered stable are ignored.\nfunc GetRandomStableRegion(t testing.TestingT, approvedRegions []string, forbiddenRegions []string, subscriptionID string) string {\n\tregionsToPickFrom := stableRegions\n\tif len(approvedRegions) > 0 {\n\t\tregionsToPickFrom = collections.ListIntersection(regionsToPickFrom, approvedRegions)\n\t}\n\tif len(forbiddenRegions) > 0 {\n\t\tregionsToPickFrom = collections.ListSubtract(regionsToPickFrom, forbiddenRegions)\n\t}\n\treturn GetRandomRegion(t, regionsToPickFrom, nil, subscriptionID)\n}\n\n// GetRandomRegion gets a randomly chosen Azure region. If approvedRegions is not empty, this will be a region from the approvedRegions\n// list; otherwise, this method will fetch the latest list of regions from the Azure APIs and pick one of those. If\n// forbiddenRegions is not empty, this method will make sure the returned region is not in the forbiddenRegions list.\nfunc GetRandomRegion(t testing.TestingT, approvedRegions []string, forbiddenRegions []string, subscriptionID string) string {\n\t// Validate Azure subscription ID\n\tsubscriptionID, err := getTargetAzureSubscription(subscriptionID)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tregion, err := GetRandomRegionE(t, approvedRegions, forbiddenRegions, subscriptionID)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn region\n}\n\n// GetRandomRegionE gets a randomly chosen Azure region. If approvedRegions is not empty, this will be a region from the approvedRegions\n// list; otherwise, this method will fetch the latest list of regions from the Azure APIs and pick one of those. If\n// forbiddenRegions is not empty, this method will make sure the returned region is not in the forbiddenRegions list\nfunc GetRandomRegionE(t testing.TestingT, approvedRegions []string, forbiddenRegions []string, subscriptionID string) (string, error) {\n\t// Validate Azure subscription ID\n\tsubscriptionID, err := getTargetAzureSubscription(subscriptionID)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tregionsToPickFrom := approvedRegions\n\n\tif len(regionsToPickFrom) == 0 {\n\t\tallRegions, err := GetAllAzureRegionsE(t, subscriptionID)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\tregionsToPickFrom = allRegions\n\t}\n\n\tregionsToPickFrom = collections.ListSubtract(regionsToPickFrom, forbiddenRegions)\n\tregion := random.RandomString(regionsToPickFrom)\n\n\treturn region, nil\n}\n\n// GetAllAzureRegions gets the list of Azure regions available in this subscription.\nfunc GetAllAzureRegions(t testing.TestingT, subscriptionID string) []string {\n\t// Validate Azure subscription ID\n\tsubscriptionID, err := getTargetAzureSubscription(subscriptionID)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t// Get list of Azure locations\n\tout, err := GetAllAzureRegionsE(t, subscriptionID)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\treturn out\n}\n\n// GetAllAzureRegionsE gets the list of Azure regions available in this subscription\nfunc GetAllAzureRegionsE(t testing.TestingT, subscriptionID string) ([]string, error) {\n\n\t// Validate Azure subscription ID\n\tsubscriptionID, err := getTargetAzureSubscription(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Setup Subscription client\n\tsubscriptionClient, err := GetSubscriptionClientE()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Get list of Azure locations\n\tout, err := subscriptionClient.ListLocations(context.Background(), subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Populate a return slice\n\tregions := []string{}\n\tfor _, region := range *out.Value {\n\t\tregions = append(regions, *region.Name)\n\t}\n\n\treturn regions, nil\n}\n"
  },
  {
    "path": "modules/azure/region_test.go",
    "content": "//go:build azure\n// +build azure\n\n// NOTE: We use build tags to differentiate azure testing because we currently do not have azure access setup for\n// CircleCI.\n\npackage azure\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestGetRandomRegion(t *testing.T) {\n\tt.Parallel()\n\n\trandomRegion := GetRandomRegion(t, nil, nil, \"\")\n\tassertLooksLikeRegionName(t, randomRegion)\n}\n\nfunc TestGetRandomRegionExcludesForbiddenRegions(t *testing.T) {\n\tt.Parallel()\n\n\tapprovedRegions := []string{\"canadacentral\", \"eastus\", \"eastus2\", \"westus\", \"westus2\", \"westeurope\", \"northeurope\", \"uksouth\", \"southeastasia\", \"eastasia\", \"japaneast\", \"australiacentral\"}\n\tforbiddenRegions := []string{\"westus2\", \"japaneast\"}\n\n\tfor i := 0; i < 48; i++ {\n\t\trandomRegion := GetRandomRegion(t, approvedRegions, forbiddenRegions, \"\")\n\t\tassert.NotContains(t, forbiddenRegions, randomRegion)\n\t}\n}\n\nfunc TestGetAllAzureRegions(t *testing.T) {\n\tt.Parallel()\n\n\tregions := GetAllAzureRegions(t, \"\")\n\n\t// The typical subscription had access to 30+ live regions as of\n\t// July 2019: https://azure.microsoft.com/en-us/global-infrastructure/regions/\n\tassert.True(t, len(regions) >= 30, \"Number of regions: %d\", len(regions))\n\tfor _, region := range regions {\n\t\tassertLooksLikeRegionName(t, region)\n\t}\n}\n\nfunc assertLooksLikeRegionName(t *testing.T, regionName string) {\n\tassert.Regexp(t, \"[a-z]\", regionName)\n}\n"
  },
  {
    "path": "modules/azure/resourcegroup.go",
    "content": "package azure\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/Azure/azure-sdk-for-go/sdk/azcore\"\n\t\"github.com/Azure/azure-sdk-for-go/services/resources/mgmt/2020-10-01/resources\"\n\t\"github.com/Azure/go-autorest/autorest\"\n\t\"github.com/Azure/go-autorest/autorest/azure\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// ResourceGroupExists indicates whether a resource group exists within a subscription; otherwise false\n// This function would fail the test if there is an error.\nfunc ResourceGroupExists(t *testing.T, resourceGroupName string, subscriptionID string) bool {\n\tresult, err := ResourceGroupExistsE(resourceGroupName, subscriptionID)\n\trequire.NoError(t, err)\n\treturn result\n}\n\n// ResourceGroupExistsE indicates whether a resource group exists within a subscription\nfunc ResourceGroupExistsE(resourceGroupName, subscriptionID string) (bool, error) {\n\texists, err := GetResourceGroupE(resourceGroupName, subscriptionID)\n\tif err != nil {\n\t\tif resourceGroupNotFoundError(err) {\n\t\t\treturn false, nil\n\t\t}\n\t\treturn false, err\n\t}\n\treturn exists, nil\n\n}\n\n// GetResourceGroupE gets a resource group within a subscription\nfunc GetResourceGroupE(resourceGroupName, subscriptionID string) (bool, error) {\n\n\trg, err := GetAResourceGroupE(resourceGroupName, subscriptionID)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\treturn (resourceGroupName == *rg.Name), nil\n}\n\n// GetResourceGroupClientE gets a resource group client in a subscription\n// TODO: remove in next version\nfunc GetResourceGroupClientE(subscriptionID string) (*resources.GroupsClient, error) {\n\tsubscriptionID, err := getTargetAzureSubscription(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tresourceGroupClient := resources.NewGroupsClient(subscriptionID)\n\tauthorizer, err := NewAuthorizer()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tresourceGroupClient.Authorizer = *authorizer\n\treturn &resourceGroupClient, nil\n}\n\n// GetAResourceGroup returns a resource group within a subscription\n// This function would fail the test if there is an error.\nfunc GetAResourceGroup(t *testing.T, resourceGroupName string, subscriptionID string) *resources.Group {\n\trg, err := GetAResourceGroupE(resourceGroupName, subscriptionID)\n\trequire.NoError(t, err)\n\treturn rg\n}\n\n// GetAResourceGroupE gets a resource group within a subscription\nfunc GetAResourceGroupE(resourceGroupName, subscriptionID string) (*resources.Group, error) {\n\tclient, err := CreateResourceGroupClientE(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\trg, err := client.Get(context.Background(), resourceGroupName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &rg, nil\n}\n\n// ListResourceGroupsByTag returns a resource group list within a subscription based on a tag key\n// This function would fail the test if there is an error.\nfunc ListResourceGroupsByTag(t *testing.T, tag, subscriptionID string) []resources.Group {\n\trg, err := ListResourceGroupsByTagE(tag, subscriptionID)\n\trequire.NoError(t, err)\n\treturn rg\n}\n\n// ListResourceGroupsByTagE returns a resource group list within a subscription based on a tag key\nfunc ListResourceGroupsByTagE(tag string, subscriptionID string) ([]resources.Group, error) {\n\tclient, err := CreateResourceGroupClientE(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\trg, err := client.List(context.Background(), fmt.Sprintf(\"tagName eq '%s'\", tag), nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn rg.Values(), nil\n}\n\nfunc resourceGroupNotFoundError(err error) bool {\n\tif err != nil {\n\t\tif autorestError, ok := err.(autorest.DetailedError); ok {\n\t\t\tif requestError, ok := autorestError.Original.(*azure.RequestError); ok {\n\t\t\t\treturn (requestError.ServiceError.Code == \"ResourceGroupNotFound\")\n\t\t\t}\n\t\t}\n\t\tif azcoreErr, ok := err.(*azcore.ResponseError); ok {\n\t\t\treturn azcoreErr.ErrorCode == \"ResourceGroupNotFound\"\n\t\t}\n\t}\n\treturn false\n}\n"
  },
  {
    "path": "modules/azure/resourcegroup_test.go",
    "content": "//go:build azure\n// +build azure\n\n// NOTE: We use build tags to differentiate azure testing because we currently do not have azure access setup for\n// CircleCI.\npackage azure\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n/*\nThe below tests are currently stubbed out, with the expectation that they will throw errors.\nIf/when methods to create and delete resource groups are added, these tests can be extended.\n*/\n\nfunc TestResourceGroupExists(t *testing.T) {\n\tt.Parallel()\n\n\tresourceGroupName := \"fakeResourceGroupName\"\n\texists, err := ResourceGroupExistsE(resourceGroupName, \"\")\n\tassert.NoError(t, err)\n\trequire.False(t, exists)\n}\n\nfunc TestGetAResourceGroup(t *testing.T) {\n\tt.Parallel()\n\n\tresourceGroupName := \"fakeResourceGroupName\"\n\n\t_, err := GetAResourceGroupE(resourceGroupName, \"\")\n\trequire.Error(t, err)\n}\n"
  },
  {
    "path": "modules/azure/resourcegroupv2.go",
    "content": "package azure\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// ResourceGroupExists indicates whether a resource group exists within a subscription; otherwise false\n// This function would fail the test if there is an error.\nfunc ResourceGroupExistsV2(t *testing.T, resourceGroupName string, subscriptionID string) bool {\n\tresult, err := ResourceGroupExistsV2E(resourceGroupName, subscriptionID)\n\trequire.NoError(t, err)\n\treturn result\n}\n\n// ResourceGroupExistsE indicates whether a resource group exists within a subscription\nfunc ResourceGroupExistsV2E(resourceGroupName, subscriptionID string) (bool, error) {\n\texists, err := GetResourceGroupV2E(resourceGroupName, subscriptionID)\n\tif err != nil {\n\t\tif resourceGroupNotFoundError(err) {\n\t\t\treturn false, nil\n\t\t}\n\t\treturn false, err\n\t}\n\treturn exists, nil\n\n}\n\n// GetResourceGroupE gets a resource group within a subscription\nfunc GetResourceGroupV2E(resourceGroupName, subscriptionID string) (bool, error) {\n\trg, err := GetAResourceGroupV2E(resourceGroupName, subscriptionID)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\treturn (resourceGroupName == *rg.Name), nil\n}\n\n// GetAResourceGroup returns a resource group within a subscription\n// This function would fail the test if there is an error.\nfunc GetAResourceGroupV2(t *testing.T, resourceGroupName string, subscriptionID string) *armresources.ResourceGroup {\n\trg, err := GetAResourceGroupV2E(resourceGroupName, subscriptionID)\n\trequire.NoError(t, err)\n\treturn rg\n}\n\n// GetAResourceGroupE gets a resource group within a subscription\nfunc GetAResourceGroupV2E(resourceGroupName, subscriptionID string) (*armresources.ResourceGroup, error) {\n\tclient, err := CreateResourceGroupClientV2E(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\trg, err := client.Get(context.Background(), resourceGroupName, &armresources.ResourceGroupsClientGetOptions{})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &rg.ResourceGroup, nil\n}\n\n// ListResourceGroupsByTag returns a resource group list within a subscription based on a tag key\n// This function would fail the test if there is an error.\nfunc ListResourceGroupsByTagV2(t *testing.T, tag, subscriptionID string) []*armresources.ResourceGroup {\n\trg, err := ListResourceGroupsByTagV2E(tag, subscriptionID)\n\trequire.NoError(t, err)\n\treturn rg\n}\n\n// ListResourceGroupsByTagE returns a resource group list within a subscription based on a tag key\nfunc ListResourceGroupsByTagV2E(tag string, subscriptionID string) (rg []*armresources.ResourceGroup, err error) {\n\tclient, err := CreateResourceGroupClientV2E(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfilter := fmt.Sprintf(\"tagName eq '%s'\", tag)\n\tpager := client.NewListPager(&armresources.ResourceGroupsClientListOptions{\n\t\tFilter: &filter,\n\t})\n\tctx := context.Background()\n\tfor pager.More() {\n\t\tpage, err := pager.NextPage(ctx)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\trg = append(rg, page.ResourceGroupListResult.Value...)\n\t}\n\n\treturn\n}\n"
  },
  {
    "path": "modules/azure/resourcegroupv2_test.go",
    "content": "//go:build azure\n// +build azure\n\n// NOTE: We use build tags to differentiate azure testing because we currently do not have azure access setup for\n// CircleCI.\npackage azure\n\nimport (\n\t\"testing\"\n\n\t\"github.com/Azure/azure-sdk-for-go/sdk/azcore\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n/*\nThe below tests are currently stubbed out, with the expectation that they will throw errors.\nIf/when methods to create and delete resource groups are added, these tests can be extended.\n*/\n\nfunc TestResourceGroupExistsV2(t *testing.T) {\n\tt.Parallel()\n\n\tresourceGroupName := \"fakeResourceGroupName\"\n\texists, err := ResourceGroupExistsV2E(resourceGroupName, \"\")\n\tassert.NoError(t, err)\n\tassert.False(t, exists)\n}\n\nfunc TestGetAResourceGroupV2(t *testing.T) {\n\tt.Parallel()\n\n\tresourceGroupName := \"fakeResourceGroupName\"\n\n\t_, err := GetAResourceGroupV2E(resourceGroupName, \"\")\n\terrAzure := &azcore.ResponseError{}\n\trequire.ErrorAs(t, err, &errAzure)\n\tassert.Equal(t, errAzure.StatusCode, 404)\n}\n"
  },
  {
    "path": "modules/azure/resourceid.go",
    "content": "package azure\n\nimport \"github.com/gruntwork-io/terratest/modules/collections\"\n\n// GetNameFromResourceID gets the Name from an Azure Resource ID.\nfunc GetNameFromResourceID(resourceID string) string {\n\tid, err := GetNameFromResourceIDE(resourceID)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\treturn id\n}\n\n// GetNameFromResourceIDE gets the Name from an Azure Resource ID.\n// This function would fail the test if there is an error.\nfunc GetNameFromResourceIDE(resourceID string) (string, error) {\n\tid, err := collections.GetSliceLastValueE(resourceID, \"/\")\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn id, nil\n}\n"
  },
  {
    "path": "modules/azure/resourceid_test.go",
    "content": "//go:build azure\n// +build azure\n\n// NOTE: We use build tags to differentiate azure testing because we currently do not have azure access setup for\n// CircleCI.\n\npackage azure\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestGetNameFromResourceID(t *testing.T) {\n\tt.Parallel()\n\n\t// set slice variables\n\tsliceSource := \"this/is/a/long/slash/separated/string/ResourceID\"\n\tsliceResult := \"ResourceID\"\n\tsliceNotFound := \"noresourcepresent\"\n\n\t// verify success\n\tresultSuccess := GetNameFromResourceID(sliceSource)\n\tassert.Equal(t, sliceResult, resultSuccess)\n\n\t// verify error when seperator not found\n\tresultBadSeperator := GetNameFromResourceID(sliceNotFound)\n\tassert.Equal(t, \"\", resultBadSeperator)\n}\n"
  },
  {
    "path": "modules/azure/servicebus.go",
    "content": "package azure\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/Azure/azure-sdk-for-go/services/servicebus/mgmt/2017-04-01/servicebus\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc serviceBusNamespaceClientE(subscriptionID string) (*servicebus.NamespacesClient, error) {\n\tauthorizer, err := NewAuthorizer()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tnsClient := servicebus.NewNamespacesClient(subscriptionID)\n\tnsClient.Authorizer = *authorizer\n\treturn &nsClient, nil\n}\n\nfunc serviceBusTopicClientE(subscriptionID string) (*servicebus.TopicsClient, error) {\n\tauthorizer, err := NewAuthorizer()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\ttClient := servicebus.NewTopicsClient(subscriptionID)\n\ttClient.Authorizer = *authorizer\n\treturn &tClient, nil\n}\n\nfunc serviceBusSubscriptionsClientE(subscriptionID string) (*servicebus.SubscriptionsClient, error) {\n\tauthorizer, err := NewAuthorizer()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tsClient := servicebus.NewSubscriptionsClient(subscriptionID)\n\tsClient.Authorizer = *authorizer\n\treturn &sClient, nil\n}\n\n// ListServiceBusNamespaceE list all SB namespaces in all resource groups in the given subscription ID.\nfunc ListServiceBusNamespaceE(subscriptionID string) ([]servicebus.SBNamespace, error) {\n\tnsClient, err := serviceBusNamespaceClientE(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\titeratorSBNamespace, err := nsClient.ListComplete(context.Background())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresults := make([]servicebus.SBNamespace, 0)\n\tfor iteratorSBNamespace.NotDone() {\n\t\tresults = append(results, iteratorSBNamespace.Value())\n\t\tif err := iteratorSBNamespace.Next(); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn results, nil\n}\n\n// ListServiceBusNamespace - list all SB namespaces in all resource groups in the given subscription ID. This function would fail the test if there is an error.\nfunc ListServiceBusNamespace(t *testing.T, subscriptionID string) []servicebus.SBNamespace {\n\tresults, err := ListServiceBusNamespaceE(subscriptionID)\n\n\trequire.NoError(t, err)\n\n\treturn results\n}\n\n// ListServiceBusNamespaceNamesE list names of all SB namespaces in all resource groups in the given subscription ID.\nfunc ListServiceBusNamespaceNamesE(subscriptionID string) ([]string, error) {\n\tsbNamespace, err := ListServiceBusNamespaceE(subscriptionID)\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresults := BuildNamespaceNamesList(sbNamespace)\n\treturn results, nil\n}\n\n// BuildNamespaceNamesList helper method to build namespace name list\nfunc BuildNamespaceNamesList(sbNamespace []servicebus.SBNamespace) []string {\n\tresults := []string{}\n\tfor _, namespace := range sbNamespace {\n\t\tresults = append(results, *namespace.Name)\n\n\t}\n\n\treturn results\n}\n\n// BuildNamespaceIdsList helper method to build namespace id list\nfunc BuildNamespaceIdsList(sbNamespace []servicebus.SBNamespace) []string {\n\tresults := []string{}\n\tfor _, namespace := range sbNamespace {\n\t\tresults = append(results, *namespace.ID)\n\n\t}\n\n\treturn results\n}\n\n// ListServiceBusNamespaceNames list names of all SB namespaces in all resource groups in the given subscription ID. This function would fail the test if there is an error.\nfunc ListServiceBusNamespaceNames(t *testing.T, subscriptionID string) []string {\n\tresults, err := ListServiceBusNamespaceNamesE(subscriptionID)\n\n\trequire.NoError(t, err)\n\n\treturn results\n}\n\n// ListServiceBusNamespaceIDsE list IDs of all SB namespaces in all resource groups in the given subscription ID.\nfunc ListServiceBusNamespaceIDsE(subscriptionID string) ([]string, error) {\n\tsbNamespace, err := ListServiceBusNamespaceE(subscriptionID)\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresults := BuildNamespaceIdsList(sbNamespace)\n\treturn results, nil\n}\n\n// ListServiceBusNamespaceIDs list IDs of all SB namespaces in all resource groups in the given subscription ID. This function would fail the test if there is an error.\nfunc ListServiceBusNamespaceIDs(t *testing.T, subscriptionID string) []string {\n\tresults, err := ListServiceBusNamespaceIDsE(subscriptionID)\n\trequire.NoError(t, err)\n\n\treturn results\n}\n\n// ListServiceBusNamespaceByResourceGroupE list all SB namespaces in the given resource group.\nfunc ListServiceBusNamespaceByResourceGroupE(subscriptionID string, resourceGroup string) ([]servicebus.SBNamespace, error) {\n\tnsClient, err := serviceBusNamespaceClientE(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\titeratorSBNamespace, err := nsClient.ListByResourceGroupComplete(context.Background(), resourceGroup)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresults := make([]servicebus.SBNamespace, 0)\n\n\tfor iteratorSBNamespace.NotDone() {\n\t\tresults = append(results, iteratorSBNamespace.Value())\n\t\tif err := iteratorSBNamespace.Next(); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn results, nil\n}\n\n// ListServiceBusNamespaceByResourceGroup list all SB namespaces in the given resource group. This function would fail the test if there is an error.\nfunc ListServiceBusNamespaceByResourceGroup(t *testing.T, subscriptionID string, resourceGroup string) []servicebus.SBNamespace {\n\tresults, err := ListServiceBusNamespaceByResourceGroupE(subscriptionID, resourceGroup)\n\trequire.NoError(t, err)\n\n\treturn results\n}\n\n// ListServiceBusNamespaceNamesByResourceGroupE list names of all SB namespaces in the given resource group. This function would fail the test if there is an error.\nfunc ListServiceBusNamespaceNamesByResourceGroupE(subscriptionID string, resourceGroup string) ([]string, error) {\n\tsbNamespace, err := ListServiceBusNamespaceByResourceGroupE(subscriptionID, resourceGroup)\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresults := BuildNamespaceNamesList(sbNamespace)\n\treturn results, nil\n}\n\n// ListServiceBusNamespaceNamesByResourceGroup list names of all SB namespaces in the given resource group.\nfunc ListServiceBusNamespaceNamesByResourceGroup(t *testing.T, subscriptionID string, resourceGroup string) []string {\n\tresults, err := ListServiceBusNamespaceNamesByResourceGroupE(subscriptionID, resourceGroup)\n\trequire.NoError(t, err)\n\n\treturn results\n}\n\n// ListServiceBusNamespaceIDsByResourceGroupE list IDs of all SB namespaces in the given resource group.\nfunc ListServiceBusNamespaceIDsByResourceGroupE(subscriptionID string, resourceGroup string) ([]string, error) {\n\tsbNamespace, err := ListServiceBusNamespaceByResourceGroupE(subscriptionID, resourceGroup)\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresults := BuildNamespaceIdsList(sbNamespace)\n\treturn results, nil\n}\n\n// ListServiceBusNamespaceIDsByResourceGroup list IDs of all SB namespaces in the given resource group. This function would fail the test if there is an error.\nfunc ListServiceBusNamespaceIDsByResourceGroup(t *testing.T, subscriptionID string, resourceGroup string) []string {\n\tresults, err := ListServiceBusNamespaceIDsByResourceGroupE(subscriptionID, resourceGroup)\n\trequire.NoError(t, err)\n\n\treturn results\n}\n\n// ListNamespaceAuthRulesE - authenticate namespace client and enumerates all values to get list of authorization rules for the given namespace name,\n// automatically crossing page boundaries as required.\nfunc ListNamespaceAuthRulesE(subscriptionID string, namespace string, resourceGroup string) ([]string, error) {\n\tnsClient, err := serviceBusNamespaceClientE(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\titeratorNamespaceRules, err := nsClient.ListAuthorizationRulesComplete(\n\t\tcontext.Background(), resourceGroup, namespace)\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresults := []string{}\n\tfor iteratorNamespaceRules.NotDone() {\n\t\tresults = append(results, *(iteratorNamespaceRules.Value()).Name)\n\t\tif err := iteratorNamespaceRules.Next(); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\treturn results, nil\n}\n\n// ListNamespaceAuthRules - authenticate namespace client and enumerates all values to get list of authorization rules for the given namespace name,\n// automatically crossing page boundaries as required. This function would fail the test if there is an error.\nfunc ListNamespaceAuthRules(t *testing.T, subscriptionID string, namespace string, resourceGroup string) []string {\n\tresults, err := ListNamespaceAuthRulesE(subscriptionID, namespace, resourceGroup)\n\trequire.NoError(t, err)\n\n\treturn results\n}\n\n// ListNamespaceTopicsE - authenticate topic client and enumerates all values, automatically crossing page boundaries as required.\nfunc ListNamespaceTopicsE(subscriptionID string, namespace string, resourceGroup string) ([]servicebus.SBTopic, error) {\n\ttClient, err := serviceBusTopicClientE(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\titeratorTopics, err := tClient.ListByNamespaceComplete(context.Background(), resourceGroup, namespace, nil, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresults := make([]servicebus.SBTopic, 0)\n\n\tfor iteratorTopics.NotDone() {\n\t\tresults = append(results, iteratorTopics.Value())\n\t\tif err := iteratorTopics.Next(); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn results, nil\n}\n\n// ListNamespaceTopics - authenticate topic client and enumerates all values, automatically crossing page boundaries as required. This function would fail the test if there is an error.\nfunc ListNamespaceTopics(t *testing.T, subscriptionID string, namespace string, resourceGroup string) []servicebus.SBTopic {\n\tresults, err := ListNamespaceTopicsE(subscriptionID, namespace, resourceGroup)\n\trequire.NoError(t, err)\n\n\treturn results\n}\n\n// ListTopicSubscriptionsE - authenticate subscriptions client and enumerates all values, automatically crossing page boundaries as required.\nfunc ListTopicSubscriptionsE(subscriptionID string, namespace string, resourceGroup string, topicName string) ([]servicebus.SBSubscription, error) {\n\tsClient, err := serviceBusSubscriptionsClientE(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\titeratorSubscription, err := sClient.ListByTopicComplete(context.Background(), resourceGroup, namespace, topicName, nil, nil)\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresults := make([]servicebus.SBSubscription, 0)\n\n\tfor iteratorSubscription.NotDone() {\n\t\tresults = append(results, iteratorSubscription.Value())\n\t\tif err := iteratorSubscription.Next(); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\treturn results, nil\n}\n\n// ListTopicSubscriptions - authenticate subscriptions client and enumerates all values, automatically crossing page boundaries as required. This function would fail the test if there is an error.\nfunc ListTopicSubscriptions(t *testing.T, subscriptionID string, namespace string, resourceGroup string, topicName string) []servicebus.SBSubscription {\n\tresults, err := ListTopicSubscriptionsE(subscriptionID, namespace, resourceGroup, topicName)\n\trequire.NoError(t, err)\n\n\treturn results\n}\n\n// ListTopicSubscriptionsNameE - authenticate subscriptions client and enumerates all values to get list of subscriptions for the given topic name,\n// automatically crossing page boundaries as required.\nfunc ListTopicSubscriptionsNameE(subscriptionID string, namespace string, resourceGroup string, topicName string) ([]string, error) {\n\tsClient, err := serviceBusSubscriptionsClientE(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\titeratorSubscription, err := sClient.ListByTopicComplete(context.Background(), resourceGroup, namespace, topicName, nil, nil)\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresults := []string{}\n\tfor iteratorSubscription.NotDone() {\n\t\tresults = append(results, *(iteratorSubscription.Value()).Name)\n\t\tif err := iteratorSubscription.Next(); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\treturn results, nil\n}\n\n// ListTopicSubscriptionsName -  authenticate subscriptions client and enumerates all values to get list of subscriptions for the given topic name,\n// automatically crossing page boundaries as required. This function would fail the test if there is an error.\nfunc ListTopicSubscriptionsName(t *testing.T, subscriptionID string, namespace string, resourceGroup string, topicName string) []string {\n\tresults, err := ListTopicSubscriptionsNameE(subscriptionID, namespace, resourceGroup, topicName)\n\trequire.NoError(t, err)\n\n\treturn results\n}\n\n// ListTopicAuthRulesE - authenticate topic client and enumerates all values to get list of authorization rules for the given topic name,\n// automatically crossing page boundaries as required.\nfunc ListTopicAuthRulesE(subscriptionID string, namespace string, resourceGroup string, topicName string) ([]string, error) {\n\ttClient, err := serviceBusTopicClientE(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\titeratorTopicsRules, err := tClient.ListAuthorizationRulesComplete(\n\t\tcontext.Background(), resourceGroup, namespace, topicName)\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresults := []string{}\n\tfor iteratorTopicsRules.NotDone() {\n\t\tresults = append(results, *(iteratorTopicsRules.Value()).Name)\n\t\tif err := iteratorTopicsRules.Next(); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\treturn results, nil\n}\n\n// ListTopicAuthRules - authenticate topic client and enumerates all values to get list of authorization rules for the given topic name,\n// automatically crossing page boundaries as required.  This function would fail the test if there is an error.\nfunc ListTopicAuthRules(t *testing.T, subscriptionID string, namespace string, resourceGroup string, topicName string) []string {\n\tresults, err := ListTopicAuthRulesE(subscriptionID, namespace, resourceGroup, topicName)\n\trequire.NoError(t, err)\n\n\treturn results\n}\n"
  },
  {
    "path": "modules/azure/servicebus_test.go",
    "content": "//go:build azure\n// +build azure\n\n// NOTE: We use build tags to differentiate azure testing because we currently do not have azure access setup for\n// CircleCI.\npackage azure\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\n/*\nThe below tests are currently stubbed out, with the expectation that they will throw errors. These tests can be extended.\n*/\nfunc TestListServiceBusNamespaceNamesE(t *testing.T) {\n\tt.Parallel()\n\n\tsubscriptionID := \"\"\n\n\t_, err := ListServiceBusNamespaceNamesE(subscriptionID)\n\trequire.Error(t, err)\n}\n\nfunc TestListServiceBusNamespaceIDsByResourceGroupE(t *testing.T) {\n\tt.Parallel()\n\n\tsubscriptionID := \"\"\n\tresourceGroup := \"\"\n\n\t_, err := ListServiceBusNamespaceIDsByResourceGroupE(subscriptionID, resourceGroup)\n\trequire.Error(t, err)\n}\n\nfunc TestListNamespaceAuthRulesE(t *testing.T) {\n\tt.Parallel()\n\n\tsubscriptionID := \"\"\n\tnamespace := \"\"\n\tresourceGroup := \"\"\n\n\t_, err := ListNamespaceAuthRulesE(subscriptionID, namespace, resourceGroup)\n\trequire.Error(t, err)\n}\n\nfunc TestListNamespaceTopicsE(t *testing.T) {\n\tt.Parallel()\n\n\tsubscriptionID := \"\"\n\tnamespace := \"\"\n\tresourceGroup := \"\"\n\n\t_, err := ListNamespaceTopicsE(subscriptionID, namespace, resourceGroup)\n\trequire.Error(t, err)\n}\n\nfunc TestListTopicAuthRulesE(t *testing.T) {\n\tt.Parallel()\n\n\tsubscriptionID := \"\"\n\tnamespace := \"\"\n\tresourceGroup := \"\"\n\ttopicName := \"\"\n\n\t_, err := ListTopicAuthRulesE(subscriptionID, namespace, resourceGroup, topicName)\n\trequire.Error(t, err)\n}\n\nfunc TestListTopicSubscriptionsNameE(t *testing.T) {\n\tt.Parallel()\n\n\tsubscriptionID := \"\"\n\tnamespace := \"\"\n\tresourceGroup := \"\"\n\ttopicName := \"\"\n\n\t_, err := ListTopicSubscriptionsNameE(subscriptionID, namespace, resourceGroup, topicName)\n\trequire.Error(t, err)\n}\n"
  },
  {
    "path": "modules/azure/sql.go",
    "content": "package azure\n\nimport (\n\t\"context\"\n\n\t\"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// GetSQLServerClient is a helper function that will setup a sql server client\nfunc GetSQLServerClient(subscriptionID string) (*armsql.ServersClient, error) {\n\treturn CreateSQLServerClient(subscriptionID)\n}\n\n// GetSQLServer is a helper function that gets the sql server object.\n// This function would fail the test if there is an error.\nfunc GetSQLServer(t testing.TestingT, resGroupName string, serverName string, subscriptionID string) *armsql.Server {\n\tsqlServer, err := GetSQLServerE(t, subscriptionID, resGroupName, serverName)\n\trequire.NoError(t, err)\n\n\treturn sqlServer\n}\n\n// GetSQLServerE is a helper function that gets the sql server object.\nfunc GetSQLServerE(t testing.TestingT, subscriptionID string, resGroupName string, serverName string) (*armsql.Server, error) {\n\t// Create a SQL Server client\n\tsqlClient, err := CreateSQLServerClient(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Get the corresponding server\n\tresp, err := sqlClient.Get(context.Background(), resGroupName, serverName, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &resp.Server, nil\n}\n\n// GetDatabaseClient is a helper function that will setup a sql DB client\nfunc GetDatabaseClient(subscriptionID string) (*armsql.DatabasesClient, error) {\n\treturn CreateDatabaseClient(subscriptionID)\n}\n\n// ListSQLServerDatabases is a helper function that gets a list of databases on a sql server\nfunc ListSQLServerDatabases(t testing.TestingT, resGroupName string, serverName string, subscriptionID string) []*armsql.Database {\n\tdbList, err := ListSQLServerDatabasesE(t, resGroupName, serverName, subscriptionID)\n\trequire.NoError(t, err)\n\n\treturn dbList\n}\n\n// ListSQLServerDatabasesE is a helper function that gets a list of databases on a sql server\nfunc ListSQLServerDatabasesE(t testing.TestingT, resGroupName string, serverName string, subscriptionID string) ([]*armsql.Database, error) {\n\t// Create a SQL db client\n\tsqlClient, err := CreateDatabaseClient(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Get the databases using pager\n\tpager := sqlClient.NewListByServerPager(resGroupName, serverName, nil)\n\tvar databases []*armsql.Database\n\tfor pager.More() {\n\t\tpage, err := pager.NextPage(context.Background())\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tdatabases = append(databases, page.Value...)\n\t}\n\n\treturn databases, nil\n}\n\n// GetSQLDatabase is a helper function that gets the sql db.\n// This function would fail the test if there is an error.\nfunc GetSQLDatabase(t testing.TestingT, resGroupName string, serverName string, dbName string, subscriptionID string) *armsql.Database {\n\tdatabase, err := GetSQLDatabaseE(t, subscriptionID, resGroupName, serverName, dbName)\n\trequire.NoError(t, err)\n\n\treturn database\n}\n\n// GetSQLDatabaseE is a helper function that gets the sql db.\nfunc GetSQLDatabaseE(t testing.TestingT, subscriptionID string, resGroupName string, serverName string, dbName string) (*armsql.Database, error) {\n\t// Create a SQL db client\n\tsqlClient, err := CreateDatabaseClient(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Get the corresponding DB\n\tresp, err := sqlClient.Get(context.Background(), resGroupName, serverName, dbName, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &resp.Database, nil\n}\n"
  },
  {
    "path": "modules/azure/sql_managedinstance.go",
    "content": "package azure\n\nimport (\n\t\"context\"\n\n\t\"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// SQLManagedInstanceExists indicates whether the SQL Managed Instance exists for the subscription.\n// This function would fail the test if there is an error.\nfunc SQLManagedInstanceExists(t testing.TestingT, managedInstanceName string, resourceGroupName string, subscriptionID string) bool {\n\texists, err := SQLManagedInstanceExistsE(managedInstanceName, resourceGroupName, subscriptionID)\n\trequire.NoError(t, err)\n\treturn exists\n}\n\n// SQLManagedInstanceExistsE indicates whether the specified SQL Managed Instance exists and may return an error.\nfunc SQLManagedInstanceExistsE(managedInstanceName string, resourceGroupName string, subscriptionID string) (bool, error) {\n\t_, err := GetManagedInstanceE(subscriptionID, resourceGroupName, managedInstanceName)\n\tif err != nil {\n\t\tif ResourceNotFoundErrorExists(err) {\n\t\t\treturn false, nil\n\t\t}\n\t\treturn false, err\n\t}\n\treturn true, nil\n}\n\n// GetManagedInstance is a helper function that gets the sql managed instance object.\n// This function would fail the test if there is an error.\nfunc GetManagedInstance(t testing.TestingT, resGroupName string, managedInstanceName string, subscriptionID string) *armsql.ManagedInstance {\n\tmanagedInstance, err := GetManagedInstanceE(subscriptionID, resGroupName, managedInstanceName)\n\trequire.NoError(t, err)\n\n\treturn managedInstance\n}\n\n// GetManagedInstanceDatabase is a helper function that gets the sql managed database object.\n// This function would fail the test if there is an error.\nfunc GetManagedInstanceDatabase(t testing.TestingT, resGroupName string, managedInstanceName string, databaseName string, subscriptionID string) *armsql.ManagedDatabase {\n\tmanagedDatabase, err := GetManagedInstanceDatabaseE(t, subscriptionID, resGroupName, managedInstanceName, databaseName)\n\trequire.NoError(t, err)\n\n\treturn managedDatabase\n}\n\n// GetManagedInstanceE is a helper function that gets the sql managed instance object.\nfunc GetManagedInstanceE(subscriptionID string, resGroupName string, managedInstanceName string) (*armsql.ManagedInstance, error) {\n\t// Create a SQL Managed Instance client\n\tsqlmiClient, err := CreateSQLMangedInstanceClient(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Get the corresponding managed instance\n\tresp, err := sqlmiClient.Get(context.Background(), resGroupName, managedInstanceName, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &resp.ManagedInstance, nil\n}\n\n// GetManagedInstanceDatabaseE is a helper function that gets the sql managed database object.\nfunc GetManagedInstanceDatabaseE(t testing.TestingT, subscriptionID string, resGroupName string, managedInstanceName string, databaseName string) (*armsql.ManagedDatabase, error) {\n\t// Create a SQL MI db client\n\tsqlmiDbClient, err := CreateSQLMangedDatabasesClient(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Get the corresponding database\n\tresp, err := sqlmiDbClient.Get(context.Background(), resGroupName, managedInstanceName, databaseName, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &resp.ManagedDatabase, nil\n}\n"
  },
  {
    "path": "modules/azure/sql_managedinstance_test.go",
    "content": "//go:build azure\n// +build azure\n\n// NOTE: We use build tags to differentiate azure testing because we currently do not have azure access setup for\n// CircleCI.\npackage azure\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\n/*\nThe below tests are currently stubbed out, with the expectation that they will throw errors.\nIf/when CRUD methods are introduced for Azure SQL DB, these tests can be extended\n*/\n\nfunc TestSQLManagedInstanceExists(t *testing.T) {\n\tt.Parallel()\n\n\tmanagedInstanceName := \"\"\n\tresourceGroupName := \"\"\n\tsubscriptionID := \"\"\n\n\texists, err := SQLManagedInstanceExistsE(managedInstanceName, resourceGroupName, subscriptionID)\n\n\trequire.False(t, exists)\n\trequire.Error(t, err)\n}\n\nfunc TestGetManagedInstanceE(t *testing.T) {\n\tt.Parallel()\n\n\tresGroupName := \"\"\n\tmanagedInstanceName := \"\"\n\tsubscriptionID := \"\"\n\n\t_, err := GetManagedInstanceE(subscriptionID, resGroupName, managedInstanceName)\n\trequire.Error(t, err)\n}\n\nfunc TestGetManagedInstanceDatabasesE(t *testing.T) {\n\tt.Parallel()\n\n\tresGroupName := \"\"\n\tmanagedInstanceName := \"\"\n\tdatabaseName := \"\"\n\tsubscriptionID := \"\"\n\n\t_, err := GetManagedInstanceDatabaseE(t, subscriptionID, resGroupName, managedInstanceName, databaseName)\n\trequire.Error(t, err)\n}\n"
  },
  {
    "path": "modules/azure/sql_test.go",
    "content": "//go:build azure\n// +build azure\n\n// NOTE: We use build tags to differentiate azure testing because we currently do not have azure access setup for\n// CircleCI.\npackage azure\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\n/*\nThe below tests are currently stubbed out, with the expectation that they will throw errors.\nIf/when CRUD methods are introduced for Azure SQL DB, these tests can be extended\n*/\n\nfunc TestGetSQLServerE(t *testing.T) {\n\tt.Parallel()\n\n\tresGroupName := \"\"\n\tserverName := \"\"\n\tsubscriptionID := \"\"\n\n\t_, err := GetSQLServerE(t, resGroupName, serverName, subscriptionID)\n\trequire.Error(t, err)\n}\n\nfunc TestGetSQLDatabaseE(t *testing.T) {\n\tt.Parallel()\n\n\tresGroupName := \"\"\n\tserverName := \"\"\n\tdbName := \"\"\n\tsubscriptionID := \"\"\n\n\t_, err := GetSQLDatabaseE(t, resGroupName, serverName, dbName, subscriptionID)\n\trequire.Error(t, err)\n}\n"
  },
  {
    "path": "modules/azure/storage.go",
    "content": "package azure\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/Azure/azure-sdk-for-go/services/storage/mgmt/2019-06-01/storage\"\n\t\"github.com/Azure/go-autorest/autorest/azure\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// StorageAccountExists indicates whether the storage account name exactly matches; otherwise false.\n// This function would fail the test if there is an error.\nfunc StorageAccountExists(t *testing.T, storageAccountName string, resourceGroupName string, subscriptionID string) bool {\n\tresult, err := StorageAccountExistsE(storageAccountName, resourceGroupName, subscriptionID)\n\trequire.NoError(t, err)\n\treturn result\n}\n\n// StorageBlobContainerExists returns true if the container name exactly matches; otherwise false\n// This function would fail the test if there is an error.\nfunc StorageBlobContainerExists(t *testing.T, containerName string, storageAccountName string, resourceGroupName string, subscriptionID string) bool {\n\tresult, err := StorageBlobContainerExistsE(containerName, storageAccountName, resourceGroupName, subscriptionID)\n\trequire.NoError(t, err)\n\treturn result\n}\n\n// StorageFileShareExists returns true if the file share name exactly matches; otherwise false\n// This function would fail the test if there is an error.\nfunc StorageFileShareExists(t *testing.T, fileSahreName string, storageAccountName string, resourceGroupName string, subscriptionID string) bool {\n\tresult, err := StorageFileShareExistsE(t, fileSahreName, storageAccountName, resourceGroupName, subscriptionID)\n\trequire.NoError(t, err)\n\n\treturn result\n}\n\n// StorageFileShareExists returns true if the file share name exactly matches; otherwise false\nfunc StorageFileShareExistsE(t *testing.T, fileSahreName string, storageAccountName string, resourceGroupName string, subscriptionID string) (bool, error) {\n\t_, err := GetStorageFileShareE(fileSahreName, storageAccountName, resourceGroupName, subscriptionID)\n\tif err != nil {\n\t\tif ResourceNotFoundErrorExists(err) {\n\t\t\treturn false, nil\n\t\t}\n\t\treturn false, err\n\t}\n\treturn true, nil\n}\n\n// GetStorageBlobContainerPublicAccess indicates whether a storage container has public access; otherwise false.\n// This function would fail the test if there is an error.\nfunc GetStorageBlobContainerPublicAccess(t *testing.T, containerName string, storageAccountName string, resourceGroupName string, subscriptionID string) bool {\n\tresult, err := GetStorageBlobContainerPublicAccessE(containerName, storageAccountName, resourceGroupName, subscriptionID)\n\trequire.NoError(t, err)\n\treturn result\n}\n\n// GetStorageAccountKind returns one of Storage, StorageV2, BlobStorage, FileStorage, or BlockBlobStorage.\n// This function would fail the test if there is an error.\nfunc GetStorageAccountKind(t *testing.T, storageAccountName string, resourceGroupName string, subscriptionID string) string {\n\tresult, err := GetStorageAccountKindE(storageAccountName, resourceGroupName, subscriptionID)\n\trequire.NoError(t, err)\n\treturn result\n}\n\n// GetStorageAccountSkuTier returns the storage account sku tier as Standard or Premium.\n// This function would fail the test if there is an error.\nfunc GetStorageAccountSkuTier(t *testing.T, storageAccountName string, resourceGroupName string, subscriptionID string) string {\n\tresult, err := GetStorageAccountSkuTierE(storageAccountName, resourceGroupName, subscriptionID)\n\trequire.NoError(t, err)\n\treturn result\n}\n\n// GetStorageDNSString builds and returns the storage account dns string if the storage account exists.\n// This function would fail the test if there is an error.\nfunc GetStorageDNSString(t *testing.T, storageAccountName string, resourceGroupName string, subscriptionID string) string {\n\tresult, err := GetStorageDNSStringE(storageAccountName, resourceGroupName, subscriptionID)\n\trequire.NoError(t, err)\n\treturn result\n}\n\n// StorageAccountExistsE indicates whether the storage account name exists; otherwise false.\nfunc StorageAccountExistsE(storageAccountName, resourceGroupName, subscriptionID string) (bool, error) {\n\t_, err := GetStorageAccountE(storageAccountName, resourceGroupName, subscriptionID)\n\tif err != nil {\n\t\tif ResourceNotFoundErrorExists(err) {\n\t\t\treturn false, nil\n\t\t}\n\t\treturn false, err\n\t}\n\treturn true, nil\n}\n\n// GetStorageAccountE gets a storage account; otherwise error.  See https://docs.microsoft.com/rest/api/storagerp/storageaccounts/getproperties for more information.\nfunc GetStorageAccountE(storageAccountName, resourceGroupName, subscriptionID string) (*storage.Account, error) {\n\tsubscriptionID, err := getTargetAzureSubscription(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tresourceGroupName, err2 := getTargetAzureResourceGroupName((resourceGroupName))\n\tif err2 != nil {\n\t\treturn nil, err2\n\t}\n\tstorageAccount, err3 := GetStorageAccountPropertyE(storageAccountName, resourceGroupName, subscriptionID)\n\tif err3 != nil {\n\t\treturn nil, err3\n\t}\n\treturn storageAccount, nil\n}\n\n// StorageBlobContainerExistsE returns true if the container name exists; otherwise false.\nfunc StorageBlobContainerExistsE(containerName, storageAccountName, resourceGroupName, subscriptionID string) (bool, error) {\n\t_, err := GetStorageBlobContainerE(containerName, storageAccountName, resourceGroupName, subscriptionID)\n\tif err != nil {\n\t\tif ResourceNotFoundErrorExists(err) {\n\t\t\treturn false, nil\n\t\t}\n\t\treturn false, err\n\t}\n\treturn true, nil\n}\n\n// GetStorageBlobContainerPublicAccessE indicates whether a storage container has public access; otherwise false.\nfunc GetStorageBlobContainerPublicAccessE(containerName, storageAccountName, resourceGroupName, subscriptionID string) (bool, error) {\n\tcontainer, err := GetStorageBlobContainerE(containerName, storageAccountName, resourceGroupName, subscriptionID)\n\tif err != nil {\n\t\tif ResourceNotFoundErrorExists(err) {\n\t\t\treturn false, nil\n\t\t}\n\t\treturn false, err\n\t}\n\n\treturn (string(container.PublicAccess) != \"None\"), nil\n}\n\n// GetStorageAccountKindE returns one of Storage, StorageV2, BlobStorage, FileStorage, or BlockBlobStorage.\nfunc GetStorageAccountKindE(storageAccountName, resourceGroupName, subscriptionID string) (string, error) {\n\n\tstorageAccount, err := GetStorageAccountPropertyE(storageAccountName, resourceGroupName, subscriptionID)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn string(storageAccount.Kind), nil\n}\n\n// GetStorageAccountSkuTierE returns the storage account sku tier as Standard or Premium.\nfunc GetStorageAccountSkuTierE(storageAccountName, resourceGroupName, subscriptionID string) (string, error) {\n\tstorageAccount, err := GetStorageAccountPropertyE(storageAccountName, resourceGroupName, subscriptionID)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn string(storageAccount.Sku.Tier), nil\n}\n\n// GetStorageBlobContainerE returns Blob container client.\nfunc GetStorageBlobContainerE(containerName, storageAccountName, resourceGroupName, subscriptionID string) (*storage.BlobContainer, error) {\n\tsubscriptionID, err := getTargetAzureSubscription(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tresourceGroupName, err2 := getTargetAzureResourceGroupName((resourceGroupName))\n\tif err2 != nil {\n\t\treturn nil, err2\n\t}\n\tclient, err := CreateStorageBlobContainerClientE(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tcontainer, err := client.Get(context.Background(), resourceGroupName, storageAccountName, containerName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &container, nil\n}\n\n// GetStorageAccountPropertyE returns StorageAccount properties.\nfunc GetStorageAccountPropertyE(storageAccountName, resourceGroupName, subscriptionID string) (*storage.Account, error) {\n\tsubscriptionID, err := getTargetAzureSubscription(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tresourceGroupName, err2 := getTargetAzureResourceGroupName((resourceGroupName))\n\tif err2 != nil {\n\t\treturn nil, err2\n\t}\n\tclient, err := CreateStorageAccountClientE(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\taccount, err := client.GetProperties(context.Background(), resourceGroupName, storageAccountName, \"\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &account, nil\n}\n\n// GetStorageFileShare returns specified file share. This function would fail the test if there is an error.\nfunc GetStorageFileShare(t *testing.T, fileShareName, storageAccountName, resourceGroupName, subscriptionID string) *storage.FileShare {\n\tfileSahre, err := GetStorageFileShareE(fileShareName, storageAccountName, resourceGroupName, subscriptionID)\n\trequire.NoError(t, err)\n\n\treturn fileSahre\n}\n\n// GetStorageFileSharesE returns specified file share.\nfunc GetStorageFileShareE(fileShareName, storageAccountName, resourceGroupName, subscriptionID string) (*storage.FileShare, error) {\n\tresourceGroupName, err2 := getTargetAzureResourceGroupName(resourceGroupName)\n\tif err2 != nil {\n\t\treturn nil, err2\n\t}\n\tclient, err := CreateStorageFileSharesClientE(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tfileShare, err := client.Get(context.Background(), resourceGroupName, storageAccountName, fileShareName, \"stats\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &fileShare, nil\n}\n\n// GetStorageAccountClientE creates a storage account client.\n// TODO: remove in next version\nfunc GetStorageAccountClientE(subscriptionID string) (*storage.AccountsClient, error) {\n\t// Validate Azure subscription ID\n\tsubscriptionID, err := getTargetAzureSubscription(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tstorageAccountClient := storage.NewAccountsClient(subscriptionID)\n\tauthorizer, err := NewAuthorizer()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tstorageAccountClient.Authorizer = *authorizer\n\treturn &storageAccountClient, nil\n}\n\n// GetStorageBlobContainerClientE creates a storage container client.\n// TODO: remove in next version\nfunc GetStorageBlobContainerClientE(subscriptionID string) (*storage.BlobContainersClient, error) {\n\tsubscriptionID, err := getTargetAzureSubscription(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tblobContainerClient := storage.NewBlobContainersClient(subscriptionID)\n\tauthorizer, err := NewAuthorizer()\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tblobContainerClient.Authorizer = *authorizer\n\treturn &blobContainerClient, nil\n}\n\n// GetStorageURISuffixE returns the proper storage URI suffix for the configured Azure environment.\nfunc GetStorageURISuffixE() (string, error) {\n\tenvName := \"AzurePublicCloud\"\n\tenv, err := azure.EnvironmentFromName(envName)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn env.StorageEndpointSuffix, nil\n}\n\n// GetStorageAccountPrimaryBlobEndpointE gets the storage account blob endpoint as URI string.\nfunc GetStorageAccountPrimaryBlobEndpointE(storageAccountName, resourceGroupName, subscriptionID string) (string, error) {\n\tstorageAccount, err := GetStorageAccountPropertyE(storageAccountName, resourceGroupName, subscriptionID)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn *storageAccount.AccountProperties.PrimaryEndpoints.Blob, nil\n}\n\n// GetStorageDNSStringE builds and returns the storage account dns string if the storage account exists.\nfunc GetStorageDNSStringE(storageAccountName, resourceGroupName, subscriptionID string) (string, error) {\n\tretval, err := StorageAccountExistsE(storageAccountName, resourceGroupName, subscriptionID)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tif retval {\n\t\tstorageSuffix, err2 := GetStorageURISuffixE()\n\t\tif err2 != nil {\n\t\t\treturn \"\", err2\n\t\t}\n\t\treturn fmt.Sprintf(\"https://%s.blob.%s/\", storageAccountName, storageSuffix), nil\n\t}\n\n\treturn \"\", NewNotFoundError(\"storage account\", storageAccountName, \"\")\n}\n"
  },
  {
    "path": "modules/azure/storage_test.go",
    "content": "package azure\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\n/*\nThe below tests are currently stubbed out, with the expectation that they will throw errors.\nIf/when methods to create and delete storage accounts are added, these tests can be extended.\n*/\n\nfunc TestStorageAccountExists(t *testing.T) {\n\t_, err := StorageAccountExistsE(\"\", \"\", \"\")\n\trequire.Error(t, err)\n}\n\nfunc TestStorageBlobContainerExists(t *testing.T) {\n\t_, err := StorageBlobContainerExistsE(\"\", \"\", \"\", \"\")\n\trequire.Error(t, err)\n}\n\nfunc TestStorageBlobContainerPublicAccess(t *testing.T) {\n\t_, err := GetStorageBlobContainerPublicAccessE(\"\", \"\", \"\", \"\")\n\trequire.Error(t, err)\n}\n\nfunc TestGetStorageAccountKind(t *testing.T) {\n\t_, err := GetStorageAccountKindE(\"\", \"\", \"\")\n\trequire.Error(t, err)\n}\n\nfunc TestGetStorageAccountSkuTier(t *testing.T) {\n\t_, err := GetStorageAccountSkuTierE(\"\", \"\", \"\")\n\trequire.Error(t, err)\n}\n\nfunc TestGetStorageDNSString(t *testing.T) {\n\t_, err := GetStorageDNSStringE(\"\", \"\", \"\")\n\trequire.Error(t, err)\n}\n"
  },
  {
    "path": "modules/azure/subscription.go",
    "content": "package azure\n\nimport (\n\t\"github.com/Azure/azure-sdk-for-go/services/resources/mgmt/2019-06-01/subscriptions\"\n)\n\n// GetSubscriptionClientE is a helper function that will setup an Azure Subscription client on your behalf\nfunc GetSubscriptionClientE() (*subscriptions.Client, error) {\n\t// Create a Subscription client\n\tclient, err := CreateSubscriptionsClientE()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Create an authorizer\n\tauthorizer, err := NewAuthorizer()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Attach authorizer to the client\n\tclient.Authorizer = *authorizer\n\treturn &client, nil\n}\n"
  },
  {
    "path": "modules/azure/synapse.go",
    "content": "package azure\n\nimport (\n\t\"context\"\n\n\t\"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/synapse/armsynapse\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// GetSynapseWorkspace is a helper function that gets the synapse workspace.\n// This function would fail the test if there is an error.\nfunc GetSynapseWorkspace(t testing.TestingT, resGroupName string, workspaceName string, subscriptionID string) *armsynapse.Workspace {\n\tWorkspace, err := GetSynapseWorkspaceE(t, subscriptionID, resGroupName, workspaceName)\n\trequire.NoError(t, err)\n\n\treturn Workspace\n}\n\n// GetSynapseSqlPool is a helper function that gets the synapse sql pool.\n// This function would fail the test if there is an error.\nfunc GetSynapseSqlPool(t testing.TestingT, resGroupName string, workspaceName string, sqlPoolName string, subscriptionID string) *armsynapse.SQLPool {\n\tSQLPool, err := GetSynapseSqlPoolE(t, subscriptionID, resGroupName, workspaceName, sqlPoolName)\n\trequire.NoError(t, err)\n\n\treturn SQLPool\n}\n\n// GetSynapseWorkspaceE is a helper function that gets the workspace.\nfunc GetSynapseWorkspaceE(t testing.TestingT, subscriptionID string, resGroupName string, workspaceName string) (*armsynapse.Workspace, error) {\n\t// Create a synapse client\n\tsynapseClient, err := CreateSynapseWorkspaceClientE(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Get the corresponding synapse workspace\n\tresp, err := synapseClient.Get(context.Background(), resGroupName, workspaceName, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &resp.Workspace, nil\n}\n\n// GetSynapseSqlPoolE is a helper function that gets the synapse sql pool.\nfunc GetSynapseSqlPoolE(t testing.TestingT, subscriptionID string, resGroupName string, workspaceName string, sqlPoolName string) (*armsynapse.SQLPool, error) {\n\t// Create a synapse sql pool client\n\tsynapseSqlPoolClient, err := CreateSynapseSqlPoolClientE(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Get the corresponding synapse sql pool\n\tresp, err := synapseSqlPoolClient.Get(context.Background(), resGroupName, workspaceName, sqlPoolName, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &resp.SQLPool, nil\n}\n"
  },
  {
    "path": "modules/azure/synapse_test.go",
    "content": "package azure\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\n/*\nThe below tests are currently stubbed out, with the expectation that they will throw errors.\nIf/when CRUD methods are introduced for Azure Synapse, these tests can be extended\n*/\n\nfunc TestGetSynapseWorkspaceE(t *testing.T) {\n\tt.Parallel()\n\n\tresGroupName := \"\"\n\tsubscriptionID := \"\"\n\tworkspaceName := \"\"\n\n\t_, err := GetSynapseWorkspaceE(t, subscriptionID, resGroupName, workspaceName)\n\trequire.Error(t, err)\n}\n\nfunc TestGetSynapseSqlPoolE(t *testing.T) {\n\tt.Parallel()\n\n\tresGroupName := \"\"\n\tsubscriptionID := \"\"\n\tworkspaceName := \"\"\n\tsqlPoolName := \"\"\n\n\t_, err := GetSynapseSqlPoolE(t, subscriptionID, resGroupName, workspaceName, sqlPoolName)\n\trequire.Error(t, err)\n}\n"
  },
  {
    "path": "modules/azure/virtualnetwork.go",
    "content": "package azure\n\nimport (\n\t\"context\"\n\t\"net\"\n\n\t\"github.com/Azure/azure-sdk-for-go/services/network/mgmt/2019-09-01/network\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// VirtualNetworkExists indicates whether the specified Azure Virtual Network exists.\n// This function would fail the test if there is an error.\nfunc VirtualNetworkExists(t testing.TestingT, vnetName string, resGroupName string, subscriptionID string) bool {\n\texists, err := VirtualNetworkExistsE(vnetName, resGroupName, subscriptionID)\n\trequire.NoError(t, err)\n\treturn exists\n}\n\n// VirtualNetworkExistsE indicates whether the specified Azure Virtual Network exists.\nfunc VirtualNetworkExistsE(vnetName string, resGroupName string, subscriptionID string) (bool, error) {\n\t// Get the Virtual Network\n\t_, err := GetVirtualNetworkE(vnetName, resGroupName, subscriptionID)\n\tif err != nil {\n\t\tif ResourceNotFoundErrorExists(err) {\n\t\t\treturn false, nil\n\t\t}\n\t\treturn false, err\n\t}\n\treturn true, nil\n}\n\n// SubnetExists indicates whether the specified Azure Virtual Network Subnet exists.\n// This function would fail the test if there is an error.\nfunc SubnetExists(t testing.TestingT, subnetName string, vnetName string, resGroupName string, subscriptionID string) bool {\n\texists, err := SubnetExistsE(subnetName, vnetName, resGroupName, subscriptionID)\n\trequire.NoError(t, err)\n\treturn exists\n}\n\n// SubnetExistsE indicates whether the specified Azure Virtual Network Subnet exists.\nfunc SubnetExistsE(subnetName string, vnetName string, resGroupName string, subscriptionID string) (bool, error) {\n\t// Get the Subnet\n\t_, err := GetSubnetE(subnetName, vnetName, resGroupName, subscriptionID)\n\tif err != nil {\n\t\tif ResourceNotFoundErrorExists(err) {\n\t\t\treturn false, nil\n\t\t}\n\t\treturn false, err\n\t}\n\treturn true, nil\n}\n\n// CheckSubnetContainsIP checks if the Private IP is contined in the Subnet Address Range.\n// This function would fail the test if there is an error.\nfunc CheckSubnetContainsIP(t testing.TestingT, IP string, subnetName string, vnetName string, resGroupName string, subscriptionID string) bool {\n\tinRange, err := CheckSubnetContainsIPE(IP, subnetName, vnetName, resGroupName, subscriptionID)\n\trequire.NoError(t, err)\n\treturn inRange\n}\n\n// CheckSubnetContainsIPE checks if the Private IP is contined in the Subnet Address Range.\nfunc CheckSubnetContainsIPE(ipAddress string, subnetName string, vnetName string, resGroupName string, subscriptionID string) (bool, error) {\n\t// Convert the IP to a net IP address\n\tip := net.ParseIP(ipAddress)\n\tif ip == nil {\n\t\treturn false, NewFailedToParseError(\"IP Address\", ipAddress)\n\t}\n\n\t// Get Subnet\n\tsubnet, err := GetSubnetE(subnetName, vnetName, resGroupName, subscriptionID)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\t// Get Subnet IP range, this required field is never nil therefore no exception handling required.\n\tsubnetPrefix := *subnet.AddressPrefix\n\n\t// Check if the IP is in the Subnet Range using the net package\n\t_, ipNet, err := net.ParseCIDR(subnetPrefix)\n\tif err != nil {\n\t\treturn false, NewFailedToParseError(\"Subnet Range\", subnetPrefix)\n\t}\n\n\treturn ipNet.Contains(ip), nil\n}\n\n// GetVirtualNetworkSubnets gets all Subnet names and their respective address prefixes in the\n// specified Virtual Network. This function would fail the test if there is an error.\nfunc GetVirtualNetworkSubnets(t testing.TestingT, vnetName string, resGroupName string, subscriptionID string) map[string]string {\n\tsubnets, err := GetVirtualNetworkSubnetsE(vnetName, resGroupName, subscriptionID)\n\trequire.NoError(t, err)\n\treturn subnets\n}\n\n// GetVirtualNetworkSubnetsE gets all Subnet names and their respective address prefixes in the specified Virtual Network.\n// Returning both the name and prefix together helps reduce calls for these frequently accessed properties.\nfunc GetVirtualNetworkSubnetsE(vnetName string, resGroupName string, subscriptionID string) (map[string]string, error) {\n\tsubNetDetails := map[string]string{}\n\n\tclient, err := GetSubnetClientE(subscriptionID)\n\tif err != nil {\n\t\treturn subNetDetails, err\n\t}\n\n\tsubnets, err := client.List(context.Background(), resGroupName, vnetName)\n\tif err != nil {\n\t\treturn subNetDetails, err\n\t}\n\n\tfor _, v := range subnets.Values() {\n\t\tsubnetName := v.Name\n\t\tsubNetAddressPrefix := v.AddressPrefix\n\n\t\tsubNetDetails[string(*subnetName)] = string(*subNetAddressPrefix)\n\t}\n\treturn subNetDetails, nil\n}\n\n// GetVirtualNetworkDNSServerIPs gets a list of all Virtual Network DNS server IPs.\n// This function would fail the test if there is an error.\nfunc GetVirtualNetworkDNSServerIPs(t testing.TestingT, vnetName string, resGroupName string, subscriptionID string) []string {\n\tvnetDNSIPs, err := GetVirtualNetworkDNSServerIPsE(vnetName, resGroupName, subscriptionID)\n\trequire.NoError(t, err)\n\n\treturn vnetDNSIPs\n}\n\n// GetVirtualNetworkDNSServerIPsE gets a list of all Virtual Network DNS server IPs with Error.\nfunc GetVirtualNetworkDNSServerIPsE(vnetName string, resGroupName string, subscriptionID string) ([]string, error) {\n\t// Get Virtual Network\n\tvnet, err := GetVirtualNetworkE(vnetName, resGroupName, subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn *vnet.DhcpOptions.DNSServers, nil\n}\n\n// GetSubnetE gets a subnet.\nfunc GetSubnetE(subnetName string, vnetName string, resGroupName string, subscriptionID string) (*network.Subnet, error) {\n\t// Validate Azure Resource Group\n\tresGroupName, err := getTargetAzureResourceGroupName(resGroupName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Get the client reference\n\tclient, err := GetSubnetClientE(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Get the Subnet\n\tsubnet, err := client.Get(context.Background(), resGroupName, vnetName, subnetName, \"\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &subnet, nil\n}\n\n// GetSubnetClientE creates a subnet client.\nfunc GetSubnetClientE(subscriptionID string) (*network.SubnetsClient, error) {\n\t// Create a new Subnet client from client factory\n\tclient, err := CreateNewSubnetClientE(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Create an authorizer\n\tauthorizer, err := NewAuthorizer()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tclient.Authorizer = *authorizer\n\n\treturn client, nil\n}\n\n// GetVirtualNetworkE gets Virtual Network in the specified Azure Resource Group.\nfunc GetVirtualNetworkE(vnetName string, resGroupName string, subscriptionID string) (*network.VirtualNetwork, error) {\n\t// Validate Azure Resource Group\n\tresGroupName, err := getTargetAzureResourceGroupName(resGroupName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Get the client reference\n\tclient, err := GetVirtualNetworksClientE(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Get the Virtual Network\n\tvnet, err := client.Get(context.Background(), resGroupName, vnetName, \"\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &vnet, nil\n}\n\n// GetVirtualNetworksClientE creates a virtual network client in the specified Azure Subscription.\nfunc GetVirtualNetworksClientE(subscriptionID string) (*network.VirtualNetworksClient, error) {\n\t// Create a new Virtual Network client from client factory\n\tclient, err := CreateNewVirtualNetworkClientE(subscriptionID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Create an authorizer\n\tauthorizer, err := NewAuthorizer()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tclient.Authorizer = *authorizer\n\n\treturn client, nil\n}\n"
  },
  {
    "path": "modules/azure/virtualnetwork_test.go",
    "content": "//go:build azure || (azureslim && network)\n// +build azure azureslim,network\n\n// NOTE: We use build tags to differentiate azure testing because we currently do not have azure access setup for\n// CircleCI.\n\npackage azure\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\n/*\nThe below tests are currently stubbed out, with the expectation that they will throw errors.\nIf/when methods can be mocked or Create/Delete APIs are added, these tests can be extended.\n*/\n\nfunc TestGetVirtualNetworkE(t *testing.T) {\n\tt.Parallel()\n\n\tvnetName := \"\"\n\trgName := \"\"\n\tsubID := \"\"\n\n\t_, err := GetVirtualNetworkE(vnetName, rgName, subID)\n\n\trequire.Error(t, err)\n}\n\nfunc TestGetSubnetE(t *testing.T) {\n\tt.Parallel()\n\n\tsubnetName := \"\"\n\tvnetName := \"\"\n\trgName := \"\"\n\tsubID := \"\"\n\n\t_, err := GetSubnetE(subnetName, vnetName, rgName, subID)\n\n\trequire.Error(t, err)\n}\n\nfunc TestGetVirtualNetworkDNSServerIPsE(t *testing.T) {\n\tt.Parallel()\n\n\tvnetName := \"\"\n\trgName := \"\"\n\tsubID := \"\"\n\n\t_, err := GetVirtualNetworkDNSServerIPsE(vnetName, rgName, subID)\n\n\trequire.Error(t, err)\n}\n\nfunc TestGetVirtualNetworkSubnetsE(t *testing.T) {\n\tt.Parallel()\n\n\tvnetName := \"\"\n\trgName := \"\"\n\tsubID := \"\"\n\n\t_, err := GetVirtualNetworkSubnetsE(vnetName, rgName, subID)\n\n\trequire.Error(t, err)\n}\n\nfunc TestCheckSubnetContainsIPE(t *testing.T) {\n\tt.Parallel()\n\n\tipAddress := \"\"\n\tsubnetName := \"\"\n\tvnetName := \"\"\n\trgName := \"\"\n\tsubID := \"\"\n\n\t_, err := CheckSubnetContainsIPE(ipAddress, subnetName, vnetName, rgName, subID)\n\n\trequire.Error(t, err)\n}\n\nfunc TestSubnetExistsE(t *testing.T) {\n\tt.Parallel()\n\n\tsubnetName := \"\"\n\tvnetName := \"\"\n\trgName := \"\"\n\tsubID := \"\"\n\n\t_, err := SubnetExistsE(subnetName, vnetName, rgName, subID)\n\n\trequire.Error(t, err)\n}\n\nfunc TestVirtualNetworkExistsE(t *testing.T) {\n\tt.Parallel()\n\n\tvnetName := \"\"\n\trgName := \"\"\n\tsubID := \"\"\n\n\t_, err := VirtualNetworkExistsE(vnetName, rgName, subID)\n\n\trequire.Error(t, err)\n}\n"
  },
  {
    "path": "modules/collections/collections.go",
    "content": "// Package collections allows to interact with lists of things.\npackage collections\n"
  },
  {
    "path": "modules/collections/errors.go",
    "content": "package collections\n\n// SliceValueNotFoundError is returned when a provided values file input is not found on the host path.\ntype SliceValueNotFoundError struct {\n\tsourceString string\n}\n\nfunc (err SliceValueNotFoundError) Error() string {\n\treturn \"Could not resolve requested slice value from string \" + err.sourceString\n}\n\n// NewSliceValueNotFoundError creates a new slice found error\nfunc NewSliceValueNotFoundError(sourceString string) SliceValueNotFoundError {\n\treturn SliceValueNotFoundError{sourceString}\n}\n"
  },
  {
    "path": "modules/collections/lists.go",
    "content": "package collections\n\nimport \"slices\"\n\n// ListIntersection returns all the items in both list1 and list2. Note that this will dedup the items so that the\n// output is more predictable. Otherwise, the end list depends on which list was used as the base.\nfunc ListIntersection[T comparable](list1 []T, list2 []T) []T {\n\tout := []T{}\n\n\t// Only need to iterate list1, because we want items in both lists, not union.\n\tfor _, item := range list1 {\n\t\tif slices.Contains(list2, item) && !slices.Contains(out, item) {\n\t\t\tout = append(out, item)\n\t\t}\n\t}\n\n\treturn out\n}\n\n// ListSubtract removes all the items in list2 from list1.\nfunc ListSubtract[T comparable](list1 []T, list2 []T) []T {\n\tout := []T{}\n\n\tfor _, item := range list1 {\n\t\tif !slices.Contains(list2, item) {\n\t\t\tout = append(out, item)\n\t\t}\n\t}\n\n\treturn out\n}\n\n// ListContains returns true if the given list of strings (haystack) contains the given string (needle).\n//\n// Deprecated: Use slices.Contains instead.\nfunc ListContains(haystack []string, needle string) bool {\n\treturn slices.Contains(haystack, needle)\n}\n"
  },
  {
    "path": "modules/collections/lists_test.go",
    "content": "package collections_test\n\nimport (\n\t\"slices\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/collections\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestListContains(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tdescription string\n\t\telement     string\n\t\tlist        []string\n\t\texpected    bool\n\t}{\n\t\t{\"empty list, empty element\", \"\", []string{}, false},\n\t\t{\"empty list, non-empty element\", \"foo\", []string{}, false},\n\t\t{\"list with 1 item, element matches\", \"foo\", []string{\"foo\"}, true},\n\t\t{\"list with 1 item, element doesn't match\", \"foo\", []string{\"bar\"}, false},\n\t\t{\"list with 3 items, element matches\", \"foo\", []string{\"bar\", \"foo\", \"baz\"}, true},\n\t\t{\"list with 3 items, element doesn't match\", \"nope\", []string{\"bar\", \"foo\", \"baz\"}, false},\n\t\t{\"list with 3 items, empty element\", \"\", []string{\"bar\", \"foo\", \"baz\"}, false},\n\t}\n\n\tfor _, testCase := range testCases {\n\t\tt.Run(testCase.description, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tactual := slices.Contains(testCase.list, testCase.element)\n\t\t\tassert.Equal(t, testCase.expected, actual)\n\t\t})\n\t}\n}\n\nfunc TestSubtract(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tdescription string\n\t\tlist1       []string\n\t\tlist2       []string\n\t\texpected    []string\n\t}{\n\t\t{\"empty list, empty list\", []string{}, []string{}, []string{}},\n\t\t{\"empty list, non-empty list\", []string{}, []string{\"foo\"}, []string{}},\n\t\t{\"non-empty list, empty list\", []string{\"foo\"}, []string{}, []string{\"foo\"}},\n\t\t{\"list with 1 item, list with no matches\", []string{\"foo\"}, []string{\"bar\"}, []string{\"foo\"}},\n\t\t{\"list with 1 item, list with 1 match\", []string{\"foo\"}, []string{\"foo\"}, []string{}},\n\t\t{\"list with 1 item, list with multiple matches and non-matches\", []string{\"foo\"}, []string{\"foo\", \"bar\", \"foo\"}, []string{}},\n\t\t{\"list with multiple items, list with no matches\", []string{\"foo\", \"bar\", \"baz\"}, []string{\"abc\", \"def\"}, []string{\"foo\", \"bar\", \"baz\"}},\n\t\t{\"list with multiple items, list with 1 match\", []string{\"foo\", \"bar\", \"baz\"}, []string{\"abc\", \"foo\", \"def\"}, []string{\"bar\", \"baz\"}},\n\t\t{\"list with multiple items, list with multiple matches\", []string{\"foo\", \"bar\", \"baz\", \"foo\", \"bar\", \"baz\"}, []string{\"abc\", \"foo\", \"baz\"}, []string{\"bar\", \"bar\"}},\n\t}\n\n\tfor _, testCase := range testCases {\n\t\tt.Run(testCase.description, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tactual := collections.ListSubtract(testCase.list1, testCase.list2)\n\t\t\tassert.Equal(t, testCase.expected, actual)\n\t\t})\n\t}\n}\n\nfunc TestIntersection(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tdescription string\n\t\tlist1       []string\n\t\tlist2       []string\n\t\texpected    []string\n\t}{\n\t\t{\"empty list, empty list\", []string{}, []string{}, []string{}},\n\t\t{\"empty list, non-empty list\", []string{}, []string{\"foo\"}, []string{}},\n\t\t{\"non-empty list, empty list\", []string{\"foo\"}, []string{}, []string{}},\n\t\t{\"list with 1 item, list with no matches\", []string{\"foo\"}, []string{\"bar\"}, []string{}},\n\t\t{\"list with 1 item, list with 1 match\", []string{\"foo\"}, []string{\"foo\"}, []string{\"foo\"}},\n\t\t{\"list with 1 item, list with multiple matches and non-matches\", []string{\"foo\"}, []string{\"foo\", \"bar\", \"foo\"}, []string{\"foo\"}},\n\t\t{\"list with multiple items, list with no matches\", []string{\"foo\", \"bar\", \"baz\"}, []string{\"abc\", \"def\"}, []string{}},\n\t\t{\"list with multiple items, list with 1 match\", []string{\"foo\", \"bar\", \"baz\"}, []string{\"abc\", \"foo\", \"def\"}, []string{\"foo\"}},\n\t\t{\"list with multiple items, list with multiple matches\", []string{\"foo\", \"bar\", \"baz\", \"foo\", \"bar\", \"baz\"}, []string{\"abc\", \"foo\", \"baz\"}, []string{\"foo\", \"baz\"}},\n\t}\n\n\tfor _, testCase := range testCases {\n\t\tt.Run(testCase.description, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tactual := collections.ListIntersection(testCase.list1, testCase.list2)\n\t\t\tassert.Equal(t, testCase.expected, actual)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "modules/collections/stringslicevalue.go",
    "content": "package collections\n\nimport (\n\t\"strings\"\n)\n\n// GetSliceLastValueE will take a source string and returns the last value when split by the separator char.\nfunc GetSliceLastValueE(source string, separator string) (string, error) {\n\tif len(source) > 0 && len(separator) > 0 && strings.Contains(source, separator) {\n\t\ttmp := strings.Split(source, separator)\n\n\t\treturn tmp[len(tmp)-1], nil\n\t}\n\n\treturn \"\", NewSliceValueNotFoundError(source)\n}\n\n// GetSliceIndexValueE will take a source string and returns the value at the given index when split by\n// the separator char.\nfunc GetSliceIndexValueE(source string, separator string, index int) (string, error) {\n\tif len(source) > 0 && len(separator) > 0 && strings.Contains(source, separator) && index >= 0 {\n\t\ttmp := strings.Split(source, separator)\n\t\tif index >= len(tmp) {\n\t\t\treturn \"\", NewSliceValueNotFoundError(source)\n\t\t}\n\n\t\treturn tmp[index], nil\n\t}\n\n\treturn \"\", NewSliceValueNotFoundError(source)\n}\n"
  },
  {
    "path": "modules/collections/stringslicevalue_test.go",
    "content": "package collections_test\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/collections\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestGetSliceLastValue(t *testing.T) {\n\tt.Parallel()\n\n\tvar testCases = []struct {\n\t\ttestName       string\n\t\tsliceSource    string\n\t\tsliceSeperator string\n\t\texpectedReturn string\n\t\texpectedError  bool\n\t}{\n\t\t{testName: \"longSlice\", sliceSource: \"this/is/a/long/slash/separated/string/success\", sliceSeperator: \"/\", expectedReturn: \"success\", expectedError: false},\n\t\t{testName: \"shortendSlice\", sliceSource: \"this/is/a/long/slash/separated\", sliceSeperator: \"/\", expectedReturn: \"separated\", expectedError: false},\n\t\t{testName: \"dashSlice\", sliceSource: \"this-is-a-long-dash-separated-string-success\", sliceSeperator: \"-\", expectedReturn: \"success\", expectedError: false},\n\t\t{testName: \"seperatorNotPresent\", sliceSource: \"this-is-a-long-dash-separated-string-success\", sliceSeperator: \"/\", expectedReturn: \"\", expectedError: true},\n\t\t{testName: \"sourceNoSeperator\", sliceSource: \"noslicepresent\", sliceSeperator: \"/\", expectedReturn: \"\", expectedError: true},\n\t\t{testName: \"emptyStrings\", sliceSource: \"\", sliceSeperator: \"\", expectedReturn: \"\", expectedError: true},\n\t}\n\n\tfor _, tc := range testCases {\n\t\ttestFor := tc // necessary range capture\n\n\t\tt.Run(testFor.testName, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tactualReturn, err := collections.GetSliceLastValueE(testFor.sliceSource, testFor.sliceSeperator)\n\t\t\tswitch testFor.expectedError {\n\t\t\tcase true:\n\t\t\t\trequire.Error(t, err)\n\t\t\tcase false:\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\n\t\t\tassert.Equal(t, testFor.expectedReturn, actualReturn)\n\t\t})\n\t}\n}\n\nfunc TestGetSliceIndexValue(t *testing.T) {\n\tt.Parallel()\n\n\tvar testCases = []struct {\n\t\texpectedReturn string\n\t\tsliceIndex     int\n\t\texpectedError  bool\n\t}{\n\t\t{expectedReturn: \"\", sliceIndex: -1, expectedError: true},\n\t\t{expectedReturn: \"this\", sliceIndex: 0, expectedError: false},\n\t\t{expectedReturn: \"slash\", sliceIndex: 4, expectedError: false},\n\t\t{expectedReturn: \"success\", sliceIndex: 7, expectedError: false},\n\t\t{expectedReturn: \"\", sliceIndex: 10, expectedError: true},\n\t}\n\n\tsliceSource := \"this/is/a/long/slash/separated/string/success\"\n\tsliceSeperator := \"/\"\n\n\tfor _, tc := range testCases {\n\t\ttestFor := tc // necessary range capture\n\n\t\tt.Run(fmt.Sprintf(\"Index_%v\", testFor.sliceIndex), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tactualReturn, err := collections.GetSliceIndexValueE(sliceSource, sliceSeperator, testFor.sliceIndex)\n\t\t\tswitch testFor.expectedError {\n\t\t\tcase true:\n\t\t\t\trequire.Error(t, err)\n\t\t\tcase false:\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\n\t\t\tassert.Equal(t, testFor.expectedReturn, actualReturn)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "modules/database/database.go",
    "content": "package database\n\nimport (\n\t\"database/sql\"\n\t\"fmt\"\n\t\"testing\"\n\n\t// Microsoft SQL Database Driver\n\t_ \"github.com/denisenkom/go-mssqldb\"\n\n\t// PostgreSQL Database Driver\n\t_ \"github.com/lib/pq\"\n\n\t// MySQL Database Driver\n\t_ \"github.com/go-sql-driver/mysql\"\n)\n\nconst (\n\t_databaseTypeMSSQL    = \"mssql\"\n\t_databaseTypePostgres = \"postgres\"\n\t_databaseTypeMySQL    = \"mysql\"\n\t_postgresConnStr      = \"host=%s port=%s user=%s password=%s dbname=%s sslmode=disable\"\n\t_mssqlConnStr         = \"server = %s; port = %s; user id = %s; password = %s; database = %s\"\n\t_mysqlConnStr         = \"%s:%s@tcp(%s:%s)/%s?allowNativePasswords=true\"\n)\n\n// DBConfig using server name, user name, password and database name\ntype DBConfig struct {\n\tHost     string\n\tPort     string\n\tUser     string\n\tPassword string\n\tDatabase string\n}\n\n// DBConnection connects to the database using database configuration and database type, i.e. mssql, and then return the database. If there's any error, fail the test.\nfunc DBConnection(t *testing.T, dbType string, dbConfig DBConfig) *sql.DB {\n\tdb, err := DBConnectionE(t, dbType, dbConfig)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn db\n}\n\n// DBConnectionE connects to the database using database configuration and database type, i.e. mssql. Return the database or an error.\nfunc DBConnectionE(t *testing.T, dbType string, dbConfig DBConfig) (*sql.DB, error) {\n\tconfig := \"\"\n\tswitch dbType {\n\tcase _databaseTypeMSSQL:\n\t\tconfig = fmt.Sprintf(_mssqlConnStr, dbConfig.Host, dbConfig.Port, dbConfig.User, dbConfig.Password, dbConfig.Database)\n\tcase _databaseTypePostgres:\n\t\tconfig = fmt.Sprintf(_postgresConnStr, dbConfig.Host, dbConfig.Port, dbConfig.User, dbConfig.Password, dbConfig.Database)\n\tcase _databaseTypeMySQL:\n\t\tconfig = fmt.Sprintf(_mysqlConnStr, dbConfig.User, dbConfig.Password, dbConfig.Host, dbConfig.Port, dbConfig.Database)\n\tdefault:\n\t\treturn nil, DBUnknown{dbType: dbType}\n\t}\n\tdb, err := sql.Open(dbType, config)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\terr = db.Ping()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn db, nil\n}\n\n// DBExecution executes specific SQL commands, i.e. insertion. If there's any error, fail the test.\nfunc DBExecution(t *testing.T, db *sql.DB, command string) {\n\t_, err := DBExecutionE(t, db, command)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n\n// DBExecutionE executes specific SQL commands, i.e. insertion. Return the result or an error.\nfunc DBExecutionE(t *testing.T, db *sql.DB, command string) (sql.Result, error) {\n\tresult, err := db.Exec(command)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn result, nil\n}\n\n// DBQuery queries from database, i.e. selection, and then return the result. If there's any error, fail the test.\nfunc DBQuery(t *testing.T, db *sql.DB, command string) *sql.Rows {\n\trows, err := DBQueryE(t, db, command)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn rows\n}\n\n// DBQueryE queries from database, i.e. selection. Return the result or an error.\nfunc DBQueryE(t *testing.T, db *sql.DB, command string) (*sql.Rows, error) {\n\trows, err := db.Query(command)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn rows, nil\n}\n\n// DBQueryWithValidation queries from database and validate whether the result is the same as expected text. If there's any error, fail the test.\nfunc DBQueryWithValidation(t *testing.T, db *sql.DB, command string, expected string) {\n\terr := DBQueryWithValidationE(t, db, command, expected)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n\n// DBQueryWithValidationE queries from database and validate whether the result is the same as expected text. If not, return an error.\nfunc DBQueryWithValidationE(t *testing.T, db *sql.DB, command string, expected string) error {\n\treturn DBQueryWithCustomValidationE(t, db, command, func(rows *sql.Rows) bool {\n\t\tvar name string\n\t\tfor rows.Next() {\n\t\t\terr := rows.Scan(&name)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tif name != expected {\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t\treturn true\n\t})\n}\n\n// DBQueryWithCustomValidation queries from database and validate whether the result meets the requirement. If there's any error, fail the test.\nfunc DBQueryWithCustomValidation(t *testing.T, db *sql.DB, command string, validateResponse func(*sql.Rows) bool) {\n\terr := DBQueryWithCustomValidationE(t, db, command, validateResponse)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n\n// DBQueryWithCustomValidationE queries from database and validate whether the result meets the requirement. If not, return an error.\nfunc DBQueryWithCustomValidationE(t *testing.T, db *sql.DB, command string, validateResponse func(*sql.Rows) bool) error {\n\trows, err := DBQueryE(t, db, command)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer rows.Close()\n\tif !validateResponse(rows) {\n\t\treturn ValidationFunctionFailed{command: command}\n\t}\n\treturn nil\n}\n\n// ValidationFunctionFailed is an error that occurs if the validation function fails.\ntype ValidationFunctionFailed struct {\n\tcommand string\n}\n\nfunc (err ValidationFunctionFailed) Error() string {\n\treturn fmt.Sprintf(\"Validation failed for command: %s.\", err.command)\n}\n\n// DBUnknown is an error that occurs if the given database type is unknown or not supported.\ntype DBUnknown struct {\n\tdbType string\n}\n\nfunc (err DBUnknown) Error() string {\n\treturn fmt.Sprintf(\"Database unknown or not supported: %s. We only support mssql, postgres and mysql.\", err.dbType)\n}\n"
  },
  {
    "path": "modules/dns-helper/dns_helper.go",
    "content": "// Package dns_helper contains helpers to interact with the Domain Name System.\npackage dns_helper\n\nimport (\n\t\"fmt\"\n\t\"net\"\n\t\"reflect\"\n\t\"sort\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/gruntwork-io/terratest/modules/logger\"\n\t\"github.com/gruntwork-io/terratest/modules/retry\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/miekg/dns\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// DNSFindNameservers tries to find the NS record for the given FQDN, iterating down the domain hierarchy\n// until it founds the NS records and returns it. Fails if there's any error or no NS record is found up to the apex domain.\nfunc DNSFindNameservers(t testing.TestingT, fqdn string, resolvers []string) []string {\n\tnameservers, err := DNSFindNameserversE(t, fqdn, resolvers)\n\trequire.NoError(t, err)\n\treturn nameservers\n}\n\n// DNSFindNameserversE tries to find the NS record for the given FQDN, iterating down the domain hierarchy\n// until it founds the NS records and returns it. Returns the last error if the apex domain is reached with no result.\nfunc DNSFindNameserversE(t testing.TestingT, fqdn string, resolvers []string) ([]string, error) {\n\tvar lookupFunc func(domain string) ([]string, error)\n\n\tif resolvers == nil {\n\t\tlookupFunc = func(domain string) ([]string, error) {\n\t\t\tvar nameservers []string\n\t\t\tres, err := net.LookupNS(domain)\n\t\t\tfor _, ns := range res {\n\t\t\t\tnameservers = append(nameservers, ns.Host)\n\t\t\t}\n\t\t\treturn nameservers, err\n\t\t}\n\t} else {\n\t\tlookupFunc = func(domain string) ([]string, error) {\n\t\t\tvar nameservers []string\n\t\t\tres, err := DNSLookupE(t, DNSQuery{\"NS\", domain}, resolvers)\n\t\t\tfor _, r := range res {\n\t\t\t\tif r.Type == \"NS\" {\n\t\t\t\t\tnameservers = append(nameservers, r.Value)\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn nameservers, err\n\t\t}\n\t}\n\n\tparts := strings.Split(fqdn, \".\")\n\n\tvar domain string\n\tfor i := range parts[:len(parts)-1] {\n\t\tdomain = strings.Join(parts[i:], \".\")\n\t\tres, err := lookupFunc(domain)\n\n\t\tif len(res) > 0 {\n\t\t\tvar nameservers []string\n\n\t\t\tfor _, ns := range res {\n\t\t\t\tnameservers = append(nameservers, strings.TrimSuffix(ns, \".\"))\n\t\t\t}\n\n\t\t\tlogger.Default.Logf(t, \"FQDN %s belongs to domain %s, found NS record: %s\", fqdn, domain, nameservers)\n\t\t\treturn nameservers, nil\n\t\t}\n\n\t\tif err != nil {\n\t\t\tlogger.Default.Logf(t, \"%s\", err.Error())\n\t\t}\n\t}\n\n\terr := &NSNotFoundError{fqdn, domain}\n\treturn nil, err\n}\n\n// DNSLookupAuthoritative gets authoritative answers for the specified record and type.\n// If resolvers are defined, uses them instead of the default system ones to find the authoritative nameservers.\n// Fails on any error from DNSLookupAuthoritativeE.\nfunc DNSLookupAuthoritative(t testing.TestingT, query DNSQuery, resolvers []string) DNSAnswers {\n\tres, err := DNSLookupAuthoritativeE(t, query, resolvers)\n\trequire.NoError(t, err)\n\treturn res\n}\n\n// DNSLookupAuthoritativeE gets authoritative answers for the specified record and type.\n// If resolvers are defined, uses them instead of the default system ones to find the authoritative nameservers.\n// Returns NotFoundError when no answer found in any authoritative nameserver.\n// Returns any underlying error from individual lookups.\nfunc DNSLookupAuthoritativeE(t testing.TestingT, query DNSQuery, resolvers []string) (DNSAnswers, error) {\n\tnameservers, err := DNSFindNameserversE(t, query.Name, resolvers)\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn DNSLookupE(t, query, nameservers)\n}\n\n// DNSLookupAuthoritativeWithRetry repeatedly gets authoritative answers for the specified record and type\n// until ANY of the authoritative nameservers found replies with non-empty answer matching the expectedAnswers,\n// or until max retries has been exceeded.\n// If resolvers are defined, uses them instead of the default system ones to find the authoritative nameservers.\n// Fails on any error from DNSLookupAuthoritativeWithRetryE.\nfunc DNSLookupAuthoritativeWithRetry(t testing.TestingT, query DNSQuery, resolvers []string, maxRetries int, sleepBetweenRetries time.Duration) DNSAnswers {\n\tres, err := DNSLookupAuthoritativeWithRetryE(t, query, resolvers, maxRetries, sleepBetweenRetries)\n\trequire.NoError(t, err)\n\treturn res\n}\n\n// DNSLookupAuthoritativeWithRetryE repeatedly gets authoritative answers for the specified record and type\n// until ANY of the authoritative nameservers found replies with non-empty answer matching the expectedAnswers,\n// or until max retries has been exceeded.\n// If resolvers are defined, uses them instead of the default system ones to find the authoritative nameservers.\nfunc DNSLookupAuthoritativeWithRetryE(t testing.TestingT, query DNSQuery, resolvers []string, maxRetries int, sleepBetweenRetries time.Duration) (DNSAnswers, error) {\n\tres, err := retry.DoWithRetryInterfaceE(\n\t\tt, fmt.Sprintf(\"DNSLookupAuthoritativeE %s record for %s using authoritative nameservers\", query.Type, query.Name),\n\t\tmaxRetries, sleepBetweenRetries,\n\t\tfunc() (interface{}, error) {\n\t\t\treturn DNSLookupAuthoritativeE(t, query, resolvers)\n\t\t})\n\n\treturn res.(DNSAnswers), err\n}\n\n// DNSLookupAuthoritativeAll gets authoritative answers for the specified record and type.\n// All the authoritative nameservers found must give the same answers.\n// If resolvers are defined, uses them instead of the default system ones to find the authoritative nameservers.\n// Fails on any error from DNSLookupAuthoritativeAllE.\nfunc DNSLookupAuthoritativeAll(t testing.TestingT, query DNSQuery, resolvers []string) DNSAnswers {\n\tres, err := DNSLookupAuthoritativeAllE(t, query, resolvers)\n\trequire.NoError(t, err)\n\treturn res\n}\n\n// DNSLookupAuthoritativeAllE gets authoritative answers for the specified record and type.\n// All the authoritative nameservers found must give the same answers.\n// If resolvers are defined, uses them instead of the default system ones to find the authoritative nameservers.\n// Returns InconsistentAuthoritativeError when any authoritative nameserver gives a different answer.\n// Returns any underlying error.\nfunc DNSLookupAuthoritativeAllE(t testing.TestingT, query DNSQuery, resolvers []string) (DNSAnswers, error) {\n\tnameservers, err := DNSFindNameserversE(t, query.Name, resolvers)\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar answers DNSAnswers\n\n\tfor _, ns := range nameservers {\n\t\tres, err := DNSLookupE(t, query, []string{ns})\n\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tif len(answers) > 0 {\n\t\t\tif !reflect.DeepEqual(answers, res) {\n\t\t\t\terr := &InconsistentAuthoritativeError{Query: query, Answers: res, Nameserver: ns, PreviousAnswers: answers}\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t} else {\n\t\t\tanswers = res\n\t\t}\n\t}\n\n\treturn answers, nil\n}\n\n// DNSLookupAuthoritativeAllWithRetry repeatedly sends DNS requests for the specified record and type,\n// until ALL authoritative nameservers reply with the exact same non-empty answers or until max retries has been exceeded.\n// If defined, uses the given resolvers instead of the default system ones to find the authoritative nameservers.\n// Fails when max retries has been exceeded.\nfunc DNSLookupAuthoritativeAllWithRetry(t testing.TestingT, query DNSQuery, resolvers []string, maxRetries int, sleepBetweenRetries time.Duration) {\n\t_, err := DNSLookupAuthoritativeAllWithRetryE(t, query, resolvers, maxRetries, sleepBetweenRetries)\n\trequire.NoError(t, err)\n}\n\n// DNSLookupAuthoritativeAllWithRetryE repeatedly sends DNS requests for the specified record and type,\n// until ALL authoritative nameservers reply with the exact same non-empty answers or until max retries has been exceeded.\n// If defined, uses the given resolvers instead of the default system ones to find the authoritative nameservers.\nfunc DNSLookupAuthoritativeAllWithRetryE(t testing.TestingT, query DNSQuery, resolvers []string, maxRetries int, sleepBetweenRetries time.Duration) (DNSAnswers, error) {\n\tres, err := retry.DoWithRetryInterfaceE(\n\t\tt, fmt.Sprintf(\"DNSLookupAuthoritativeAllE %s record for %s using authoritative nameservers\", query.Type, query.Name),\n\t\tmaxRetries, sleepBetweenRetries,\n\t\tfunc() (interface{}, error) {\n\t\t\treturn DNSLookupAuthoritativeAllE(t, query, resolvers)\n\t\t})\n\n\treturn res.(DNSAnswers), err\n}\n\n// DNSLookupAuthoritativeAllWithValidation gets authoritative answers for the specified record and type.\n// All the authoritative nameservers found must give the same answers and match the expectedAnswers.\n// If resolvers are defined, uses them instead of the default system ones to find the authoritative nameservers.\n// Fails on any underlying error from DNSLookupAuthoritativeAllWithValidationE.\nfunc DNSLookupAuthoritativeAllWithValidation(t testing.TestingT, query DNSQuery, resolvers []string, expectedAnswers DNSAnswers) {\n\terr := DNSLookupAuthoritativeAllWithValidationE(t, query, resolvers, expectedAnswers)\n\trequire.NoError(t, err)\n}\n\n// DNSLookupAuthoritativeAllWithValidationE gets authoritative answers for the specified record and type.\n// All the authoritative nameservers found must give the same answers and match the expectedAnswers.\n// If resolvers are defined, uses them instead of the default system ones to find the authoritative nameservers.\n// Returns ValidationError when expectedAnswers differ from the obtained ones.\n// Returns any underlying error from DNSLookupAuthoritativeAllE.\nfunc DNSLookupAuthoritativeAllWithValidationE(t testing.TestingT, query DNSQuery, resolvers []string, expectedAnswers DNSAnswers) error {\n\texpectedAnswers.Sort()\n\n\tanswers, err := DNSLookupAuthoritativeAllE(t, query, resolvers)\n\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif !reflect.DeepEqual(answers, expectedAnswers) {\n\t\terr := &ValidationError{Query: query, Answers: answers, ExpectedAnswers: expectedAnswers}\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// DNSLookupAuthoritativeAllWithValidationRetry repeatedly gets authoritative answers for the specified record and type\n// until ALL the authoritative nameservers found give the same answers and match the expectedAnswers,\n// or until max retries has been exceeded.\n// If resolvers are defined, uses them instead of the default system ones to find the authoritative nameservers.\n// Fails when max retries has been exceeded.\nfunc DNSLookupAuthoritativeAllWithValidationRetry(t testing.TestingT, query DNSQuery, resolvers []string, expectedAnswers DNSAnswers, maxRetries int, sleepBetweenRetries time.Duration) {\n\terr := DNSLookupAuthoritativeAllWithValidationRetryE(t, query, resolvers, expectedAnswers, maxRetries, sleepBetweenRetries)\n\trequire.NoError(t, err)\n}\n\n// DNSLookupAuthoritativeAllWithValidationRetryE repeatedly gets authoritative answers for the specified record and type\n// until ALL the authoritative nameservers found give the same answers and match the expectedAnswers,\n// or until max retries has been exceeded.\n// If resolvers are defined, uses them instead of the default system ones to find the authoritative nameservers.\nfunc DNSLookupAuthoritativeAllWithValidationRetryE(t testing.TestingT, query DNSQuery, resolvers []string, expectedAnswers DNSAnswers, maxRetries int, sleepBetweenRetries time.Duration) error {\n\t_, err := retry.DoWithRetryInterfaceE(\n\t\tt, fmt.Sprintf(\"DNSLookupAuthoritativeAllWithValidationRetryE %s record for %s using authoritative nameservers\", query.Type, query.Name),\n\t\tmaxRetries, sleepBetweenRetries,\n\t\tfunc() (interface{}, error) {\n\t\t\treturn nil, DNSLookupAuthoritativeAllWithValidationE(t, query, resolvers, expectedAnswers)\n\t\t})\n\n\treturn err\n}\n\n// DNSLookup sends a DNS query for the specified record and type using the given resolvers.\n// Fails on any error.\n// Supported record types: A, AAAA, CNAME, MX, NS, TXT\nfunc DNSLookup(t testing.TestingT, query DNSQuery, resolvers []string) DNSAnswers {\n\tres, err := DNSLookupE(t, query, resolvers)\n\trequire.NoError(t, err)\n\treturn res\n}\n\n// DNSLookupE sends a DNS query for the specified record and type using the given resolvers.\n// Returns QueryTypeError when record type is not supported.\n// Returns any underlying error.\n// Supported record types: A, AAAA, CNAME, MX, NS, TXT\nfunc DNSLookupE(t testing.TestingT, query DNSQuery, resolvers []string) (DNSAnswers, error) {\n\tif len(resolvers) == 0 {\n\t\terr := &NoResolversError{}\n\t\treturn nil, err\n\t}\n\n\tvar dnsAnswers DNSAnswers\n\tvar err error\n\tfor _, resolver := range resolvers {\n\t\tdnsAnswers, err = dnsLookup(t, query, resolver)\n\n\t\tif err == nil {\n\t\t\treturn dnsAnswers, nil\n\t\t}\n\t}\n\n\treturn nil, err\n}\n\n// dnsLookup sends a DNS query for the specified record and type using the given resolver.\n// Returns DNSAnswers to the DNSQuery.\n// If no records found, returns NotFoundError.\nfunc dnsLookup(t testing.TestingT, query DNSQuery, resolver string) (DNSAnswers, error) {\n\tswitch query.Type {\n\tcase \"A\", \"AAAA\", \"CNAME\", \"MX\", \"NS\", \"TXT\":\n\tdefault:\n\t\terr := &QueryTypeError{query.Type}\n\t\treturn nil, err\n\t}\n\n\tqType, ok := dns.StringToType[strings.ToUpper(query.Type)]\n\tif !ok {\n\t\terr := &QueryTypeError{query.Type}\n\t\treturn nil, err\n\t}\n\n\tif strings.LastIndex(resolver, \":\") <= strings.LastIndex(resolver, \"]\") {\n\t\tresolver += \":53\"\n\t}\n\n\tc := new(dns.Client)\n\tm := new(dns.Msg)\n\tm.SetQuestion(dns.Fqdn(query.Name), qType)\n\n\tin, _, err := c.Exchange(m, resolver)\n\tif err != nil {\n\t\tlogger.Default.Logf(t, \"Error sending DNS query %s: %s\", query, err)\n\t\treturn nil, err\n\t}\n\n\tif len(in.Answer) == 0 {\n\t\terr := &NotFoundError{query, resolver}\n\t\treturn nil, err\n\t}\n\n\tvar dnsAnswers DNSAnswers\n\n\tfor _, a := range in.Answer {\n\t\tswitch at := a.(type) {\n\t\tcase *dns.A:\n\t\t\tdnsAnswers = append(dnsAnswers, DNSAnswer{\"A\", at.A.String()})\n\t\tcase *dns.AAAA:\n\t\t\tdnsAnswers = append(dnsAnswers, DNSAnswer{\"AAAA\", at.AAAA.String()})\n\t\tcase *dns.CNAME:\n\t\t\tdnsAnswers = append(dnsAnswers, DNSAnswer{\"CNAME\", at.Target})\n\t\tcase *dns.NS:\n\t\t\tdnsAnswers = append(dnsAnswers, DNSAnswer{\"NS\", at.Ns})\n\t\tcase *dns.MX:\n\t\t\tdnsAnswers = append(dnsAnswers, DNSAnswer{\"MX\", fmt.Sprintf(\"%d %s\", at.Preference, at.Mx)})\n\t\tcase *dns.TXT:\n\t\t\tfor _, txt := range at.Txt {\n\t\t\t\tdnsAnswers = append(dnsAnswers, DNSAnswer{\"TXT\", fmt.Sprintf(`\"%s\"`, txt)})\n\t\t\t}\n\t\t}\n\t}\n\n\tdnsAnswers.Sort()\n\n\treturn dnsAnswers, nil\n}\n\n// DNSQuery type\ntype DNSQuery struct {\n\tType, Name string\n}\n\n// DNSAnswer type\ntype DNSAnswer struct {\n\tType, Value string\n}\n\nfunc (a DNSAnswer) String() string {\n\treturn fmt.Sprintf(\"%s %s\", a.Type, a.Value)\n}\n\n// DNSAnswers type\ntype DNSAnswers []DNSAnswer\n\n// Sort sorts the answers by type and value\nfunc (a DNSAnswers) Sort() {\n\tsort.Slice(a, func(i, j int) bool {\n\t\treturn a[i].Type < a[j].Type || a[i].Value < a[j].Value\n\t})\n}\n"
  },
  {
    "path": "modules/dns-helper/dns_helper_test.go",
    "content": "package dns_helper\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/gruntwork-io/terratest/modules/retry\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// These are the current public nameservers for gruntwork.io domain\n// They should be updated whenever they change to pass the tests\n// relying on the public DNS infrastructure\nvar publicDomainNameservers = []string{\n\t\"ns-1499.awsdns-59.org\",\n\t\"ns-190.awsdns-23.com\",\n\t\"ns-1989.awsdns-56.co.uk\",\n\t\"ns-853.awsdns-42.net\",\n}\n\nvar testDNSDatabase = dnsDatabase{\n\tDNSQuery{\"A\", \"a.\" + testDomain}: DNSAnswers{\n\t\t{\"A\", \"2.2.2.2\"},\n\t\t{\"A\", \"1.1.1.1\"},\n\t},\n\n\tDNSQuery{\"AAAA\", \"aaaa.\" + testDomain}: DNSAnswers{\n\t\t{\"AAAA\", \"2001:db8::aaaa\"},\n\t},\n\n\tDNSQuery{\"CNAME\", \"terratest.\" + testDomain}: DNSAnswers{\n\t\t{\"CNAME\", \"gruntwork-io.github.io.\"},\n\t},\n\n\tDNSQuery{\"CNAME\", \"cname1.\" + testDomain}: DNSAnswers{\n\t\t{\"CNAME\", \"cname2.\" + testDomain + \".\"},\n\t},\n\n\tDNSQuery{\"A\", \"cname1.\" + testDomain}: DNSAnswers{\n\t\t{\"CNAME\", \"cname2.\" + testDomain + \".\"},\n\t\t{\"CNAME\", \"cname3.\" + testDomain + \".\"},\n\t\t{\"CNAME\", \"cname4.\" + testDomain + \".\"},\n\t\t{\"CNAME\", \"cnamefinal.\" + testDomain + \".\"},\n\t\t{\"A\", \"1.1.1.1\"},\n\t},\n\n\tDNSQuery{\"TXT\", \"txt.\" + testDomain}: DNSAnswers{\n\t\t{\"TXT\", `\"This is a text.\"`},\n\t},\n\n\tDNSQuery{\"MX\", testDomain}: DNSAnswers{\n\t\t{\"MX\", \"10 mail.\" + testDomain + \".\"},\n\t},\n}\n\n// Lookup should succeed in finding the nameservers of the public domain\n// Uses system resolver config\nfunc TestOkDNSFindNameservers(t *testing.T) {\n\tt.Parallel()\n\tfqdn := \"terratest.gruntwork.io\"\n\texpectedNameservers := publicDomainNameservers\n\tnameservers, err := DNSFindNameserversE(t, fqdn, nil)\n\trequire.NoError(t, err)\n\trequire.ElementsMatch(t, nameservers, expectedNameservers)\n}\n\n// Lookup should fail because of inexistent domain\n// Uses system resolver config\nfunc TestErrorDNSFindNameservers(t *testing.T) {\n\tt.Parallel()\n\tfqdn := \"this.domain.doesnt.exist\"\n\tnameservers, err := DNSFindNameserversE(t, fqdn, nil)\n\trequire.Error(t, err)\n\trequire.Nil(t, nameservers)\n}\n\n// Lookup should succeed with answers from just one authoritative nameserver\n// Uses system resolver config to lookup a public domain and its public nameservers\nfunc TestOkTerratestDNSLookupAuthoritative(t *testing.T) {\n\tt.Parallel()\n\tdnsQuery := DNSQuery{\"CNAME\", \"terratest.\" + testDomain}\n\texpected := DNSAnswers{{\"CNAME\", \"gruntwork-io.github.io.\"}}\n\tres, err := DNSLookupAuthoritativeE(t, dnsQuery, nil)\n\trequire.NoError(t, err)\n\trequire.ElementsMatch(t, res, expected)\n}\n\n// ***********************************\n// Tests that use local dnsTestServers\n\n// Lookup should succeed with answers from just one authoritative nameserver\nfunc TestOkLocalDNSLookupAuthoritative(t *testing.T) {\n\tt.Parallel()\n\ts1, s2 := setupTestDNSServers(t)\n\tdefer shutDownServers(t, s1, s2)\n\tfor dnsQuery, expected := range testDNSDatabase {\n\t\ts1.AddEntryToDNSDatabase(dnsQuery, expected)\n\t\tres, err := DNSLookupAuthoritativeE(t, dnsQuery, []string{s1.Address(), s2.Address()})\n\t\trequire.NoError(t, err)\n\t\trequire.ElementsMatch(t, res, expected)\n\t}\n}\n\n// Lookup should fail because of missing answers from all authoritative nameservers\nfunc TestErrorLocalDNSLookupAuthoritative(t *testing.T) {\n\tt.Parallel()\n\ts1, s2 := setupTestDNSServers(t)\n\tdefer shutDownServers(t, s1, s2)\n\tdnsQuery := DNSQuery{\"A\", \"txt.\" + testDomain}\n\t_, err := DNSLookupAuthoritativeE(t, dnsQuery, []string{s1.Address(), s2.Address()})\n\tif _, ok := err.(*NotFoundError); !ok {\n\t\tt.Errorf(\"unexpected error, got %q\", err)\n\t}\n}\n\n// Lookup should succeed with consistent answers from all authoritative nameservers\nfunc TestOkLocalDNSLookupAuthoritativeAll(t *testing.T) {\n\tt.Parallel()\n\ts1, s2 := setupTestDNSServers(t)\n\tdefer shutDownServers(t, s1, s2)\n\tfor dnsQuery, expected := range testDNSDatabase {\n\t\ts1.AddEntryToDNSDatabase(dnsQuery, expected)\n\t\ts2.AddEntryToDNSDatabase(dnsQuery, expected)\n\t\tres, err := DNSLookupAuthoritativeE(t, dnsQuery, []string{s1.Address(), s2.Address()})\n\t\trequire.NoError(t, err)\n\t\trequire.ElementsMatch(t, res, expected)\n\t}\n}\n\n// Lookup should fail because of missing answers from all authoritative nameservers\nfunc TestError1DNSLookupAuthoritativeAll(t *testing.T) {\n\tt.Parallel()\n\ts1, s2 := setupTestDNSServers(t)\n\tdefer shutDownServers(t, s1, s2)\n\tdnsQuery := DNSQuery{\"A\", \"txt.\" + testDomain}\n\t_, err := DNSLookupAuthoritativeAllE(t, dnsQuery, []string{s1.Address(), s2.Address()})\n\tif _, ok := err.(*NotFoundError); !ok {\n\t\tt.Errorf(\"unexpected error, got %q\", err)\n\t}\n}\n\n// Lookup should fail because of missing answers from one authoritative nameserver\nfunc TestError2DNSLookupAuthoritativeAll(t *testing.T) {\n\tt.Parallel()\n\ts1, s2 := setupTestDNSServers(t)\n\tdefer shutDownServers(t, s1, s2)\n\tdnsQuery := DNSQuery{\"A\", \"a.\" + testDomain}\n\ts1.AddEntryToDNSDatabase(dnsQuery, DNSAnswers{{\"A\", \"1.1.1.1\"}})\n\t_, err := DNSLookupAuthoritativeAllE(t, dnsQuery, []string{s1.Address(), s2.Address()})\n\tif _, ok := err.(*NotFoundError); !ok {\n\t\tt.Errorf(\"unexpected error, got %q\", err)\n\t}\n}\n\n// Lookup should fail because of inconsistent answers from authoritative nameservers\nfunc TestError3DNSLookupAuthoritativeAll(t *testing.T) {\n\tt.Parallel()\n\ts1, s2 := setupTestDNSServers(t)\n\tdefer shutDownServers(t, s1, s2)\n\tdnsQuery := DNSQuery{\"A\", \"a.\" + testDomain}\n\ts1.AddEntryToDNSDatabase(dnsQuery, DNSAnswers{{\"A\", \"1.1.1.1\"}})\n\ts2.AddEntryToDNSDatabase(dnsQuery, DNSAnswers{{\"A\", \"2.2.2.2\"}})\n\t_, err := DNSLookupAuthoritativeAllE(t, dnsQuery, []string{s1.Address(), s2.Address()})\n\tif _, ok := err.(*InconsistentAuthoritativeError); !ok {\n\t\tt.Errorf(\"unexpected error, got %q\", err)\n\t}\n}\n\n// Lookup should fail because of inexistent domain\nfunc TestError4DNSLookupAuthoritativeAll(t *testing.T) {\n\tt.Parallel()\n\ts1, s2 := setupTestDNSServers(t)\n\tdefer shutDownServers(t, s1, s2)\n\tdnsQuery := DNSQuery{\"A\", \"this.domain.doesnt.exist\"}\n\t_, err := DNSLookupAuthoritativeAllE(t, dnsQuery, []string{s1.Address(), s2.Address()})\n\tif _, ok := err.(*NSNotFoundError); !ok {\n\t\tt.Errorf(\"unexpected error, got %q\", err)\n\t}\n}\n\n// First lookups should fail because of missing answers from all authoritative nameservers\n// Retry lookups should succeed with answers from just one authoritative nameserver\nfunc TestOkDNSLookupAuthoritativeWithRetry(t *testing.T) {\n\tt.Parallel()\n\ts1, s2 := setupTestDNSServersRetry(t)\n\tdefer shutDownServers(t, s1, s2)\n\tdnsQuery := DNSQuery{\"A\", \"a.\" + testDomain}\n\texpectedRes := DNSAnswers{{\"A\", \"1.1.1.1\"}, {\"A\", \"2.2.2.2\"}}\n\ts1.AddEntryToDNSDatabaseRetry(dnsQuery, expectedRes)\n\tres, err := DNSLookupAuthoritativeWithRetryE(t, dnsQuery, []string{s1.Address(), s2.Address()}, 5, time.Second)\n\trequire.NoError(t, err)\n\trequire.ElementsMatch(t, res, expectedRes)\n}\n\n// First lookups should fail because of missing answers from all authoritative nameservers\n// Retry lookups should fail because of missing answers from all authoritative nameservers\nfunc TestErrorDNSLookupAuthoritativeWithRetry(t *testing.T) {\n\tt.Parallel()\n\ts1, s2 := setupTestDNSServersRetry(t)\n\tdefer shutDownServers(t, s1, s2)\n\tdnsQuery := DNSQuery{\"A\", \"txt.\" + testDomain}\n\t_, err := DNSLookupAuthoritativeWithRetryE(t, dnsQuery, []string{s1.Address(), s2.Address()}, 5, time.Second)\n\trequire.Error(t, err)\n\tif _, ok := err.(retry.MaxRetriesExceeded); !ok {\n\t\tt.Errorf(\"unexpected error, got %q\", err)\n\t}\n}\n\n// First lookups should fail because of missing answers from one authoritative nameservers\n// Retry lookups should succeed with consistent answers\nfunc TestOkDNSLookupAuthoritativeAllWithRetryNotfound(t *testing.T) {\n\tt.Parallel()\n\ts1, s2 := setupTestDNSServersRetry(t)\n\tdefer shutDownServers(t, s1, s2)\n\tdnsQuery := DNSQuery{\"A\", \"a.\" + testDomain}\n\texpectedRes := DNSAnswers{{\"A\", \"1.1.1.1\"}, {\"A\", \"2.2.2.2\"}}\n\ts1.AddEntryToDNSDatabase(dnsQuery, expectedRes)\n\ts1.AddEntryToDNSDatabaseRetry(dnsQuery, expectedRes)\n\ts2.AddEntryToDNSDatabaseRetry(dnsQuery, expectedRes)\n\tres, err := DNSLookupAuthoritativeAllWithRetryE(t, dnsQuery, []string{s1.Address(), s2.Address()}, 5, time.Second)\n\trequire.NoError(t, err)\n\trequire.ElementsMatch(t, res, expectedRes)\n}\n\n// First lookups should fail because of inconsistent answers from authoritative nameservers\n// Retry lookups should succeed with consistent answers\nfunc TestOkDNSLookupAuthoritativeAllWithRetryInconsistent(t *testing.T) {\n\tt.Parallel()\n\ts1, s2 := setupTestDNSServersRetry(t)\n\tdefer shutDownServers(t, s1, s2)\n\tdnsQuery := DNSQuery{\"A\", \"a.\" + testDomain}\n\texpectedRes := DNSAnswers{{\"A\", \"1.1.1.1\"}, {\"A\", \"2.2.2.2\"}}\n\ts1.AddEntryToDNSDatabase(dnsQuery, expectedRes)\n\ts2.AddEntryToDNSDatabase(dnsQuery, DNSAnswers{{\"A\", \"2.2.2.2\"}})\n\ts1.AddEntryToDNSDatabaseRetry(dnsQuery, expectedRes)\n\ts2.AddEntryToDNSDatabaseRetry(dnsQuery, expectedRes)\n\tres, err := DNSLookupAuthoritativeAllWithRetryE(t, dnsQuery, []string{s1.Address(), s2.Address()}, 5, time.Second)\n\trequire.NoError(t, err)\n\trequire.ElementsMatch(t, res, expectedRes)\n}\n\n// First lookups should fail because of missing answer from one authoritative nameserver\n// Retry lookups should fail because of inconsistent answers from authoritative nameservers\nfunc TestErrorDNSLookupAuthoritativeAllWithRetry(t *testing.T) {\n\tt.Parallel()\n\ts1, s2 := setupTestDNSServersRetry(t)\n\tdefer shutDownServers(t, s1, s2)\n\tdnsQuery := DNSQuery{\"A\", \"a.\" + testDomain}\n\ts1.AddEntryToDNSDatabase(dnsQuery, DNSAnswers{{\"A\", \"2.2.2.2\"}})\n\ts1.AddEntryToDNSDatabaseRetry(dnsQuery, DNSAnswers{{\"A\", \"1.1.1.1\"}, {\"A\", \"2.2.2.2\"}})\n\ts2.AddEntryToDNSDatabaseRetry(dnsQuery, DNSAnswers{{\"A\", \"1.1.1.1\"}})\n\t_, err := DNSLookupAuthoritativeAllWithRetryE(t, dnsQuery, []string{s1.Address(), s2.Address()}, 5, time.Second)\n\trequire.Error(t, err)\n\tif _, ok := err.(retry.MaxRetriesExceeded); !ok {\n\t\tt.Errorf(\"unexpected error, got %q\", err)\n\t}\n}\n\n// Lookup should succeed with consistent and validated replies\nfunc TestOkDNSLookupAuthoritativeAllWithValidation(t *testing.T) {\n\tt.Parallel()\n\ts1, s2 := setupTestDNSServers(t)\n\tdefer shutDownServers(t, s1, s2)\n\tdnsQuery := DNSQuery{\"A\", \"a.\" + testDomain}\n\texpectedRes := DNSAnswers{{\"A\", \"1.1.1.1\"}, {\"A\", \"2.2.2.2\"}}\n\ts1.AddEntryToDNSDatabase(dnsQuery, expectedRes)\n\ts2.AddEntryToDNSDatabase(dnsQuery, expectedRes)\n\terr := DNSLookupAuthoritativeAllWithValidationE(t, dnsQuery, []string{s1.Address(), s2.Address()}, expectedRes)\n\trequire.NoError(t, err)\n}\n\n// Lookup should fail because of missing answers from all authoritative nameservers\nfunc TestErrorDNSLookupAuthoritativeAllWithValidation(t *testing.T) {\n\tt.Parallel()\n\ts1, s2 := setupTestDNSServers(t)\n\tdefer shutDownServers(t, s1, s2)\n\tdnsQuery := DNSQuery{\"A\", \"a.\" + testDomain}\n\texpectedRes := DNSAnswers{{\"A\", \"1.1.1.1\"}, {\"A\", \"2.2.2.2\"}}\n\terr := DNSLookupAuthoritativeAllWithValidationE(t, dnsQuery, []string{s1.Address(), s2.Address()}, expectedRes)\n\trequire.Error(t, err)\n\tif _, ok := err.(*NotFoundError); !ok {\n\t\tt.Errorf(\"unexpected error, got %q\", err)\n\t}\n}\n\n// Lookup should fail because of missing answers from one authoritative nameservers\nfunc TestError2DNSLookupAuthoritativeAllWithValidation(t *testing.T) {\n\tt.Parallel()\n\ts1, s2 := setupTestDNSServers(t)\n\tdefer shutDownServers(t, s1, s2)\n\tdnsQuery := DNSQuery{\"A\", \"a.\" + testDomain}\n\texpectedRes := DNSAnswers{{\"A\", \"1.1.1.1\"}, {\"A\", \"2.2.2.2\"}}\n\ts1.AddEntryToDNSDatabase(dnsQuery, expectedRes)\n\terr := DNSLookupAuthoritativeAllWithValidationE(t, dnsQuery, []string{s1.Address(), s2.Address()}, expectedRes)\n\trequire.Error(t, err)\n\tif _, ok := err.(*NotFoundError); !ok {\n\t\tt.Errorf(\"unexpected error, got %q\", err)\n\t}\n}\n\n// Lookup should fail because of inconsistent authoritative replies\nfunc TestError3DNSLookupAuthoritativeAllWithValidation(t *testing.T) {\n\tt.Parallel()\n\ts1, s2 := setupTestDNSServers(t)\n\tdefer shutDownServers(t, s1, s2)\n\tdnsQuery := DNSQuery{\"A\", \"a.\" + testDomain}\n\texpectedRes := DNSAnswers{{\"A\", \"1.1.1.1\"}, {\"A\", \"2.2.2.2\"}}\n\ts1.AddEntryToDNSDatabase(dnsQuery, expectedRes)\n\ts2.AddEntryToDNSDatabase(dnsQuery, DNSAnswers{{\"A\", \"2.2.2.2\"}})\n\terr := DNSLookupAuthoritativeAllWithValidationE(t, dnsQuery, []string{s1.Address(), s2.Address()}, expectedRes)\n\trequire.Error(t, err)\n\tif _, ok := err.(*InconsistentAuthoritativeError); !ok {\n\t\tt.Errorf(\"unexpected error, got %q\", err)\n\t}\n}\n\n// First lookups should fail because of missing answers from all authoritative nameservers\n// Retry lookups should succeed with consistent and validated replies\nfunc TestOkDNSLookupAuthoritativeAllWithValidationRetry(t *testing.T) {\n\tt.Parallel()\n\ts1, s2 := setupTestDNSServersRetry(t)\n\tdefer shutDownServers(t, s1, s2)\n\tdnsQuery := DNSQuery{\"A\", \"a.\" + testDomain}\n\texpectedRes := DNSAnswers{{\"A\", \"1.1.1.1\"}, {\"A\", \"2.2.2.2\"}}\n\ts1.AddEntryToDNSDatabaseRetry(dnsQuery, expectedRes)\n\ts2.AddEntryToDNSDatabaseRetry(dnsQuery, expectedRes)\n\terr := DNSLookupAuthoritativeAllWithValidationRetryE(t, dnsQuery, []string{s1.Address(), s2.Address()}, expectedRes, 5, time.Second)\n\trequire.NoError(t, err)\n}\n\n// First lookups should fail because of missing answer from one authoritative nameserver\n// Retry lookups should succeed with consistent and validated replies\nfunc TestOk2DNSLookupAuthoritativeAllWithValidationRetry(t *testing.T) {\n\tt.Parallel()\n\ts1, s2 := setupTestDNSServersRetry(t)\n\tdefer shutDownServers(t, s1, s2)\n\tdnsQuery := DNSQuery{\"A\", \"a.\" + testDomain}\n\texpectedRes := DNSAnswers{{\"A\", \"1.1.1.1\"}, {\"A\", \"2.2.2.2\"}}\n\ts1.AddEntryToDNSDatabase(dnsQuery, DNSAnswers{{\"A\", \"2.2.2.2\"}})\n\ts1.AddEntryToDNSDatabaseRetry(dnsQuery, expectedRes)\n\ts2.AddEntryToDNSDatabaseRetry(dnsQuery, expectedRes)\n\terr := DNSLookupAuthoritativeAllWithValidationRetryE(t, dnsQuery, []string{s1.Address(), s2.Address()}, expectedRes, 5, time.Second)\n\trequire.NoError(t, err)\n}\n\n// First lookups should fail because of inconsistent authoritative replies\n// Retry lookups should succeed with consistent and validated replies\nfunc TestOk3DNSLookupAuthoritativeAllWithValidationRetry(t *testing.T) {\n\tt.Parallel()\n\ts1, s2 := setupTestDNSServersRetry(t)\n\tdefer shutDownServers(t, s1, s2)\n\tdnsQuery := DNSQuery{\"A\", \"a.\" + testDomain}\n\texpectedRes := DNSAnswers{{\"A\", \"1.1.1.1\"}, {\"A\", \"2.2.2.2\"}}\n\ts1.AddEntryToDNSDatabase(dnsQuery, expectedRes)\n\ts2.AddEntryToDNSDatabase(dnsQuery, DNSAnswers{{\"A\", \"2.2.2.2\"}})\n\ts1.AddEntryToDNSDatabaseRetry(dnsQuery, expectedRes)\n\ts2.AddEntryToDNSDatabaseRetry(dnsQuery, expectedRes)\n\terr := DNSLookupAuthoritativeAllWithValidationRetryE(t, dnsQuery, []string{s1.Address(), s2.Address()}, expectedRes, 5, time.Second)\n\trequire.NoError(t, err)\n}\n\n// First lookups should fail because of inconsistent authoritative replies\n// Retry lookups should fail also because of inconsistent authoritative replies\nfunc TestErrorDNSLookupAuthoritativeAllWithValidationRetry(t *testing.T) {\n\tt.Parallel()\n\ts1, s2 := setupTestDNSServersRetry(t)\n\tdefer shutDownServers(t, s1, s2)\n\tdnsQuery := DNSQuery{\"A\", \"a.\" + testDomain}\n\texpectedRes := DNSAnswers{{\"A\", \"1.1.1.1\"}, {\"A\", \"2.2.2.2\"}}\n\ts1.AddEntryToDNSDatabase(dnsQuery, expectedRes)\n\ts2.AddEntryToDNSDatabase(dnsQuery, DNSAnswers{{\"A\", \"2.2.2.2\"}})\n\ts1.AddEntryToDNSDatabaseRetry(dnsQuery, expectedRes)\n\ts2.AddEntryToDNSDatabaseRetry(dnsQuery, DNSAnswers{{\"A\", \"2.2.2.2\"}})\n\terr := DNSLookupAuthoritativeAllWithValidationRetryE(t, dnsQuery, []string{s1.Address(), s2.Address()}, expectedRes, 5, time.Second)\n\tif _, ok := err.(retry.MaxRetriesExceeded); !ok {\n\t\tt.Errorf(\"unexpected error, got %q\", err)\n\t}\n}\n\nfunc shutDownServers(t *testing.T, s1, s2 *dnsTestServer) {\n\terr := s1.Server.Shutdown()\n\tassert.NoError(t, err)\n\terr = s2.Server.Shutdown()\n\tassert.NoError(t, err)\n}\n"
  },
  {
    "path": "modules/dns-helper/dns_local_server.go",
    "content": "package dns_helper\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"net\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/miekg/dns\"\n)\n\nvar testDomain = \"gruntwork.io\"\n\n// dnsDatabase stores a collection of DNSQuery with their respective DNSAnswers, to be used by a local dnsTestServer\ntype dnsDatabase map[DNSQuery]DNSAnswers\n\n// dnsTestServer helper for testing this package using local DNS nameservers with test records\ntype dnsTestServer struct {\n\tServer           *dns.Server\n\tDNSDatabase      dnsDatabase\n\tDNSDatabaseRetry dnsDatabase\n}\n\n// newDNSTestServer returns a new instance of dnsTestServer\nfunc newDNSTestServer(server *dns.Server) *dnsTestServer {\n\treturn &dnsTestServer{Server: server, DNSDatabase: make(dnsDatabase), DNSDatabaseRetry: make(dnsDatabase)}\n}\n\n// Address returns the host:port string of the server listener\nfunc (s *dnsTestServer) Address() string {\n\treturn s.Server.PacketConn.LocalAddr().String()\n}\n\n// AddEntryToDNSDatabase adds DNSAnswers to the DNSQuery in the database of the server\nfunc (s *dnsTestServer) AddEntryToDNSDatabase(q DNSQuery, a DNSAnswers) {\n\ts.DNSDatabase[q] = append(s.DNSDatabase[q], a...)\n}\n\n// AddEntryToDNSDatabaseRetry adds DNSAnswers to the DNSQuery in the database used when retrying\nfunc (s *dnsTestServer) AddEntryToDNSDatabaseRetry(q DNSQuery, a DNSAnswers) {\n\ts.DNSDatabaseRetry[q] = append(s.DNSDatabaseRetry[q], a...)\n}\n\n// setupTestDNSServers runs and returns 2x local dnsTestServer, initialized with NS records for the testDomain pointing to themselves\n// it uses a handler that will send replies stored in their internal DNSDatabase\nfunc setupTestDNSServers(t *testing.T) (s1, s2 *dnsTestServer) {\n\ts1 = runTestDNSServer(t, \"0\")\n\ts2 = runTestDNSServer(t, \"0\")\n\n\tq := DNSQuery{\"NS\", testDomain}\n\ta := DNSAnswers{{\"NS\", s1.Address() + \".\"}, {\"NS\", s2.Address() + \".\"}}\n\ts1.AddEntryToDNSDatabase(q, a)\n\ts2.AddEntryToDNSDatabase(q, a)\n\n\ts1.Server.Handler.(*dns.ServeMux).HandleFunc(testDomain+\".\", func(w dns.ResponseWriter, r *dns.Msg) {\n\t\tstdDNSHandler(t, w, r, s1, false)\n\t})\n\ts2.Server.Handler.(*dns.ServeMux).HandleFunc(testDomain+\".\", func(w dns.ResponseWriter, r *dns.Msg) {\n\t\tstdDNSHandler(t, w, r, s2, true)\n\t})\n\n\treturn s1, s2\n}\n\n// setupTestDNSServersRetry runs and returns 2x local dnsTestServer, initialized with NS records for the testDomain pointing to themselves\n// it uses a handler that will send replies stored in their internal DNSDatabase, and then switch to their DNSDatabaseRetry after some time\nfunc setupTestDNSServersRetry(t *testing.T) (s1, s2 *dnsTestServer) {\n\ts1 = runTestDNSServer(t, \"0\")\n\ts2 = runTestDNSServer(t, \"0\")\n\n\tq := DNSQuery{\"NS\", testDomain}\n\ta := DNSAnswers{{\"NS\", s1.Address() + \".\"}, {\"NS\", s2.Address() + \".\"}}\n\ts1.AddEntryToDNSDatabase(q, a)\n\ts2.AddEntryToDNSDatabase(q, a)\n\ts1.AddEntryToDNSDatabaseRetry(q, a)\n\ts2.AddEntryToDNSDatabaseRetry(q, a)\n\n\ts1.Server.Handler.(*dns.ServeMux).HandleFunc(testDomain+\".\", func(w dns.ResponseWriter, r *dns.Msg) {\n\t\tretryDNSHandler(t, w, r, s1, false)\n\t})\n\ts2.Server.Handler.(*dns.ServeMux).HandleFunc(testDomain+\".\", func(w dns.ResponseWriter, r *dns.Msg) {\n\t\tretryDNSHandler(t, w, r, s2, true)\n\t})\n\n\treturn s1, s2\n}\n\n// runTestDNSServer starts and returns a new dnsTestServer listening in localhost and the given UDP port\nfunc runTestDNSServer(t *testing.T, port string) *dnsTestServer {\n\tlistener, err := net.ListenPacket(\"udp\", \"127.0.0.1:\"+port)\n\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tmux := dns.NewServeMux()\n\tserver := &dns.Server{PacketConn: listener, Net: \"udp\", Handler: mux}\n\n\tgo func() {\n\t\tif err := server.ActivateAndServe(); err != nil {\n\t\t\tlog.Printf(\"Error in local DNS server: %s\", err)\n\t\t}\n\t}()\n\n\treturn newDNSTestServer(server)\n}\n\n// doDNSAnswer sends replies to the DNS question from client, using the dnsDatabase to lookup the answers to the query\n// when invertAnswers is true, reverses the order of the answers from the dnsDatabase, useful to simulate realistic nameservers behaviours\nfunc doDNSAnswer(t *testing.T, w dns.ResponseWriter, r *dns.Msg, d dnsDatabase, invertAnswers bool) {\n\tm := new(dns.Msg)\n\tm.SetReply(r)\n\tm.Authoritative = true\n\tq := m.Question[0]\n\tqtype := dns.TypeToString[q.Qtype]\n\tanswers := d[DNSQuery{qtype, strings.TrimSuffix(q.Name, \".\")}]\n\n\tvar seen = make(map[DNSAnswer]bool)\n\n\tfor _, r := range answers {\n\t\tif seen[r] {\n\t\t\tcontinue\n\t\t}\n\t\tseen[r] = true\n\n\t\trr, err := dns.NewRR(fmt.Sprintf(\"%s %s\", q.Name, r.String()))\n\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"err: %s\", err)\n\t\t}\n\n\t\tm.Answer = append(m.Answer, rr)\n\t}\n\n\tif invertAnswers {\n\t\tfor i, j := 0, len(m.Answer)-1; i < j; i, j = i+1, j-1 {\n\t\t\tm.Answer[i], m.Answer[j] = m.Answer[j], m.Answer[i]\n\t\t}\n\t}\n\n\tw.WriteMsg(m)\n}\n\n// stdDNSHandler uses the internal DNSDatabase to send answers to DNS queries\nfunc stdDNSHandler(t *testing.T, w dns.ResponseWriter, r *dns.Msg, s *dnsTestServer, invertAnswers bool) {\n\tdoDNSAnswer(t, w, r, s.DNSDatabase, invertAnswers)\n}\n\nvar startTime = time.Now()\n\n// retryDNSHandler uses the internal DNSDatabase to send answers to DNS queries, and switches\n// to using the internal DNSDatabaseRetry after 3 seconds from startup\nfunc retryDNSHandler(t *testing.T, w dns.ResponseWriter, r *dns.Msg, s *dnsTestServer, invertAnswers bool) {\n\tif time.Now().Sub(startTime).Seconds() > 3 {\n\t\tdoDNSAnswer(t, w, r, s.DNSDatabaseRetry, invertAnswers)\n\t} else {\n\t\tdoDNSAnswer(t, w, r, s.DNSDatabase, invertAnswers)\n\t}\n}\n"
  },
  {
    "path": "modules/dns-helper/errors.go",
    "content": "package dns_helper\n\nimport \"fmt\"\n\n// NoResolversError is an error that occurs if no resolvers have been set for DNSLookupE\ntype NoResolversError struct{}\n\nfunc (err NoResolversError) Error() string {\n\treturn \"No resolvers set for DNSLookupE call\"\n}\n\n// QueryTypeError is an error that occurs if the DNS query type is not supported\ntype QueryTypeError struct {\n\tType string\n}\n\nfunc (err QueryTypeError) Error() string {\n\treturn fmt.Sprintf(\"Wrong DNS query type: %s\", err.Type)\n}\n\n// NotFoundError is an error that occurs if no answer found\ntype NotFoundError struct {\n\tQuery      DNSQuery\n\tNameserver string\n}\n\nfunc (err NotFoundError) Error() string {\n\treturn fmt.Sprintf(\"No %s record found for %s querying nameserver %s\", err.Query.Type, err.Query.Name, err.Nameserver)\n}\n\n// InconsistentAuthoritativeError is an error that occurs if an authoritative answer is different from another\ntype InconsistentAuthoritativeError struct {\n\tQuery           DNSQuery\n\tAnswers         DNSAnswers\n\tNameserver      string\n\tPreviousAnswers DNSAnswers\n}\n\nfunc (err InconsistentAuthoritativeError) Error() string {\n\treturn fmt.Sprintf(\"Inconsistent authoritative answer from %s to DNS query %s. Got: %s Previous: %s\", err.Nameserver, err.Query, err.Answers, err.PreviousAnswers)\n}\n\n// NSNotFoundError is an error that occurs if no NS records found\ntype NSNotFoundError struct {\n\tFQDN       string\n\tNameserver string\n}\n\nfunc (err NSNotFoundError) Error() string {\n\treturn fmt.Sprintf(\"No NS record found for %s up to apex domain %s\", err.FQDN, err.Nameserver)\n}\n\n// MaxRetriesExceeded is an error that occurs when the maximum amount of retries is exceeded.\ntype MaxRetriesExceeded struct {\n\tDescription string\n\tMaxRetries  int\n}\n\nfunc (err MaxRetriesExceeded) Error() string {\n\treturn fmt.Sprintf(\"'%s' unsuccessful after %d retries\", err.Description, err.MaxRetries)\n}\n\n// ValidationError is an error that occurs when answers validation fails\ntype ValidationError struct {\n\tQuery           DNSQuery\n\tAnswers         DNSAnswers\n\tExpectedAnswers DNSAnswers\n}\n\nfunc (err ValidationError) Error() string {\n\treturn fmt.Sprintf(\"Unexpected answer to DNS query %s. Got: %s Expected: %s\", err.Query, err.Answers, err.ExpectedAnswers)\n}\n"
  },
  {
    "path": "modules/docker/build.go",
    "content": "package docker\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/gruntwork-io/terratest/modules/logger\"\n\t\"github.com/gruntwork-io/terratest/modules/shell\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/hashicorp/go-multierror\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// BuildOptions defines options that can be passed to the 'docker build' command.\ntype BuildOptions struct {\n\t// Tags for the Docker image\n\tTags []string\n\n\t// Build args to pass the 'docker build' command\n\tBuildArgs []string\n\n\t// Target build arg to pass to the 'docker build' command\n\tTarget string\n\n\t// All architectures to target in a multiarch build. Configuring this variable will cause terratest to use docker\n\t// buildx to construct multiarch images.\n\t// You can read more about multiarch docker builds in the official documentation for buildx:\n\t// https://docs.docker.com/buildx/working-with-buildx/\n\t// NOTE: This list does not automatically include the current platform. For example, if you are building images on\n\t// an Apple Silicon based MacBook, and you configure this variable to []string{\"linux/amd64\"} to build an amd64\n\t// image, the buildx command will not automatically include linux/arm64 - you must include that explicitly.\n\tArchitectures []string\n\n\t// Whether or not to push images directly to the registry on build. Note that for multiarch images (Architectures is\n\t// not empty), this must be true to ensure availability of all architectures - only the image for the current\n\t// platform will be loaded into the daemon (due to a limitation of the docker daemon), so you won't be able to run a\n\t// `docker push` command later to push the multiarch image.\n\t// See https://github.com/moby/moby/pull/38738 for more info on the limitation of multiarch images in docker daemon.\n\tPush bool\n\n\t// Whether or not to load the image into the docker daemon at the end of a multiarch build so that it can be used\n\t// locally. Note that this is only used when Architectures is set, and assumes the current architecture is already\n\t// included in the Architectures list.\n\tLoad bool\n\n\t// Custom CLI options that will be passed as-is to the 'docker build' command. This is an \"escape hatch\" that allows\n\t// Terratest to not have to support every single command-line option offered by the 'docker build' command, and\n\t// solely focus on the most important ones.\n\tOtherOptions []string\n\n\t// Whether ot not to enable buildkit. You can find more information about buildkit here https://docs.docker.com/build/buildkit/#getting-started.\n\tEnableBuildKit bool\n\n\t// Additional environment variables to pass in when running docker build command.\n\tEnv map[string]string\n\n\t// Set a logger that should be used. See the logger package for more info.\n\tLogger *logger.Logger\n}\n\n// Build runs the 'docker build' command at the given path with the given options and fails the test if there are any\n// errors.\nfunc Build(t testing.TestingT, path string, options *BuildOptions) {\n\trequire.NoError(t, BuildE(t, path, options))\n}\n\n// BuildE runs the 'docker build' command at the given path with the given options and returns any errors.\nfunc BuildE(t testing.TestingT, path string, options *BuildOptions) error {\n\toptions.Logger.Logf(t, \"Running 'docker build' in %s\", path)\n\n\tenv := make(map[string]string)\n\tif options.Env != nil {\n\t\tenv = options.Env\n\t}\n\n\tif options.EnableBuildKit {\n\t\tenv[\"DOCKER_BUILDKIT\"] = \"1\"\n\t}\n\n\tcmd := shell.Command{\n\t\tCommand: \"docker\",\n\t\tArgs:    formatDockerBuildArgs(path, options),\n\t\tLogger:  options.Logger,\n\t\tEnv:     env,\n\t}\n\n\tif err := shell.RunCommandE(t, cmd); err != nil {\n\t\treturn err\n\t}\n\n\t// For non multiarch images, we need to call docker push for each tag since build does not have a push option like\n\t// buildx.\n\tif len(options.Architectures) == 0 && options.Push {\n\t\tvar errorsOccurred = new(multierror.Error)\n\t\tfor _, tag := range options.Tags {\n\t\t\tif err := PushE(t, options.Logger, tag); err != nil {\n\t\t\t\toptions.Logger.Logf(t, \"ERROR: error pushing tag %s\", tag)\n\t\t\t\terrorsOccurred = multierror.Append(errorsOccurred, err)\n\t\t\t}\n\t\t}\n\t\treturn errorsOccurred.ErrorOrNil()\n\t}\n\n\t// For multiarch images, if a load is requested call the load command to export the built image into the daemon.\n\tif len(options.Architectures) > 0 && options.Load {\n\t\tloadCmd := shell.Command{\n\t\t\tCommand: \"docker\",\n\t\t\tArgs:    formatDockerBuildxLoadArgs(path, options),\n\t\t\tLogger:  options.Logger,\n\t\t}\n\t\treturn shell.RunCommandE(t, loadCmd)\n\t}\n\n\treturn nil\n}\n\n// GitCloneAndBuild builds a new Docker image from a given Git repo. This function will clone the given repo at the\n// specified ref, and call the docker build command on the cloned repo from the given relative path (relative to repo\n// root). This will fail the test if there are any errors.\nfunc GitCloneAndBuild(\n\tt testing.TestingT,\n\trepo string,\n\tref string,\n\tpath string,\n\tdockerBuildOpts *BuildOptions,\n) {\n\trequire.NoError(t, GitCloneAndBuildE(t, repo, ref, path, dockerBuildOpts))\n}\n\n// GitCloneAndBuildE builds a new Docker image from a given Git repo. This function will clone the given repo at the\n// specified ref, and call the docker build command on the cloned repo from the given relative path (relative to repo\n// root).\nfunc GitCloneAndBuildE(\n\tt testing.TestingT,\n\trepo string,\n\tref string,\n\tpath string,\n\tdockerBuildOpts *BuildOptions,\n) error {\n\tworkingDir, err := os.MkdirTemp(\"\", \"\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer os.RemoveAll(workingDir)\n\n\tcloneCmd := shell.Command{\n\t\tCommand: \"git\",\n\t\tArgs:    []string{\"clone\", repo, workingDir},\n\t}\n\tif err := shell.RunCommandE(t, cloneCmd); err != nil {\n\t\treturn err\n\t}\n\n\tcheckoutCmd := shell.Command{\n\t\tCommand:    \"git\",\n\t\tArgs:       []string{\"checkout\", ref},\n\t\tWorkingDir: workingDir,\n\t}\n\tif err := shell.RunCommandE(t, checkoutCmd); err != nil {\n\t\treturn err\n\t}\n\n\tcontextPath := filepath.Join(workingDir, path)\n\tif err := BuildE(t, contextPath, dockerBuildOpts); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\n// formatDockerBuildArgs formats the arguments for the 'docker build' command.\nfunc formatDockerBuildArgs(path string, options *BuildOptions) []string {\n\targs := []string{}\n\n\tif len(options.Architectures) > 0 {\n\t\targs = append(\n\t\t\targs,\n\t\t\t\"buildx\",\n\t\t\t\"build\",\n\t\t\t\"--platform\",\n\t\t\tstrings.Join(options.Architectures, \",\"),\n\t\t)\n\t\tif options.Push {\n\t\t\targs = append(args, \"--push\")\n\t\t}\n\t} else {\n\t\targs = append(args, \"build\")\n\t}\n\n\treturn append(args, formatDockerBuildBaseArgs(path, options)...)\n}\n\n// formatDockerBuildxLoadArgs formats the arguments for calling load on the 'docker buildx' command.\nfunc formatDockerBuildxLoadArgs(path string, options *BuildOptions) []string {\n\targs := []string{\n\t\t\"buildx\",\n\t\t\"build\",\n\t\t\"--load\",\n\t}\n\treturn append(args, formatDockerBuildBaseArgs(path, options)...)\n}\n\n// formatDockerBuildBaseArgs formats the common args for the build command, both for `build` and `buildx`.\nfunc formatDockerBuildBaseArgs(path string, options *BuildOptions) []string {\n\targs := []string{}\n\tfor _, tag := range options.Tags {\n\t\targs = append(args, \"--tag\", tag)\n\t}\n\n\tfor _, arg := range options.BuildArgs {\n\t\targs = append(args, \"--build-arg\", arg)\n\t}\n\n\tif len(options.Target) > 0 {\n\t\targs = append(args, \"--target\", options.Target)\n\t}\n\n\targs = append(args, options.OtherOptions...)\n\n\targs = append(args, path)\n\treturn args\n}\n"
  },
  {
    "path": "modules/docker/build_test.go",
    "content": "package docker\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/git\"\n\t\"github.com/gruntwork-io/terratest/modules/logger\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestBuild(t *testing.T) {\n\tt.Parallel()\n\n\ttag := \"gruntwork-io/test-image:v1\"\n\ttext := \"Hello, World!\"\n\n\toptions := &BuildOptions{\n\t\tTags:      []string{tag},\n\t\tBuildArgs: []string{fmt.Sprintf(\"text=%s\", text)},\n\t}\n\n\tBuild(t, \"../../test/fixtures/docker\", options)\n\n\tout := Run(t, tag, &RunOptions{Remove: true})\n\trequire.Contains(t, out, text)\n}\n\nfunc TestBuildWithBuildKit(t *testing.T) {\n\tt.Parallel()\n\n\ttag := \"gruntwork-io/test-image-with-buildkit:v1\"\n\ttestToken := \"testToken\"\n\toptions := &BuildOptions{\n\t\tTags:           []string{tag},\n\t\tEnableBuildKit: true,\n\t\tOtherOptions:   []string{\"--secret\", fmt.Sprintf(\"id=github-token,env=%s\", \"GITHUB_OAUTH_TOKEN\")},\n\t\tEnv:            map[string]string{\"GITHUB_OAUTH_TOKEN\": testToken},\n\t}\n\n\tBuild(t, \"../../test/fixtures/docker-with-buildkit\", options)\n\tout := Run(t, tag, &RunOptions{Remove: false})\n\trequire.Contains(t, out, testToken)\n}\n\nfunc TestBuildMultiArch(t *testing.T) {\n\tt.Parallel()\n\n\ttag := \"gruntwork-io/test-image:v1\"\n\ttext := \"Hello, World!\"\n\n\toptions := &BuildOptions{\n\t\tTags:          []string{tag},\n\t\tBuildArgs:     []string{fmt.Sprintf(\"text=%s\", text)},\n\t\tArchitectures: []string{\"linux/arm64\", \"linux/amd64\"},\n\t\tLoad:          true,\n\t}\n\n\tBuild(t, \"../../test/fixtures/docker\", options)\n\tout := Run(t, tag, &RunOptions{Remove: true})\n\trequire.Contains(t, out, text)\n}\n\nfunc TestBuildWithTarget(t *testing.T) {\n\tt.Parallel()\n\n\ttag := \"gruntwork-io/test-image:target1\"\n\ttext := \"Hello, World!\"\n\ttext1 := \"Hello, World! This is build target 1!\"\n\n\toptions := &BuildOptions{\n\t\tTags:      []string{tag},\n\t\tBuildArgs: []string{fmt.Sprintf(\"text=%s\", text), fmt.Sprintf(\"text1=%s\", text1)},\n\t\tTarget:    \"step1\",\n\t}\n\n\tBuild(t, \"../../test/fixtures/docker\", options)\n\n\tout := Run(t, tag, &RunOptions{Remove: true})\n\trequire.Contains(t, out, text1)\n}\n\nfunc TestGitCloneAndBuild(t *testing.T) {\n\tt.Parallel()\n\n\tuniqueID := strings.ToLower(random.UniqueId())\n\timageTag := \"gruntwork-io-foo-test:\" + uniqueID\n\ttext := \"Hello, World!\"\n\n\tbuildOpts := &BuildOptions{\n\t\tTags:      []string{imageTag},\n\t\tBuildArgs: []string{fmt.Sprintf(\"text=%s\", text)},\n\t}\n\tgitBranchName := git.GetCurrentBranchName(t)\n\tif gitBranchName == \"\" {\n\t\tlogger.Logf(t, \"WARNING: git.GetCurrentBranchName returned an empty string; falling back to main\")\n\t\tgitBranchName = \"main\"\n\t}\n\tGitCloneAndBuild(t, \"git@github.com:gruntwork-io/terratest.git\", gitBranchName, \"test/fixtures/docker\", buildOpts)\n\n\tout := Run(t, imageTag, &RunOptions{Remove: true})\n\trequire.Contains(t, out, text)\n}\n"
  },
  {
    "path": "modules/docker/docker.go",
    "content": "// Package docker allows to interact with Docker and docker compose resources.\npackage docker\n"
  },
  {
    "path": "modules/docker/docker_compose.go",
    "content": "package docker\n\nimport (\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/gruntwork-io/terratest/modules/logger\"\n\t\"github.com/gruntwork-io/terratest/modules/shell\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/stretchr/testify/require\"\n\t\"gotest.tools/v3/icmd\"\n)\n\n// Options are Docker options.\ntype Options struct {\n\tWorkingDir string\n\tEnvVars    map[string]string\n\n\t// Whether ot not to enable buildkit. You can find more information about buildkit here https://docs.docker.com/build/buildkit/#getting-started.\n\tEnableBuildKit bool\n\n\t// Set a logger that should be used. See the logger package for more info.\n\tLogger      *logger.Logger\n\tProjectName string\n}\n\n// RunDockerCompose runs docker compose with the given arguments and options and return stdout/stderr.\nfunc RunDockerCompose(t testing.TestingT, options *Options, args ...string) string {\n\tout, err := runDockerComposeE(t, false, options, args...)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn out\n}\n\n// RunDockerComposeAndGetStdout runs docker compose with the given arguments and options and returns only stdout.\nfunc RunDockerComposeAndGetStdOut(t testing.TestingT, options *Options, args ...string) string {\n\tout, err := runDockerComposeE(t, true, options, args...)\n\trequire.NoError(t, err)\n\treturn out\n}\n\n// RunDockerComposeE runs docker compose with the given arguments and options and return stdout/stderr.\nfunc RunDockerComposeE(t testing.TestingT, options *Options, args ...string) (string, error) {\n\treturn runDockerComposeE(t, false, options, args...)\n}\n\nfunc runDockerComposeE(t testing.TestingT, stdout bool, options *Options, args ...string) (string, error) {\n\tvar cmd shell.Command\n\n\tprojectName := options.ProjectName\n\tif len(projectName) <= 0 {\n\t\tprojectName = strings.ToLower(t.Name())\n\t}\n\n\tdockerComposeVersionCmd := icmd.Command(\"docker\", \"compose\", \"version\")\n\tresult := icmd.RunCmd(dockerComposeVersionCmd)\n\n\tif options.EnableBuildKit {\n\t\tif options.EnvVars == nil {\n\t\t\toptions.EnvVars = make(map[string]string)\n\t\t}\n\n\t\toptions.EnvVars[\"DOCKER_BUILDKIT\"] = \"1\"\n\t\toptions.EnvVars[\"COMPOSE_DOCKER_CLI_BUILD\"] = \"1\"\n\t}\n\n\tif result.ExitCode == 0 {\n\t\tcmd = shell.Command{\n\t\t\tCommand:    \"docker\",\n\t\t\tArgs:       append([]string{\"compose\", \"--project-name\", generateValidDockerComposeProjectName(projectName)}, args...),\n\t\t\tWorkingDir: options.WorkingDir,\n\t\t\tEnv:        options.EnvVars,\n\t\t\tLogger:     options.Logger,\n\t\t}\n\t} else {\n\t\tcmd = shell.Command{\n\t\t\tCommand: \"docker-compose\",\n\t\t\t// We append --project-name to ensure containers from multiple different tests using Docker Compose don't end\n\t\t\t// up in the same project and end up conflicting with each other.\n\t\t\tArgs:       append([]string{\"--project-name\", generateValidDockerComposeProjectName(projectName)}, args...),\n\t\t\tWorkingDir: options.WorkingDir,\n\t\t\tEnv:        options.EnvVars,\n\t\t\tLogger:     options.Logger,\n\t\t}\n\t}\n\n\tif stdout {\n\t\treturn shell.RunCommandAndGetStdOut(t, cmd), nil\n\t}\n\n\treturn shell.RunCommandAndGetOutputE(t, cmd)\n}\n\n// Note: docker-compose command doesn't like lower case or special characters, other than -.\nfunc generateValidDockerComposeProjectName(str string) string {\n\tlower_str := strings.ToLower(str)\n\treturn regexp.MustCompile(`[^a-zA-Z0-9 ]+`).ReplaceAllString(lower_str, \"-\")\n}\n"
  },
  {
    "path": "modules/docker/docker_compose_test.go",
    "content": "package docker\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestDockerComposeWithBuildKit(t *testing.T) {\n\tt.Parallel()\n\n\ttestToken := \"testToken\"\n\tdockerOptions := &Options{\n\t\t// Directory where docker-compose.yml lives\n\t\tWorkingDir: \"../../test/fixtures/docker-compose-with-buildkit\",\n\n\t\t// Configure the port the web app will listen on and the text it will return using environment variables\n\t\tEnvVars: map[string]string{\n\t\t\t\"GITHUB_OAUTH_TOKEN\": testToken,\n\t\t},\n\t\tEnableBuildKit: true,\n\t}\n\tout := RunDockerCompose(t, dockerOptions, \"build\", \"--no-cache\")\n\tout = RunDockerCompose(t, dockerOptions, \"up\")\n\n\trequire.Contains(t, out, testToken)\n}\n\nfunc TestDockerComposeWithCustomProjectName(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname     string\n\t\toptions  *Options\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname: \"Testing \",\n\t\t\toptions: &Options{\n\t\t\t\tWorkingDir: \"../../test/fixtures/docker-compose-with-custom-project-name\",\n\t\t\t},\n\t\t\texpected: \"testdockercomposewithcustomprojectname\",\n\t\t},\n\t\t{\n\t\t\tname: \"Testing\",\n\t\t\toptions: &Options{\n\t\t\t\tWorkingDir:  \"../../test/fixtures/docker-compose-with-custom-project-name\",\n\t\t\t\tProjectName: \"testingProjectName\",\n\t\t\t},\n\t\t\texpected: \"testingprojectname\",\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tt.Log(test.name)\n\n\t\t\toutput := RunDockerCompose(t, test.options, \"up\", \"-d\")\n\t\t\tdefer RunDockerCompose(t, test.options, \"down\", \"--remove-orphans\", \"--timeout\", \"2\")\n\n\t\t\trequire.Contains(t, output, test.expected)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "modules/docker/host.go",
    "content": "package docker\n\nimport (\n\t\"os\"\n\t\"strings\"\n)\n\n// GetDockerHost returns the name or address of the host on which the Docker engine is running.\nfunc GetDockerHost() string {\n\treturn getDockerHostFromEnv(os.Environ())\n}\n\nfunc getDockerHostFromEnv(env []string) string {\n\t// Parses the DOCKER_HOST environment variable to find the address\n\t//\n\t// For valid formats see:\n\t// https://github.com/docker/cli/blob/6916b427a0b07e8581d121967633235ced6db9a1/opts/hosts.go#L69\n\tvar dockerUrl []string\n\n\tfor _, item := range env {\n\t\tenvVar := strings.Split(item, \"=\")\n\t\tif len(envVar) == 2 && envVar[0] == \"DOCKER_HOST\" {\n\t\t\tdockerUrl = strings.Split(envVar[1], \":\")\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif len(dockerUrl) < 2 {\n\t\t// DOCKER_HOST was empty, not present or not a valid URL\n\t\treturn \"localhost\"\n\t}\n\n\tswitch dockerUrl[0] {\n\tcase \"tcp\", \"ssh\", \"fd\":\n\t\treturn strings.TrimPrefix(dockerUrl[1], \"//\")\n\tdefault:\n\t\t// if DOCKER_HOST is not in one of the formats listed above, return default\n\t\treturn \"localhost\"\n\t}\n}\n"
  },
  {
    "path": "modules/docker/host_test.go",
    "content": "package docker\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestGetDockerHostFromEnv(t *testing.T) {\n\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tInput    string\n\t\tExpected string\n\t}{\n\t\t{\n\t\t\t\"unix:///var/run/docker.sock\",\n\t\t\t\"localhost\",\n\t\t},\n\t\t{\n\t\t\t\"npipe:////./pipe/docker_engine\",\n\t\t\t\"localhost\",\n\t\t},\n\t\t{\n\t\t\t\"tcp://1.2.3.4:1234\",\n\t\t\t\"1.2.3.4\",\n\t\t},\n\t\t{\n\t\t\t\"tcp://1.2.3.4\",\n\t\t\t\"1.2.3.4\",\n\t\t},\n\t\t{\n\t\t\t\"ssh://1.2.3.4:22\",\n\t\t\t\"1.2.3.4\",\n\t\t},\n\t\t{\n\t\t\t\"fd://1.2.3.4:1234\",\n\t\t\t\"1.2.3.4\",\n\t\t},\n\t\t{\n\t\t\t\"\",\n\t\t\t\"localhost\",\n\t\t},\n\t\t{\n\t\t\t\"invalidValue\",\n\t\t\t\"localhost\",\n\t\t},\n\t\t{\n\t\t\t\"invalid::value::with::semicolons\",\n\t\t\t\"localhost\",\n\t\t},\n\t}\n\tfor _, test := range tests {\n\t\tt.Run(fmt.Sprintf(\"GetDockerHostFromEnv: %s\", test.Input), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\ttestEnv := []string{\n\t\t\t\t\"FOO=bar\",\n\t\t\t\tfmt.Sprintf(\"DOCKER_HOST=%s\", test.Input),\n\t\t\t\t\"BAR=baz\",\n\t\t\t}\n\n\t\t\thost := getDockerHostFromEnv(testEnv)\n\t\t\tassert.Equal(t, test.Expected, host)\n\t\t})\n\t}\n\n\tt.Run(\"GetDockerHostFromEnv: DOCKER_HOST unset\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\ttestEnv := []string{\n\t\t\t\"FOO=bar\",\n\t\t\t\"BAR=baz\",\n\t\t}\n\n\t\thost := getDockerHostFromEnv(testEnv)\n\t\tassert.Equal(t, \"localhost\", host)\n\t})\n}\n"
  },
  {
    "path": "modules/docker/images.go",
    "content": "package docker\n\nimport (\n\t\"bufio\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"github.com/gruntwork-io/terratest/modules/logger\"\n\t\"github.com/gruntwork-io/terratest/modules/shell\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// Image represents a docker image, and exports all the fields that the docker images command returns for the\n// image.\ntype Image struct {\n\t// ID is the image ID in docker, and can be used to identify the image in place of the repo and tag.\n\tID string\n\n\t// Repository is the image repository.\n\tRepository string\n\n\t// Tag is the image tag wichin the repository.\n\tTag string\n\n\t// CreatedAt represents a timestamp for when the image was created.\n\tCreatedAt string\n\n\t// CreatedSince is a diff between when the image was created to now.\n\tCreatedSince string\n\n\t// SharedSize is the amount of space that an image shares with another one (i.e. their common data).\n\tSharedSize string\n\n\t// UniqueSize is the amount of space that is only used by a given image.\n\tUniqueSize string\n\n\t// VirtualSize is the total size of the image, combining SharedSize and UniqueSize.\n\tVirtualSize string\n\n\t// Containers represents the list of containers that are using the image.\n\tContainers string\n\n\t// Digest is the hash digest of the image, if requested.\n\tDigest string\n}\n\nfunc (image Image) String() string {\n\treturn fmt.Sprintf(\"%s:%s\", image.Repository, image.Tag)\n}\n\n// DeleteImage removes a docker image using the Docker CLI. This will fail the test if there is an error.\nfunc DeleteImage(t testing.TestingT, img string, logger *logger.Logger) {\n\trequire.NoError(t, DeleteImageE(t, img, logger))\n}\n\n// DeleteImageE removes a docker image using the Docker CLI.\nfunc DeleteImageE(t testing.TestingT, img string, logger *logger.Logger) error {\n\tcmd := shell.Command{\n\t\tCommand: \"docker\",\n\t\tArgs:    []string{\"rmi\", img},\n\t\tLogger:  logger,\n\t}\n\treturn shell.RunCommandE(t, cmd)\n}\n\n// ListImages calls docker images using the Docker CLI to list the available images on the local docker daemon.\nfunc ListImages(t testing.TestingT, logger *logger.Logger) []Image {\n\tout, err := ListImagesE(t, logger)\n\trequire.NoError(t, err)\n\treturn out\n}\n\n// ListImagesE calls docker images using the Docker CLI to list the available images on the local docker daemon.\nfunc ListImagesE(t testing.TestingT, logger *logger.Logger) ([]Image, error) {\n\tcmd := shell.Command{\n\t\tCommand: \"docker\",\n\t\tArgs:    []string{\"images\", \"--format\", \"{{ json . }}\"},\n\t\tLogger:  logger,\n\t}\n\tout, err := shell.RunCommandAndGetOutputE(t, cmd)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Parse and return the list of image objects.\n\timages := []Image{}\n\tscanner := bufio.NewScanner(strings.NewReader(out))\n\tfor scanner.Scan() {\n\t\tline := scanner.Text()\n\t\tvar image Image\n\t\terr := json.Unmarshal([]byte(line), &image)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\timages = append(images, image)\n\t}\n\treturn images, nil\n}\n\n// DoesImageExist lists the images in the docker daemon and returns true if the given image label (repo:tag) exists.\n// This will fail the test if there is an error.\nfunc DoesImageExist(t testing.TestingT, imgLabel string, logger *logger.Logger) bool {\n\timages := ListImages(t, logger)\n\timageTags := []string{}\n\tfor _, image := range images {\n\t\timageTags = append(imageTags, image.String())\n\t}\n\treturn slices.Contains(imageTags, imgLabel)\n}\n"
  },
  {
    "path": "modules/docker/images_test.go",
    "content": "package docker\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestListImagesAndDeleteImage(t *testing.T) {\n\tt.Parallel()\n\n\tuniqueID := strings.ToLower(random.UniqueId())\n\trepo := \"gruntwork-io/test-image\"\n\ttag := fmt.Sprintf(\"v1-%s\", uniqueID)\n\timg := fmt.Sprintf(\"%s:%s\", repo, tag)\n\n\toptions := &BuildOptions{\n\t\tTags: []string{img},\n\t}\n\tBuild(t, \"../../test/fixtures/docker\", options)\n\n\tassert.True(t, DoesImageExist(t, img, nil))\n\tDeleteImage(t, img, nil)\n\tassert.False(t, DoesImageExist(t, img, nil))\n}\n"
  },
  {
    "path": "modules/docker/inspect.go",
    "content": "package docker\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/gruntwork-io/terratest/modules/logger\"\n\t\"github.com/gruntwork-io/terratest/modules/shell\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// ContainerInspect defines the output of the Inspect method, with the options returned by 'docker inspect'\n// converted into a more friendly and testable interface\ntype ContainerInspect struct {\n\t// ID of the inspected container\n\tID string\n\n\t// Name of the inspected container\n\tName string\n\n\t// time.Time that the container was created\n\tCreated time.Time\n\n\t// String representing the container's status\n\tStatus string\n\n\t// Whether the container is currently running or not\n\tRunning bool\n\n\t// Container's exit code\n\tExitCode uint8\n\n\t// String with the container's error message, if there is any\n\tError string\n\n\t// Ports exposed by the container\n\tPorts []Port\n\n\t// Volume bindings made to the container\n\tBinds []VolumeBind\n\n\t// Health check\n\tHealth HealthCheck\n}\n\n// Port represents a single port mapping exported by the container\ntype Port struct {\n\tHostPort      uint16\n\tContainerPort uint16\n\tProtocol      string\n}\n\n// VolumeBind represents a single volume binding made to the container\ntype VolumeBind struct {\n\tSource      string\n\tDestination string\n}\n\n// HealthCheck represents the current health history of the container\ntype HealthCheck struct {\n\t// Health check status\n\tStatus string\n\n\t// Current count of failing health checks\n\tFailingStreak uint8\n\n\t// Log of failures\n\tLog []HealthLog\n}\n\n// HealthLog represents the output of a single Health check of the container\ntype HealthLog struct {\n\t// Start time of health check\n\tStart string\n\n\t// End time of health check\n\tEnd string\n\n\t// Exit code of health check\n\tExitCode uint8\n\n\t// Output of health check\n\tOutput string\n}\n\n// inspectOutput defines options that will be returned by 'docker inspect', in JSON format.\n// Not all options are included here, only the ones that we might need\ntype inspectOutput struct {\n\tId      string\n\tCreated string\n\tName    string\n\tState   struct {\n\t\tHealth   HealthCheck\n\t\tStatus   string\n\t\tRunning  bool\n\t\tExitCode uint8\n\t\tError    string\n\t}\n\tNetworkSettings struct {\n\t\tPorts map[string][]struct {\n\t\t\tHostIp   string\n\t\t\tHostPort string\n\t\t}\n\t}\n\tHostConfig struct {\n\t\tBinds []string\n\t}\n}\n\n// Inspect runs the 'docker inspect {container id}' command and returns a ContainerInspect\n// struct, converted from the output JSON, along with any errors\nfunc Inspect(t *testing.T, id string) *ContainerInspect {\n\tout, err := InspectE(t, id)\n\trequire.NoError(t, err)\n\n\treturn out\n}\n\n// InspectE runs the 'docker inspect {container id}' command and returns a ContainerInspect\n// struct, converted from the output JSON, along with any errors\nfunc InspectE(t *testing.T, id string) (*ContainerInspect, error) {\n\tcmd := shell.Command{\n\t\tCommand: \"docker\",\n\t\tArgs:    []string{\"container\", \"inspect\", id},\n\t\t// inspect is a short-running command, don't print the output.\n\t\tLogger: logger.Discard,\n\t}\n\n\tout, err := shell.RunCommandAndGetStdOutE(t, cmd)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar containers []inspectOutput\n\terr = json.Unmarshal([]byte(out), &containers)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif len(containers) == 0 {\n\t\treturn nil, fmt.Errorf(\"no container found with ID %s\", id)\n\t}\n\n\tcontainer := containers[0]\n\n\treturn transformContainer(t, container)\n}\n\n// transformContainerPorts converts 'docker inspect' output JSON into a more friendly and testable format\nfunc transformContainer(t *testing.T, container inspectOutput) (*ContainerInspect, error) {\n\tname := strings.TrimLeft(container.Name, \"/\")\n\n\tports, err := transformContainerPorts(container)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvolumes := transformContainerVolumes(container)\n\n\tcreated, err := time.Parse(time.RFC3339Nano, container.Created)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tinspect := ContainerInspect{\n\t\tID:       container.Id,\n\t\tName:     name,\n\t\tCreated:  created,\n\t\tStatus:   container.State.Status,\n\t\tRunning:  container.State.Running,\n\t\tExitCode: container.State.ExitCode,\n\t\tError:    container.State.Error,\n\t\tPorts:    ports,\n\t\tBinds:    volumes,\n\t\tHealth: HealthCheck{\n\t\t\tStatus:        container.State.Health.Status,\n\t\t\tFailingStreak: container.State.Health.FailingStreak,\n\t\t\tLog:           container.State.Health.Log,\n\t\t},\n\t}\n\n\treturn &inspect, nil\n}\n\n// transformContainerPorts converts Docker's ports from the following json into a more testable format\n//\n//\t{\n//\t  \"80/tcp\": [\n//\t    {\n//\t\t     \"HostIp\": \"\"\n//\t      \"HostPort\": \"8080\"\n//\t    }\n//\t  ]\n//\t}\nfunc transformContainerPorts(container inspectOutput) ([]Port, error) {\n\tvar ports []Port\n\n\tcPorts := container.NetworkSettings.Ports\n\n\tfor key, portBinding := range cPorts {\n\t\tsplit := strings.Split(key, \"/\")\n\n\t\tcontainerPort, err := strconv.ParseUint(split[0], 10, 16)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tvar protocol string\n\t\tif len(split) > 1 {\n\t\t\tprotocol = split[1]\n\t\t}\n\n\t\tfor _, port := range portBinding {\n\t\t\thostPort, err := strconv.ParseUint(port.HostPort, 10, 16)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tports = append(ports, Port{\n\t\t\t\tHostPort:      uint16(hostPort),\n\t\t\t\tContainerPort: uint16(containerPort),\n\t\t\t\tProtocol:      protocol,\n\t\t\t})\n\t\t}\n\t}\n\n\treturn ports, nil\n}\n\n// GetExposedHostPort returns an exposed host port according to requested container port. Returns 0 if the requested port is not exposed.\nfunc (inspectOutput ContainerInspect) GetExposedHostPort(containerPort uint16) uint16 {\n\tfor _, port := range inspectOutput.Ports {\n\t\tif port.ContainerPort == containerPort {\n\t\t\treturn port.HostPort\n\t\t}\n\t}\n\treturn uint16(0)\n}\n\n// transformContainerVolumes converts Docker's volume bindings from the\n// format \"/foo/bar:/foo/baz\" into a more testable one\nfunc transformContainerVolumes(container inspectOutput) []VolumeBind {\n\tbinds := container.HostConfig.Binds\n\tvolumes := make([]VolumeBind, 0, len(binds))\n\n\tfor _, bind := range binds {\n\t\tvar source, dest string\n\n\t\tsplit := strings.Split(bind, \":\")\n\n\t\t// Considering it as an unbound volume\n\t\tdest = split[0]\n\n\t\tif len(split) == 2 {\n\t\t\tsource = split[0]\n\t\t\tdest = split[1]\n\t\t}\n\n\t\tvolumes = append(volumes, VolumeBind{\n\t\t\tSource:      source,\n\t\t\tDestination: dest,\n\t\t})\n\t}\n\n\treturn volumes\n}\n"
  },
  {
    "path": "modules/docker/inspect_test.go",
    "content": "package docker\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/gruntwork-io/terratest/modules/shell\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst dockerInspectTestImage = \"nginx:1.17-alpine\"\n\nfunc TestInspect(t *testing.T) {\n\tt.Parallel()\n\n\t// append timestamp to container name to allow running tests in parallel\n\tname := \"inspect-test-\" + random.UniqueId()\n\n\t// running the container detached to allow inspection while it is running\n\toptions := &RunOptions{\n\t\tDetach: true,\n\t\tName:   name,\n\t}\n\n\tid := RunAndGetID(t, dockerInspectTestImage, options)\n\tdefer removeContainer(t, id)\n\n\tc := Inspect(t, id)\n\n\trequire.Equal(t, id, c.ID)\n\trequire.Equal(t, name, c.Name)\n\trequire.IsType(t, time.Time{}, c.Created)\n\trequire.Equal(t, true, c.Running)\n}\n\nfunc TestInspectWithExposedPort(t *testing.T) {\n\tt.Parallel()\n\n\t// choosing an unique high port to avoid conflict on test machines\n\tport := 13031\n\n\toptions := &RunOptions{\n\t\tDetach:       true,\n\t\tOtherOptions: []string{fmt.Sprintf(\"-p=%d:80\", port)},\n\t}\n\n\tid := RunAndGetID(t, dockerInspectTestImage, options)\n\tdefer removeContainer(t, id)\n\n\tc := Inspect(t, id)\n\n\trequire.NotEmptyf(t, c.Ports, \"Container's exposed ports should not be empty\")\n\trequire.EqualValues(t, 80, c.Ports[0].ContainerPort)\n\trequire.EqualValues(t, port, c.Ports[0].HostPort)\n}\n\nfunc TestInspectWithRandomExposedPort(t *testing.T) {\n\tt.Parallel()\n\n\tvar expectedPort uint16 = 80\n\tvar unexpectedPort uint16 = 1234\n\toptions := &RunOptions{\n\t\tDetach:       true,\n\t\tOtherOptions: []string{\"-P\"},\n\t}\n\n\tid := RunAndGetID(t, dockerInspectTestImage, options)\n\tdefer removeContainer(t, id)\n\n\tc := Inspect(t, id)\n\n\trequire.NotEmptyf(t, c.Ports, \"Container's exposed ports should not be empty\")\n\trequire.NotEqualf(t, uint16(0), c.GetExposedHostPort(expectedPort), fmt.Sprintf(\"There are no exposed port %d!\", expectedPort))\n\trequire.Equalf(t, uint16(0), c.GetExposedHostPort(unexpectedPort), fmt.Sprintf(\"There is an unexpected exposed port %d!\", unexpectedPort))\n}\n\nfunc TestInspectWithHostVolume(t *testing.T) {\n\tt.Parallel()\n\n\tc := runWithVolume(t, \"/tmp:/foo/bar\")\n\n\trequire.NotEmptyf(t, c.Binds, \"Container's host volumes should not be empty\")\n\trequire.Equal(t, \"/tmp\", c.Binds[0].Source)\n\trequire.Equal(t, \"/foo/bar\", c.Binds[0].Destination)\n}\n\nfunc TestInspectWithAnonymousVolume(t *testing.T) {\n\tt.Parallel()\n\n\tc := runWithVolume(t, \"/foo/bar\")\n\n\trequire.Empty(t, c.Binds, \"Container's host volumes be empty when using an anonymous volume\")\n}\n\nfunc TestInspectWithNamedVolume(t *testing.T) {\n\tt.Parallel()\n\n\tc := runWithVolume(t, \"foobar:/foo/bar\")\n\n\trequire.NotEmptyf(t, c.Binds, \"Container's host volumes should not be empty\")\n\trequire.Equal(t, \"foobar\", c.Binds[0].Source)\n\trequire.Equal(t, \"/foo/bar\", c.Binds[0].Destination)\n}\n\nfunc TestInspectWithInvalidContainerID(t *testing.T) {\n\tt.Parallel()\n\n\t_, err := InspectE(t, \"This is not a valid container ID\")\n\trequire.Error(t, err)\n}\n\nfunc TestInspectWithUnknownContainerID(t *testing.T) {\n\tt.Parallel()\n\n\t_, err := InspectE(t, \"abcde123456\")\n\trequire.Error(t, err)\n}\n\nfunc TestInspectReturnsCorrectHealthCheckWhenStarting(t *testing.T) {\n\tt.Parallel()\n\n\tc := runWithHealthCheck(t, \"service nginx status\", time.Second, 0)\n\n\trequire.Equal(t, \"starting\", c.Health.Status)\n\trequire.Equal(t, uint8(0), c.Health.FailingStreak)\n\trequire.Emptyf(t, c.Health.Log, \"Mising log of health check runs\")\n}\n\nfunc TestInspectReturnsCorrectHealthCheckWhenUnhealthy(t *testing.T) {\n\tt.Parallel()\n\n\tc := runWithHealthCheck(t, \"service nginx status\", time.Second, 5*time.Second)\n\n\trequire.Equal(t, \"unhealthy\", c.Health.Status)\n\trequire.NotEqual(t, uint8(0), c.Health.FailingStreak)\n\trequire.NotEmptyf(t, c.Health.Log, \"Mising log of health check runs\")\n\trequire.Equal(t, uint8(0x7f), c.Health.Log[0].ExitCode)\n\trequire.Equal(t, \"/bin/sh: service nginx status: not found\\n\", c.Health.Log[0].Output)\n}\n\nfunc runWithHealthCheck(t *testing.T, check string, frequency time.Duration, delay time.Duration) *ContainerInspect {\n\t// append timestamp to container name to allow running tests in parallel\n\tname := \"inspect-test-\" + random.UniqueId()\n\n\t// running the container detached to allow inspection while it is running\n\toptions := &RunOptions{\n\t\tDetach: true,\n\t\tName:   name,\n\t\tOtherOptions: []string{\n\t\t\tfmt.Sprintf(\"--health-cmd='%s'\", check),\n\t\t\tfmt.Sprintf(\"--health-interval=%s\", frequency),\n\t\t},\n\t}\n\n\tid := RunAndGetID(t, dockerInspectTestImage, options)\n\tdefer removeContainer(t, id)\n\n\ttime.Sleep(delay)\n\n\treturn Inspect(t, id)\n}\n\nfunc runWithVolume(t *testing.T, volume string) *ContainerInspect {\n\toptions := &RunOptions{\n\t\tDetach:  true,\n\t\tVolumes: []string{volume},\n\t}\n\n\tid := RunAndGetID(t, dockerInspectTestImage, options)\n\tdefer removeContainer(t, id)\n\n\treturn Inspect(t, id)\n}\n\nfunc removeContainer(t *testing.T, id string) {\n\tcmd := shell.Command{\n\t\tCommand: \"docker\",\n\t\tArgs:    []string{\"container\", \"rm\", \"--force\", id},\n\t}\n\n\tshell.RunCommand(t, cmd)\n}\n"
  },
  {
    "path": "modules/docker/push.go",
    "content": "package docker\n\nimport (\n\t\"github.com/gruntwork-io/terratest/modules/logger\"\n\t\"github.com/gruntwork-io/terratest/modules/shell\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// Push runs the 'docker push' command to push the given tag. This will fail the test if there are any errors.\nfunc Push(t testing.TestingT, logger *logger.Logger, tag string) {\n\trequire.NoError(t, PushE(t, logger, tag))\n}\n\n// PushE runs the 'docker push' command to push the given tag.\nfunc PushE(t testing.TestingT, logger *logger.Logger, tag string) error {\n\tlogger.Logf(t, \"Running 'docker push' for tag %s\", tag)\n\n\tcmd := shell.Command{\n\t\tCommand: \"docker\",\n\t\tArgs:    []string{\"push\", tag},\n\t\tLogger:  logger,\n\t}\n\treturn shell.RunCommandE(t, cmd)\n}\n"
  },
  {
    "path": "modules/docker/run.go",
    "content": "package docker\n\nimport (\n\t\"github.com/gruntwork-io/terratest/modules/logger\"\n\t\"github.com/gruntwork-io/terratest/modules/shell\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// RunOptions defines options that can be passed to the 'docker run' command.\ntype RunOptions struct {\n\t// Override the default COMMAND of the Docker image\n\tCommand []string\n\n\t// If set to true, pass the --detach flag to 'docker run' to run the container in the background\n\tDetach bool\n\n\t// Override the default ENTRYPOINT of the Docker image\n\tEntrypoint string\n\n\t// Set environment variables\n\tEnvironmentVariables []string\n\n\t// If set to true, pass the --init flag to 'docker run' to run an init inside the container that forwards signals\n\t// and reaps processes\n\tInit bool\n\n\t// Assign a name to the container\n\tName string\n\n\t// Set platform\n\tPlatform string\n\n\t// If set to true, pass the --privileged flag to 'docker run' to give extended privileges to the container\n\tPrivileged bool\n\n\t// If set to true, pass the --rm flag to 'docker run' to automatically remove the container when it exits\n\tRemove bool\n\n\t// If set to true, pass the -tty flag to 'docker run' to allocate a pseudo-TTY\n\tTty bool\n\n\t// Username or UID\n\tUser string\n\n\t// Bind mount these volume(s) when running the container\n\tVolumes []string\n\n\t// Custom CLI options that will be passed as-is to the 'docker run' command. This is an \"escape hatch\" that allows\n\t// Terratest to not have to support every single command-line option offered by the 'docker run' command, and\n\t// solely focus on the most important ones.\n\tOtherOptions []string\n\n\t// Set a logger that should be used. See the logger package for more info.\n\tLogger *logger.Logger\n}\n\n// Run runs the 'docker run' command on the given image with the given options and return stdout/stderr. This method\n// fails the test if there are any errors.\nfunc Run(t testing.TestingT, image string, options *RunOptions) string {\n\tout, err := RunE(t, image, options)\n\trequire.NoError(t, err)\n\treturn out\n}\n\n// RunE runs the 'docker run' command on the given image with the given options and return stdout/stderr, or any error.\nfunc RunE(t testing.TestingT, image string, options *RunOptions) (string, error) {\n\toptions.Logger.Logf(t, \"Running 'docker run' on image '%s'\", image)\n\n\targs, err := formatDockerRunArgs(image, options)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tcmd := shell.Command{\n\t\tCommand: \"docker\",\n\t\tArgs:    args,\n\t\tLogger:  options.Logger,\n\t}\n\n\treturn shell.RunCommandAndGetOutputE(t, cmd)\n}\n\n// RunAndGetID runs the 'docker run' command on the given image with the given options and returns the container ID\n// that is returned in stdout. This method fails the test if there are any errors.\nfunc RunAndGetID(t testing.TestingT, image string, options *RunOptions) string {\n\tout, err := RunAndGetIDE(t, image, options)\n\trequire.NoError(t, err)\n\treturn out\n}\n\n// RunAndGetIDE runs the 'docker run' command on the given image with the given options and returns the container ID\n// that is returned in stdout, or any error.\nfunc RunAndGetIDE(t testing.TestingT, image string, options *RunOptions) (string, error) {\n\toptions.Logger.Logf(t, \"Running 'docker run' on image '%s', returning stdout\", image)\n\n\targs, err := formatDockerRunArgs(image, options)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tcmd := shell.Command{\n\t\tCommand: \"docker\",\n\t\tArgs:    args,\n\t\tLogger:  options.Logger,\n\t}\n\n\treturn shell.RunCommandAndGetStdOutE(t, cmd)\n}\n\n// formatDockerRunArgs formats the arguments for the 'docker run' command.\nfunc formatDockerRunArgs(image string, options *RunOptions) ([]string, error) {\n\targs := []string{\"run\"}\n\n\tif options.Detach {\n\t\targs = append(args, \"--detach\")\n\t}\n\n\tif options.Entrypoint != \"\" {\n\t\targs = append(args, \"--entrypoint\", options.Entrypoint)\n\t}\n\n\tfor _, envVar := range options.EnvironmentVariables {\n\t\targs = append(args, \"--env\", envVar)\n\t}\n\n\tif options.Init {\n\t\targs = append(args, \"--init\")\n\t}\n\n\tif options.Name != \"\" {\n\t\targs = append(args, \"--name\", options.Name)\n\t}\n\n\tif options.Platform != \"\" {\n\t\targs = append(args, \"--platform\", options.Platform)\n\t}\n\n\tif options.Privileged {\n\t\targs = append(args, \"--privileged\")\n\t}\n\n\tif options.Remove {\n\t\targs = append(args, \"--rm\")\n\t}\n\n\tif options.Tty {\n\t\targs = append(args, \"--tty\")\n\t}\n\n\tif options.User != \"\" {\n\t\targs = append(args, \"--user\", options.User)\n\t}\n\n\tfor _, volume := range options.Volumes {\n\t\targs = append(args, \"--volume\", volume)\n\t}\n\n\targs = append(args, options.OtherOptions...)\n\n\targs = append(args, image)\n\n\targs = append(args, options.Command...)\n\n\treturn args, nil\n}\n"
  },
  {
    "path": "modules/docker/run_test.go",
    "content": "package docker\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestRun(t *testing.T) {\n\tt.Parallel()\n\n\toptions := &RunOptions{\n\t\tCommand:              []string{\"-c\", `echo \"Hello, $NAME!\"`},\n\t\tEntrypoint:           \"sh\",\n\t\tEnvironmentVariables: []string{\"NAME=World\"},\n\t\tRemove:               true,\n\t}\n\n\tout := Run(t, \"alpine:3.7\", options)\n\trequire.Contains(t, out, \"Hello, World!\")\n}\n"
  },
  {
    "path": "modules/docker/stop.go",
    "content": "package docker\n\nimport (\n\t\"strconv\"\n\n\t\"github.com/gruntwork-io/terratest/modules/logger\"\n\t\"github.com/gruntwork-io/terratest/modules/shell\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// StopOptions defines the options that can be passed to the 'docker stop' command\ntype StopOptions struct {\n\t// Seconds to wait for stop before killing the container (default 10)\n\tTime int\n\n\t// Set a logger that should be used. See the logger package for more info.\n\tLogger *logger.Logger\n}\n\n// Stop runs the 'docker stop' command for the given containers and return the stdout/stderr. This method fails\n// the test if there are any errors\nfunc Stop(t testing.TestingT, containers []string, options *StopOptions) string {\n\tout, err := StopE(t, containers, options)\n\trequire.NoError(t, err)\n\treturn out\n}\n\n// StopE runs the 'docker stop' command for the given containers and returns any errors.\nfunc StopE(t testing.TestingT, containers []string, options *StopOptions) (string, error) {\n\toptions.Logger.Logf(t, \"Running 'docker stop' on containers '%s'\", containers)\n\n\targs, err := formatDockerStopArgs(containers, options)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tcmd := shell.Command{\n\t\tCommand: \"docker\",\n\t\tArgs:    args,\n\t\tLogger:  options.Logger,\n\t}\n\n\treturn shell.RunCommandAndGetOutputE(t, cmd)\n\n}\n\n// formatDockerStopArgs formats the arguments for the 'docker stop' command\nfunc formatDockerStopArgs(containers []string, options *StopOptions) ([]string, error) {\n\targs := []string{\"stop\"}\n\n\tif options.Time != 0 {\n\t\targs = append(args, \"--time\", strconv.Itoa(options.Time))\n\t}\n\n\targs = append(args, containers...)\n\n\treturn args, nil\n}\n"
  },
  {
    "path": "modules/docker/stop_test.go",
    "content": "package docker\n\nimport (\n\t\"crypto/tls\"\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\thttp_helper \"github.com/gruntwork-io/terratest/modules/http-helper\"\n\t\"github.com/gruntwork-io/terratest/modules/shell\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestStop(t *testing.T) {\n\tt.Parallel()\n\n\t// appending timestamp to container name to run tests in parallel\n\tname := \"test-nginx\" + strconv.FormatInt(time.Now().UnixNano(), 10)\n\n\t// choosing a unique port since 80 may not fly well on test machines\n\tport := \"13030\"\n\thost := GetDockerHost()\n\n\ttestURL := fmt.Sprintf(\"http://%s:%s\", host, port)\n\n\t// for testing the stopping of a docker container\n\t// we got to run a container first and then stop it\n\trunOpts := &RunOptions{\n\t\tDetach:       true,\n\t\tName:         name,\n\t\tRemove:       true,\n\t\tOtherOptions: []string{\"-p\", port + \":80\"},\n\t}\n\tRun(t, \"nginx:1.17-alpine\", runOpts)\n\n\t// verify nginx is running\n\thttp_helper.HttpGetWithRetryWithCustomValidation(t, testURL, &tls.Config{}, 60, 2*time.Second, verifyNginxIsUp)\n\n\t// try to stop it now\n\tout := Stop(t, []string{name}, &StopOptions{})\n\trequire.Contains(t, out, name)\n\n\t// verify nginx is down\n\t// run a docker ps with name filter\n\tcommand := shell.Command{\n\t\tCommand: \"docker\",\n\t\tArgs:    []string{\"ps\", \"-q\", \"--filter\", \"name=\" + name},\n\t}\n\toutput := shell.RunCommandAndGetStdOut(t, command)\n\trequire.Empty(t, output)\n}\n\nfunc verifyNginxIsUp(statusCode int, body string) bool {\n\treturn statusCode == 200 && strings.Contains(body, \"nginx!\")\n}\n"
  },
  {
    "path": "modules/environment/environment.go",
    "content": "// Package environment provides utility functions for interacting with the OS environment (e.g environment variables).\npackage environment\n"
  },
  {
    "path": "modules/environment/envvar.go",
    "content": "package environment\n\nimport (\n\t\"os\"\n\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// GetFirstNonEmptyEnvVarOrFatal returns the first non-empty environment variable from envVarNames, or throws a fatal\nfunc GetFirstNonEmptyEnvVarOrFatal(t testing.TestingT, envVarNames []string) string {\n\tvalue := GetFirstNonEmptyEnvVarOrEmptyString(t, envVarNames)\n\tif value == \"\" {\n\t\tt.Fatalf(\"All of the following env vars %v are empty. At least one must be non-empty.\", envVarNames)\n\t}\n\n\treturn value\n}\n\n// GetFirstNonEmptyEnvVarOrEmptyString returns the first non-empty environment variable from envVarNames, or returns the\n// empty string\nfunc GetFirstNonEmptyEnvVarOrEmptyString(t testing.TestingT, envVarNames []string) string {\n\tfor _, name := range envVarNames {\n\t\tif value := os.Getenv(name); value != \"\" {\n\t\t\treturn value\n\t\t}\n\t}\n\n\treturn \"\"\n}\n\n// RequireEnvVar fails the test if the specified environment variable is not defined or is blank.\nfunc RequireEnvVar(t testing.TestingT, envVarName string) {\n\trequire.NotEmptyf(t, os.Getenv(envVarName), \"Environment variable %s must be set for this test.\", envVarName)\n}\n"
  },
  {
    "path": "modules/environment/envvar_test.go",
    "content": "package environment_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/environment\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\n// MockT is used to test that the function under test will fail the test under certain circumstances.\ntype MockT struct {\n\tFailed bool\n}\n\nfunc (t *MockT) Fail() {\n\tt.Failed = true\n}\n\nfunc (t *MockT) FailNow() {\n\tt.Failed = true\n}\n\nfunc (t *MockT) Error(args ...any) {\n\tt.Failed = true\n}\n\nfunc (t *MockT) Errorf(format string, args ...any) {\n\tt.Failed = true\n}\n\nfunc (t *MockT) Fatal(args ...any) {\n\tt.Failed = true\n}\n\nfunc (t *MockT) Fatalf(format string, args ...any) {\n\tt.Failed = true\n}\n\nfunc (t *MockT) Name() string {\n\treturn \"mockT\"\n}\n\n// End MockT\n\nvar envvarList = []string{\n\t\"TERRATEST_TEST_ENVIRONMENT\",\n\t\"TERRATESTTESTENVIRONMENT\",\n\t\"TERRATESTENVIRONMENT\",\n}\n\n//nolint:paralleltest // These tests manipulate env vars and cannot run in parallel.\nfunc TestGetFirstNonEmptyEnvVarOrEmptyStringChecksInOrder(t *testing.T) {\n\tt.Setenv(\"TERRATESTTESTENVIRONMENT\", \"test\")\n\tt.Setenv(\"TERRATESTENVIRONMENT\", \"circleCI\")\n\n\tvalue := environment.GetFirstNonEmptyEnvVarOrEmptyString(t, envvarList)\n\tassert.Equal(t, \"test\", value)\n}\n\n//nolint:paralleltest // These tests manipulate env vars and cannot run in parallel.\nfunc TestGetFirstNonEmptyEnvVarOrEmptyStringReturnsEmpty(t *testing.T) {\n\tvalue := environment.GetFirstNonEmptyEnvVarOrEmptyString(t, envvarList)\n\tassert.Empty(t, value)\n}\n\n//nolint:paralleltest // These tests manipulate env vars and cannot run in parallel.\nfunc TestRequireEnvVarFails(t *testing.T) {\n\tenvVarName := \"TERRATESTTESTENVIRONMENT\"\n\tmockT := new(MockT)\n\n\t// Make sure the check fails when env var is not set\n\tenvironment.RequireEnvVar(mockT, envVarName)\n\tassert.True(t, mockT.Failed)\n}\n\n//nolint:paralleltest // These tests manipulate env vars and cannot run in parallel.\nfunc TestRequireEnvVarPasses(t *testing.T) {\n\tenvVarName := \"TERRATESTTESTENVIRONMENT\"\n\n\t// Make sure the check passes when env var is set\n\tt.Setenv(envVarName, \"test\")\n\n\tenvironment.RequireEnvVar(t, envVarName)\n}\n"
  },
  {
    "path": "modules/files/errors.go",
    "content": "package files\n\nimport \"fmt\"\n\n// DirNotFoundError is an error that occurs if a directory doesn't exist\ntype DirNotFoundError struct {\n\tDirectory string\n}\n\nfunc (err DirNotFoundError) Error() string {\n\treturn fmt.Sprintf(\"Directory was not found: \\\"%s\\\"\", err.Directory)\n}\n"
  },
  {
    "path": "modules/files/files.go",
    "content": "// Package files allows to interact with files on a file system.\npackage files\n\nimport (\n\t\"errors\"\n\t\"io/fs\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/mattn/go-zglob\"\n)\n\nconst defaultDirPermissions = 0o755\n\n// FileExists returns true if the given file exists.\nfunc FileExists(path string) bool {\n\t_, err := os.Stat(path)\n\treturn err == nil\n}\n\n// FileExistsE returns true if the given file exists\n// It will return an error if os.Stat error is not an ErrNotExist\nfunc FileExistsE(path string) (bool, error) {\n\t_, err := os.Stat(path)\n\tif err != nil && !errors.Is(err, fs.ErrNotExist) {\n\t\treturn false, err\n\t}\n\n\treturn err == nil, nil\n}\n\n// IsExistingFile returns true if the path exists and is a file.\nfunc IsExistingFile(path string) bool {\n\tfileInfo, err := os.Stat(path)\n\treturn err == nil && !fileInfo.IsDir()\n}\n\n// IsExistingDir returns true if the path exists and is a directory\nfunc IsExistingDir(path string) bool {\n\tfileInfo, err := os.Stat(path)\n\treturn err == nil && fileInfo.IsDir()\n}\n\n// CopyTerraformFolderToDest creates a copy of the given folder and all its contents in a specified folder with a unique name and the given prefix.\n// This is useful when running multiple tests in parallel against the same set of Terraform files to ensure the\n// tests don't overwrite each other's .terraform working directory and terraform.tfstate files. This method returns\n// the path to the dest folder with the copied contents. Hidden files and folders (with the exception of the `.terraform-version` files used\n// by the [mise tool](https://github.com/jdx/mise) and `.terraform.lock.hcl` used by Terraform to lock providers versions), Terraform state\n// files, and terraform.tfvars files are not copied to this temp folder, as you typically don't want them interfering with your tests.\n// This method is useful when running through a build tool so the files are copied to a destination that is cleaned on each run of the pipeline.\nfunc CopyTerraformFolderToDest(folderPath string, destRootFolder string, tempFolderPrefix string) (string, error) {\n\tfilter := func(path string) bool {\n\t\tif PathIsTerraformVersionFile(path) || PathIsTerraformLockFile(path) {\n\t\t\treturn true\n\t\t}\n\n\t\tif PathContainsHiddenFileOrFolder(path) || PathContainsTerraformStateOrVars(path) {\n\t\t\treturn false\n\t\t}\n\n\t\treturn true\n\t}\n\n\tdestFolder, err := CopyFolderToDest(folderPath, destRootFolder, tempFolderPrefix, filter)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn destFolder, nil\n}\n\n// CopyTerraformFolderToTemp calls CopyTerraformFolderToDest, passing os.TempDir() as the root destination folder.\nfunc CopyTerraformFolderToTemp(folderPath string, tempFolderPrefix string) (string, error) {\n\treturn CopyTerraformFolderToDest(folderPath, os.TempDir(), tempFolderPrefix)\n}\n\n// CopyTerragruntFolderToDest creates a copy of the given folder and all its contents in a specified folder with a unique name and the given prefix.\n// Since terragrunt uses tfvars files to specify modules, they are copied to the directory as well.\n// Terraform state files are excluded as well as .terragrunt-cache to avoid overwriting contents.\nfunc CopyTerragruntFolderToDest(folderPath string, destRootFolder string, tempFolderPrefix string) (string, error) {\n\tfilter := func(path string) bool {\n\t\treturn !PathContainsHiddenFileOrFolder(path) && !PathContainsTerraformState(path)\n\t}\n\n\tdestFolder, err := CopyFolderToDest(folderPath, destRootFolder, tempFolderPrefix, filter)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn destFolder, nil\n}\n\n// CopyTerragruntFolderToTemp calls CopyTerragruntFolderToDest, passing os.TempDir() as the root destination folder.\nfunc CopyTerragruntFolderToTemp(folderPath string, tempFolderPrefix string) (string, error) {\n\treturn CopyTerragruntFolderToDest(folderPath, os.TempDir(), tempFolderPrefix)\n}\n\n// CopyFolderToDest creates a copy of the given folder and all its filtered contents in a temp folder\n// with a unique name and the given prefix.\nfunc CopyFolderToDest(folderPath string, destRootFolder string, tempFolderPrefix string, filter func(path string) bool) (string, error) {\n\tdestRootExists, err := FileExistsE(destRootFolder)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif !destRootExists {\n\t\treturn \"\", DirNotFoundError{Directory: destRootFolder}\n\t}\n\n\texists, err := FileExistsE(folderPath)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif !exists {\n\t\treturn \"\", DirNotFoundError{Directory: folderPath}\n\t}\n\n\ttmpDir, err := os.MkdirTemp(destRootFolder, tempFolderPrefix)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// Inside of the temp folder, we create a subfolder that preserves the name of the folder we're copying from.\n\tabsFolderPath, err := filepath.Abs(folderPath)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tfolderName := filepath.Base(absFolderPath)\n\tdestFolder := filepath.Join(tmpDir, folderName)\n\n\tif err := os.MkdirAll(destFolder, defaultDirPermissions); err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif err := CopyFolderContentsWithFilter(folderPath, destFolder, filter); err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn destFolder, nil\n}\n\n// CopyFolderToTemp calls CopyFolderToDest, passing os.TempDir() as the root destination folder.\nfunc CopyFolderToTemp(folderPath string, tempFolderPrefix string, filter func(path string) bool) (string, error) {\n\treturn CopyFolderToDest(folderPath, os.TempDir(), tempFolderPrefix, filter)\n}\n\n// CopyFolderContents copies all the files and folders within the given source folder to the destination folder.\nfunc CopyFolderContents(source string, destination string) error {\n\treturn CopyFolderContentsWithFilter(source, destination, func(path string) bool {\n\t\treturn true\n\t})\n}\n\n// CopyFolderContentsWithFilter copies the files and folders within the given source folder that pass the given filter (return true) to the\n// destination folder.\nfunc CopyFolderContentsWithFilter(source string, destination string, filter func(path string) bool) error {\n\tfiles, err := os.ReadDir(source)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfor _, file := range files {\n\t\tsrc := filepath.Join(source, file.Name())\n\t\tdest := filepath.Join(destination, file.Name())\n\t\tf, _ := file.Info()\n\n\t\tif !filter(src) {\n\t\t\tcontinue\n\t\t}\n\n\t\tswitch {\n\t\tcase file.IsDir():\n\t\t\tif err := os.MkdirAll(dest, f.Mode()); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif err := CopyFolderContentsWithFilter(src, dest, filter); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\tcase isSymLink(f):\n\t\t\tif err := copySymlink(src, dest); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\tdefault:\n\t\t\tif err := CopyFile(src, dest); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// PathContainsTerraformStateOrVars returns true if the path corresponds to a Terraform state file or .tfvars/.tfvars.json file.\nfunc PathContainsTerraformStateOrVars(path string) bool {\n\tfilename := filepath.Base(path)\n\treturn filename == \"terraform.tfstate\" || filename == \"terraform.tfstate.backup\" || filename == \"terraform.tfvars\" || filename == \"terraform.tfvars.json\"\n}\n\n// PathContainsTerraformState returns true if the path corresponds to a Terraform state file.\nfunc PathContainsTerraformState(path string) bool {\n\tfilename := filepath.Base(path)\n\treturn filename == \"terraform.tfstate\" || filename == \"terraform.tfstate.backup\"\n}\n\n// PathContainsHiddenFileOrFolder returns true if the given path contains a hidden file or folder.\nfunc PathContainsHiddenFileOrFolder(path string) bool {\n\tfor pathPart := range strings.SplitSeq(path, string(filepath.Separator)) {\n\t\tif strings.HasPrefix(pathPart, \".\") && pathPart != \".\" && pathPart != \"..\" {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// PathIsTerraformVersionFile returns true if the given path is the special '.terraform-version' file used by the [mise](https://github.com/jdx/mise) tool.\nfunc PathIsTerraformVersionFile(path string) bool {\n\treturn filepath.Base(path) == \".terraform-version\"\n}\n\n// PathIsTerraformLockFile return true if the given path is the special '.terraform.lock.hcl' file used by Terraform to lock providers versions\nfunc PathIsTerraformLockFile(path string) bool {\n\treturn filepath.Base(path) == \".terraform.lock.hcl\"\n}\n\n// CopyFile copies a file from source to destination.\nfunc CopyFile(source string, destination string) error {\n\tcontents, err := os.ReadFile(source)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn WriteFileWithSamePermissions(source, destination, contents)\n}\n\n// WriteFileWithSamePermissions writes a file to the given destination with the given contents using the same permissions as the file at source.\nfunc WriteFileWithSamePermissions(source string, destination string, contents []byte) error {\n\tfileInfo, err := os.Stat(source)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn os.WriteFile(destination, contents, fileInfo.Mode())\n}\n\n// isSymLink returns true if the given file is a symbolic link\n// Per https://stackoverflow.com/a/18062079/2308858\nfunc isSymLink(file os.FileInfo) bool {\n\treturn file.Mode()&os.ModeSymlink != 0\n}\n\n// copySymlink copies the source symbolic link to the given destination.\nfunc copySymlink(source string, destination string) error {\n\tsymlinkPath, err := os.Readlink(source)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn os.Symlink(symlinkPath, destination)\n}\n\n// FindTerraformSourceFilesInDir given a directory path, finds all the terraform source files contained in it. This will\n// recursively search subdirectories, but will ignore any hidden files (which in turn ignores terraform data dirs like\n// .terraform folder).\nfunc FindTerraformSourceFilesInDir(dirPath string) ([]string, error) {\n\tpattern := dirPath + \"/**/*.tf\"\n\n\tmatches, err := zglob.Glob(pattern)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\ttfFiles := []string{}\n\n\tfor _, match := range matches {\n\t\t// Don't include hidden .terraform directories when finding paths to validate\n\t\tif !PathContainsHiddenFileOrFolder(match) {\n\t\t\ttfFiles = append(tfFiles, match)\n\t\t}\n\t}\n\n\treturn tfFiles, nil\n}\n"
  },
  {
    "path": "modules/files/files_test.go",
    "content": "package files_test\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/files\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst copyFolderContentsFixtureRoot = \"../../test/fixtures/copy-folder-contents\"\n\nfunc TestFileExists(t *testing.T) {\n\tt.Parallel()\n\n\tcurrentFile, err := filepath.Abs(os.Args[0])\n\trequire.NoError(t, err)\n\n\tassert.True(t, files.FileExists(currentFile))\n\tassert.False(t, files.FileExists(\"/not/a/real/path\"))\n}\n\nfunc TestIsExistingFile(t *testing.T) {\n\tt.Parallel()\n\n\tcurrentFile, err := filepath.Abs(os.Args[0])\n\trequire.NoError(t, err)\n\n\tcurrentFileDir := filepath.Dir(currentFile)\n\n\tassert.True(t, files.IsExistingFile(currentFile))\n\tassert.False(t, files.IsExistingFile(\"/not/a/real/path\"))\n\tassert.False(t, files.IsExistingFile(currentFileDir))\n}\n\nfunc TestIsExistingDir(t *testing.T) {\n\tt.Parallel()\n\n\tcurrentFile, err := filepath.Abs(os.Args[0])\n\trequire.NoError(t, err)\n\n\tcurrentFileDir := filepath.Dir(currentFile)\n\n\tassert.False(t, files.IsExistingDir(currentFile))\n\tassert.False(t, files.IsExistingDir(\"/not/a/real/path\"))\n\tassert.True(t, files.IsExistingDir(currentFileDir))\n}\n\nfunc TestCopyFolderToDest(t *testing.T) {\n\tt.Parallel()\n\n\ttempFolderPrefix := \"someprefix\"\n\tdestFolder := os.TempDir()\n\ttmpDir := t.TempDir()\n\n\tfilter := func(path string) bool {\n\t\treturn !files.PathContainsHiddenFileOrFolder(path) && !files.PathContainsTerraformState(path)\n\t}\n\n\tfolder, err := files.CopyFolderToDest(\"/not/a/real/path\", destFolder, tempFolderPrefix, filter)\n\trequire.Error(t, err)\n\tassert.False(t, files.FileExists(folder))\n\n\tfolder, err = files.CopyFolderToDest(tmpDir, destFolder, tempFolderPrefix, filter)\n\tassert.DirExists(t, folder)\n\tassert.NoError(t, err)\n}\n\nfunc TestCopyFolderContents(t *testing.T) {\n\tt.Parallel()\n\n\toriginalDir := filepath.Join(copyFolderContentsFixtureRoot, \"original\")\n\texpectedDir := filepath.Join(copyFolderContentsFixtureRoot, \"full-copy\")\n\ttmpDir := t.TempDir()\n\n\terr := files.CopyFolderContents(originalDir, tmpDir)\n\trequire.NoError(t, err)\n\n\trequireDirectoriesEqual(t, expectedDir, tmpDir)\n}\n\nfunc TestCopyFolderContentsWithHiddenFilesFilter(t *testing.T) {\n\tt.Parallel()\n\n\toriginalDir := filepath.Join(copyFolderContentsFixtureRoot, \"original\")\n\texpectedDir := filepath.Join(copyFolderContentsFixtureRoot, \"no-hidden-files\")\n\ttmpDir := t.TempDir()\n\n\terr := files.CopyFolderContentsWithFilter(originalDir, tmpDir, func(path string) bool {\n\t\treturn !files.PathContainsHiddenFileOrFolder(path)\n\t})\n\trequire.NoError(t, err)\n\n\trequireDirectoriesEqual(t, expectedDir, tmpDir)\n}\n\n// Test copying a folder that contains symlinks\nfunc TestCopyFolderContentsWithSymLinks(t *testing.T) {\n\tt.Parallel()\n\n\toriginalDir := filepath.Join(copyFolderContentsFixtureRoot, \"symlinks\")\n\texpectedDir := filepath.Join(copyFolderContentsFixtureRoot, \"symlinks\")\n\ttmpDir := t.TempDir()\n\n\terr := files.CopyFolderContentsWithFilter(originalDir, tmpDir, func(path string) bool {\n\t\treturn !files.PathContainsHiddenFileOrFolder(path)\n\t})\n\trequire.NoError(t, err)\n\n\trequireDirectoriesEqual(t, expectedDir, tmpDir)\n}\n\n// Test copying a folder that contains symlinks that point to a non-existent file\nfunc TestCopyFolderContentsWithBrokenSymLinks(t *testing.T) {\n\tt.Parallel()\n\n\t// Creating broken symlink\n\tpathToFile := filepath.Join(copyFolderContentsFixtureRoot, \"symlinks-broken/nonexistent-folder/bar.txt\")\n\tpathToSymlink := filepath.Join(copyFolderContentsFixtureRoot, \"symlinks-broken/bar.txt\")\n\n\tdefer func() {\n\t\tif err := os.Remove(pathToSymlink); err != nil {\n\t\t\tt.Fatal(fmt.Errorf(\"failed to remove link: %w\", err))\n\t\t}\n\t}()\n\n\tif err := os.Symlink(pathToFile, pathToSymlink); err != nil {\n\t\tt.Fatal(fmt.Errorf(\"failed to create broken link for test: %w\", err))\n\t}\n\n\t// Test copying folder\n\toriginalDir := filepath.Join(copyFolderContentsFixtureRoot, \"symlinks-broken\")\n\ttmpDir := t.TempDir()\n\n\terr := files.CopyFolderContentsWithFilter(originalDir, tmpDir, func(path string) bool {\n\t\treturn !files.PathContainsHiddenFileOrFolder(path)\n\t})\n\trequire.NoError(t, err)\n\n\t// This requireDirectoriesEqual command uses GNU diff under the hood, but unfortunately we cannot instruct diff to\n\t// compare symlinks in two directories without attempting to dereference any symlinks until diff version 3.3.0.\n\t// Because many environments are still using diff < 3.3.0, we disregard this test for now.\n\t// Per https://unix.stackexchange.com/a/119406/129208\n\t// requireDirectoriesEqual(t, expectedDir, tmpDir)\n\tfmt.Println(\"Test completed without error, however due to a limitation in GNU diff < 3.3.0, directories have not been compared for equivalency.\")\n}\n\nfunc TestCopyTerraformFolderToTemp(t *testing.T) {\n\tt.Parallel()\n\n\toriginalDir := filepath.Join(copyFolderContentsFixtureRoot, \"original\")\n\texpectedDir := filepath.Join(copyFolderContentsFixtureRoot, \"no-hidden-files-no-terraform-files\")\n\n\ttmpDir, err := files.CopyTerraformFolderToTemp(originalDir, \"TestCopyTerraformFolderToTemp\")\n\trequire.NoError(t, err)\n\n\trequireDirectoriesEqual(t, expectedDir, tmpDir)\n}\n\nfunc TestCopyTerraformFolderToDest(t *testing.T) {\n\tt.Parallel()\n\n\toriginalDir := filepath.Join(copyFolderContentsFixtureRoot, \"original\")\n\texpectedDir := filepath.Join(copyFolderContentsFixtureRoot, \"no-hidden-files-no-terraform-files\")\n\tdestFolder := os.TempDir()\n\n\ttmpDir, err := files.CopyTerraformFolderToDest(originalDir, destFolder, \"TestCopyTerraformFolderToTemp\")\n\trequire.NoError(t, err)\n\n\trequireDirectoriesEqual(t, expectedDir, tmpDir)\n}\n\nfunc TestCopyTerragruntFolderToTemp(t *testing.T) {\n\tt.Parallel()\n\n\toriginalDir := filepath.Join(copyFolderContentsFixtureRoot, \"terragrunt-files\")\n\texpectedDir := filepath.Join(copyFolderContentsFixtureRoot, \"no-state-files\")\n\n\ttmpDir, err := files.CopyTerragruntFolderToTemp(originalDir, t.Name())\n\trequire.NoError(t, err)\n\n\trequireDirectoriesEqual(t, expectedDir, tmpDir)\n}\n\nfunc TestCopyTerragruntFolderToDest(t *testing.T) {\n\tt.Parallel()\n\n\toriginalDir := filepath.Join(copyFolderContentsFixtureRoot, \"terragrunt-files\")\n\texpectedDir := filepath.Join(copyFolderContentsFixtureRoot, \"no-state-files\")\n\tdestFolder := os.TempDir()\n\n\ttmpDir, err := files.CopyTerragruntFolderToDest(originalDir, destFolder, t.Name())\n\trequire.NoError(t, err)\n\n\trequireDirectoriesEqual(t, expectedDir, tmpDir)\n}\n\nfunc TestPathContainsTerraformStateOrVars(t *testing.T) {\n\tt.Parallel()\n\n\tvar data = []struct {\n\t\tdesc     string\n\t\tpath     string\n\t\tcontains bool\n\t}{\n\t\t{\"contains tfvars\", \"./folder/terraform.tfvars\", true},\n\t\t{\"contains tfvars.json\", \"./folder/hello/terraform.tfvars.json\", true},\n\t\t{\"contains state\", \"./folder/hello/helloagain/terraform.tfstate\", true},\n\t\t{\"contains state backup\", \"./folder/hey/terraform.tfstate.backup\", true},\n\t\t{\"does not contain any\", \"./folder/salut/terraform.json\", false},\n\t}\n\n\tfor _, tt := range data {\n\t\tt.Run(tt.desc, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tresult := files.PathContainsTerraformStateOrVars(tt.path)\n\t\t\tif result != tt.contains {\n\t\t\t\tif tt.contains {\n\t\t\t\t\tt.Errorf(\"Expected %s to contain Terraform related file\", tt.path)\n\t\t\t\t} else {\n\t\t\t\t\tt.Errorf(\"Expected %s to not contain Terraform related file\", tt.path)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\n// Diffing two directories to ensure they have the exact same files, contents, etc and showing exactly what's different\n// takes a lot of code. Why waste time on that when this functionality is already nicely implemented in the Unix/Linux\n// \"diff\" command? We shell out to that command at test time.\nfunc requireDirectoriesEqual(t *testing.T, folderWithExpectedContents string, folderWithActualContents string) {\n\tt.Helper()\n\n\tcmd := exec.CommandContext(t.Context(), \"diff\", \"-r\", \"-u\", folderWithExpectedContents, folderWithActualContents)\n\n\tbytes, err := cmd.Output()\n\toutput := string(bytes)\n\n\trequire.NoError(t, err, \"diff command exited with an error. This likely means the contents of %s and %s are different. Here is the output of the diff command:\\n%s\", folderWithExpectedContents, folderWithActualContents, output)\n}\n"
  },
  {
    "path": "modules/gcp/cloudbuild.go",
    "content": "package gcp\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\tcloudbuild \"cloud.google.com/go/cloudbuild/apiv1/v2\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/stretchr/testify/require\"\n\t\"google.golang.org/api/iterator\"\n\tcloudbuildpb \"google.golang.org/genproto/googleapis/devtools/cloudbuild/v1\"\n)\n\n// CreateBuild creates a new build blocking until the operation is complete.\nfunc CreateBuild(t testing.TestingT, projectID string, build *cloudbuildpb.Build) *cloudbuildpb.Build {\n\tout, err := CreateBuildE(t, projectID, build)\n\trequire.NoError(t, err)\n\treturn out\n}\n\n// CreateBuildE creates a new build blocking until the operation is complete.\nfunc CreateBuildE(t testing.TestingT, projectID string, build *cloudbuildpb.Build) (*cloudbuildpb.Build, error) {\n\tctx := context.Background()\n\n\tservice, err := NewCloudBuildServiceE(t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treq := &cloudbuildpb.CreateBuildRequest{\n\t\tProjectId: projectID,\n\t\tBuild:     build,\n\t}\n\n\top, err := service.CreateBuild(ctx, req)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"CreateBuildE.CreateBuild(%s) got error: %v\", projectID, err)\n\t}\n\n\tresp, err := op.Wait(ctx)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"CreateBuildE.Wait(%s) got error: %v\", projectID, err)\n\t}\n\n\treturn resp, nil\n}\n\n// GetBuild gets the given build.\nfunc GetBuild(t testing.TestingT, projectID string, buildID string) *cloudbuildpb.Build {\n\tout, err := GetBuildE(t, projectID, buildID)\n\trequire.NoError(t, err)\n\treturn out\n}\n\n// GetBuildE gets the given build.\nfunc GetBuildE(t testing.TestingT, projectID string, buildID string) (*cloudbuildpb.Build, error) {\n\tctx := context.Background()\n\n\tservice, err := NewCloudBuildServiceE(t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treq := &cloudbuildpb.GetBuildRequest{\n\t\tProjectId: projectID,\n\t\tId:        buildID,\n\t}\n\n\tresp, err := service.GetBuild(ctx, req)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"GetBuildE.GetBuild(%s, %s) got error: %v\", projectID, buildID, err)\n\t}\n\n\treturn resp, nil\n}\n\n// GetBuilds gets the list of builds for a given project.\nfunc GetBuilds(t testing.TestingT, projectID string) []*cloudbuildpb.Build {\n\tout, err := GetBuildsE(t, projectID)\n\trequire.NoError(t, err)\n\treturn out\n}\n\n// GetBuildsE gets the list of builds for a given project.\nfunc GetBuildsE(t testing.TestingT, projectID string) ([]*cloudbuildpb.Build, error) {\n\tctx := context.Background()\n\n\tservice, err := NewCloudBuildServiceE(t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treq := &cloudbuildpb.ListBuildsRequest{\n\t\tProjectId: projectID,\n\t}\n\n\tit := service.ListBuilds(ctx, req)\n\tbuilds := []*cloudbuildpb.Build{}\n\n\tfor {\n\t\tresp, err := it.Next()\n\t\tif err == iterator.Done {\n\t\t\tbreak\n\t\t}\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"GetBuildsE.ListBuilds(%s) got error: %v\", projectID, err)\n\t\t}\n\n\t\tbuilds = append(builds, resp)\n\t}\n\n\treturn builds, nil\n}\n\n// GetBuildsForTrigger gets a list of builds for a specific cloud build trigger.\nfunc GetBuildsForTrigger(t testing.TestingT, projectID string, triggerID string) []*cloudbuildpb.Build {\n\tout, err := GetBuildsForTriggerE(t, projectID, triggerID)\n\trequire.NoError(t, err)\n\treturn out\n}\n\n// GetBuildsForTriggerE gets a list of builds for a specific cloud build trigger.\nfunc GetBuildsForTriggerE(t testing.TestingT, projectID string, triggerID string) ([]*cloudbuildpb.Build, error) {\n\tbuilds, err := GetBuildsE(t, projectID)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"GetBuildsE.ListBuilds(%s) got error: %v\", projectID, err)\n\t}\n\n\tfilteredBuilds := []*cloudbuildpb.Build{}\n\tfor _, build := range builds {\n\t\tif build.GetBuildTriggerId() == triggerID {\n\t\t\tfilteredBuilds = append(filteredBuilds, build)\n\t\t}\n\t}\n\n\treturn filteredBuilds, nil\n}\n\n// NewCloudBuildService creates a new Cloud Build service, which is used to make Cloud Build API calls.\nfunc NewCloudBuildService(t testing.TestingT) *cloudbuild.Client {\n\tservice, err := NewCloudBuildServiceE(t)\n\trequire.NoError(t, err)\n\treturn service\n}\n\n// NewCloudBuildServiceE creates a new Cloud Build service, which is used to make Cloud Build API calls.\nfunc NewCloudBuildServiceE(t testing.TestingT) (*cloudbuild.Client, error) {\n\tctx := context.Background()\n\n\tservice, err := cloudbuild.NewClient(ctx, withOptions()...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn service, nil\n}\n"
  },
  {
    "path": "modules/gcp/cloudbuild_test.go",
    "content": "//go:build gcp\n// +build gcp\n\n// NOTE: We use build tags to differentiate GCP testing for better isolation and parallelism when executing our tests.\n\npackage gcp\n\nimport (\n\t\"archive/tar\"\n\t\"bytes\"\n\t\"compress/gzip\"\n\t\"fmt\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/logger\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/stretchr/testify/require\"\n\tcloudbuildpb \"google.golang.org/genproto/googleapis/devtools/cloudbuild/v1\"\n)\n\nfunc TestCreateBuild(t *testing.T) {\n\tt.Parallel()\n\t// This test performs the following steps:\n\t//\n\t// 1. Creates a tarball with a single Dockerfile\n\t// 2. Creates a GCS bucket\n\t// 3. Uploads the tarball to the GCS Bucket\n\t// 4. Triggers a build using the Cloud Build API\n\t// 5. Attempts to untag and delete all pushed Build images (best-effort cleanup)\n\t// 6. Deletes the GCS bucket\n\n\t// Create and add some files to the archive.\n\ttarball := createSampleAppTarball(t)\n\n\t// Create GCS bucket\n\tprojectID := GetGoogleProjectIDFromEnvVar(t)\n\tid := random.UniqueId()\n\tgsBucketName := \"cloud-build-terratest-\" + strings.ToLower(id)\n\tsampleAppPath := \"docker-example.tar.gz\"\n\timagePath := fmt.Sprintf(\"gcr.io/%s/test-image-%s\", projectID, strings.ToLower(id))\n\n\tlogger.Logf(t, \"Random values selected Bucket Name = %s\\n\", gsBucketName)\n\n\tCreateStorageBucket(t, projectID, gsBucketName, nil)\n\tdefer DeleteStorageBucket(t, gsBucketName)\n\n\t// Write the compressed archive to the storage bucket\n\tobjectURL := WriteBucketObject(t, gsBucketName, sampleAppPath, tarball, \"application/gzip\")\n\tlogger.Logf(t, \"Got URL: %s\", objectURL)\n\n\t// Create a new build\n\tbuild := &cloudbuildpb.Build{\n\t\tSource: &cloudbuildpb.Source{\n\t\t\tSource: &cloudbuildpb.Source_StorageSource{\n\t\t\t\tStorageSource: &cloudbuildpb.StorageSource{\n\t\t\t\t\tBucket: gsBucketName,\n\t\t\t\t\tObject: sampleAppPath,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tSteps: []*cloudbuildpb.BuildStep{{\n\t\t\tName: \"gcr.io/cloud-builders/docker\",\n\t\t\tArgs: []string{\"build\", \"-t\", imagePath, \".\"},\n\t\t}},\n\t\tImages: []string{imagePath},\n\t}\n\n\t// CreateBuild blocks until the build is complete\n\tb := CreateBuild(t, projectID, build)\n\n\t// Attempt to delete the pushed build images (best-effort cleanup).\n\t// Note: GCR (gcr.io) has been deprecated in favor of Artifact Registry.\n\t// The cleanup may fail due to permission changes, but this doesn't affect\n\t// the validity of the Cloud Build test itself.\n\t// We could just use the `b` struct above, but we want to explicitly test\n\t// the `GetBuild` method.\n\tb2 := GetBuild(t, projectID, b.GetId())\n\tfor _, image := range b2.GetImages() {\n\t\tif err := DeleteGCRRepoE(t, image); err != nil {\n\t\t\tlogger.Logf(t, \"Warning: Failed to delete image %s (this may be expected due to GCR deprecation): %v\", image, err)\n\t\t}\n\t}\n\n\t// Empty the storage bucket so we can delete it\n\tdefer EmptyStorageBucket(t, gsBucketName)\n}\n\nfunc createSampleAppTarball(t *testing.T) *bytes.Reader {\n\tvar buf bytes.Buffer\n\ttw := tar.NewWriter(&buf)\n\n\tfile := `FROM busybox:latest\nMAINTAINER Rob Morgan (rob@gruntwork.io)\n\t`\n\n\thdr := &tar.Header{\n\t\tName: \"Dockerfile\",\n\t\tMode: 0600,\n\t\tSize: int64(len(file)),\n\t}\n\n\terr := tw.WriteHeader(hdr)\n\trequire.NoError(t, err)\n\n\t_, werr := tw.Write([]byte(file))\n\trequire.NoError(t, werr)\n\n\tcerr := tw.Close()\n\trequire.NoError(t, cerr)\n\n\t// gzip the tar archive\n\tvar zbuf bytes.Buffer\n\tgzw := gzip.NewWriter(&zbuf)\n\t_, gwerr := gzw.Write(buf.Bytes())\n\trequire.NoError(t, gwerr)\n\n\tgcerr := gzw.Close()\n\trequire.NoError(t, gcerr)\n\n\t// return the compressed buffer\n\treturn bytes.NewReader(zbuf.Bytes())\n}\n"
  },
  {
    "path": "modules/gcp/compute.go",
    "content": "package gcp\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"path\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/gruntwork-io/terratest/modules/retry\"\n\t\"google.golang.org/api/compute/v1\"\n\n\t\"github.com/gruntwork-io/terratest/modules/logger\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"golang.org/x/oauth2\"\n\t\"golang.org/x/oauth2/google\"\n)\n\n// Corresponds to a GCP Compute Instance (https://cloud.google.com/compute/docs/instances/)\ntype Instance struct {\n\tprojectID string\n\t*compute.Instance\n}\n\n// Corresponds to a GCP Image (https://cloud.google.com/compute/docs/images)\ntype Image struct {\n\tprojectID string\n\t*compute.Image\n}\n\n// Corresponds to a GCP Zonal Instance Group (https://cloud.google.com/compute/docs/instance-groups/)\ntype ZonalInstanceGroup struct {\n\tprojectID string\n\t*compute.InstanceGroup\n}\n\n// Corresponds to a GCP Regional Instance Group (https://cloud.google.com/compute/docs/instance-groups/)\ntype RegionalInstanceGroup struct {\n\tprojectID string\n\t*compute.InstanceGroup\n}\n\ntype InstanceGroup interface {\n\tGetInstanceIds(t testing.TestingT) []string\n\tGetInstanceIdsE(t testing.TestingT) ([]string, error)\n}\n\n// FetchInstance queries GCP to return an instance of the (GCP Compute) Instance type\nfunc FetchInstance(t testing.TestingT, projectID string, name string) *Instance {\n\tinstance, err := FetchInstanceE(t, projectID, name)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\treturn instance\n}\n\n// FetchInstance queries GCP to return an instance of the (GCP Compute) Instance type\nfunc FetchInstanceE(t testing.TestingT, projectID string, name string) (*Instance, error) {\n\tlogger.Default.Logf(t, \"Getting Compute Instance %s\", name)\n\n\tctx := context.Background()\n\tservice, err := NewComputeServiceE(t)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t// If we want to fetch an Instance without knowing its Zone, we have to query GCP for all Instances in the project\n\t// and match on name.\n\tinstanceAggregatedList, err := service.Instances.AggregatedList(projectID).Context(ctx).Do()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"Instances.AggregatedList(%s) got error: %v\", projectID, err)\n\t}\n\n\tfor _, instanceList := range instanceAggregatedList.Items {\n\t\tfor _, instance := range instanceList.Instances {\n\t\t\tif name == instance.Name {\n\t\t\t\treturn &Instance{projectID, instance}, nil\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil, fmt.Errorf(\"Compute Instance %s could not be found in project %s\", name, projectID)\n}\n\n// FetchImage queries GCP to return a new instance of the (GCP Compute) Image type\nfunc FetchImage(t testing.TestingT, projectID string, name string) *Image {\n\timage, err := FetchImageE(t, projectID, name)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\treturn image\n}\n\n// FetchImage queries GCP to return a new instance of the (GCP Compute) Image type\nfunc FetchImageE(t testing.TestingT, projectID string, name string) (*Image, error) {\n\tlogger.Default.Logf(t, \"Getting Image %s\", name)\n\n\tctx := context.Background()\n\tservice, err := NewComputeServiceE(t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treq := service.Images.Get(projectID, name)\n\timage, err := req.Context(ctx).Do()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &Image{projectID, image}, nil\n}\n\n// FetchRegionalInstanceGroup queries GCP to return a new instance of the Regional Instance Group type\nfunc FetchRegionalInstanceGroup(t testing.TestingT, projectID string, region string, name string) *RegionalInstanceGroup {\n\tinstanceGroup, err := FetchRegionalInstanceGroupE(t, projectID, region, name)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\treturn instanceGroup\n}\n\n// FetchRegionalInstanceGroup queries GCP to return a new instance of the Regional Instance Group type\nfunc FetchRegionalInstanceGroupE(t testing.TestingT, projectID string, region string, name string) (*RegionalInstanceGroup, error) {\n\tlogger.Default.Logf(t, \"Getting Regional Instance Group %s\", name)\n\n\tctx := context.Background()\n\tservice, err := NewComputeServiceE(t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treq := service.RegionInstanceGroups.Get(projectID, region, name)\n\tinstanceGroup, err := req.Context(ctx).Do()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &RegionalInstanceGroup{projectID, instanceGroup}, nil\n}\n\n// FetchZonalInstanceGroup queries GCP to return a new instance of the Zonal Instance Group type\nfunc FetchZonalInstanceGroup(t testing.TestingT, projectID string, zone string, name string) *ZonalInstanceGroup {\n\tinstanceGroup, err := FetchZonalInstanceGroupE(t, projectID, zone, name)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\treturn instanceGroup\n}\n\n// FetchZonalInstanceGroupE queries GCP to return a new instance of the Zonal Instance Group type\nfunc FetchZonalInstanceGroupE(t testing.TestingT, projectID string, zone string, name string) (*ZonalInstanceGroup, error) {\n\tlogger.Default.Logf(t, \"Getting Zonal Instance Group %s\", name)\n\n\tctx := context.Background()\n\tservice, err := NewComputeServiceE(t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treq := service.InstanceGroups.Get(projectID, zone, name)\n\tinstanceGroup, err := req.Context(ctx).Do()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &ZonalInstanceGroup{projectID, instanceGroup}, nil\n}\n\n// GetPublicIP gets the public IP address of the given Compute Instance.\nfunc (i *Instance) GetPublicIp(t testing.TestingT) string {\n\tip, err := i.GetPublicIpE(t)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn ip\n}\n\n// GetPublicIpE gets the public IP address of the given Compute Instance.\nfunc (i *Instance) GetPublicIpE(t testing.TestingT) (string, error) {\n\t// If there are no accessConfigs specified, then this instance will have no external internet access:\n\t// https://cloud.google.com/compute/docs/reference/rest/v1/instances.\n\tif len(i.NetworkInterfaces[0].AccessConfigs) == 0 {\n\t\treturn \"\", fmt.Errorf(\"Attempted to get public IP of Compute Instance %s, but that Compute Instance does not have a public IP address\", i.Name)\n\t}\n\n\tip := i.NetworkInterfaces[0].AccessConfigs[0].NatIP\n\n\treturn ip, nil\n}\n\n// GetLabels returns all the tags for the given Compute Instance.\nfunc (i *Instance) GetLabels(t testing.TestingT) map[string]string {\n\treturn i.Labels\n}\n\n// GetZone returns the Zone in which the Compute Instance is located.\nfunc (i *Instance) GetZone(t testing.TestingT) string {\n\treturn ZoneUrlToZone(i.Zone)\n}\n\n// SetLabels adds the tags to the given Compute Instance.\nfunc (i *Instance) SetLabels(t testing.TestingT, labels map[string]string) {\n\terr := i.SetLabelsE(t, labels)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n\n// SetLabelsE adds the tags to the given Compute Instance.\nfunc (i *Instance) SetLabelsE(t testing.TestingT, labels map[string]string) error {\n\tlogger.Default.Logf(t, \"Adding labels to instance %s in zone %s\", i.Name, i.Zone)\n\n\tctx := context.Background()\n\tservice, err := NewComputeServiceE(t)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treq := compute.InstancesSetLabelsRequest{Labels: labels, LabelFingerprint: i.LabelFingerprint}\n\tif _, err := service.Instances.SetLabels(i.projectID, i.GetZone(t), i.Name, &req).Context(ctx).Do(); err != nil {\n\t\treturn fmt.Errorf(\"Instances.SetLabels(%s) got error: %v\", i.Name, err)\n\t}\n\n\treturn nil\n}\n\n// GetMetadata gets the given Compute Instance's metadata\nfunc (i *Instance) GetMetadata(t testing.TestingT) []*compute.MetadataItems {\n\treturn i.Metadata.Items\n}\n\n// SetMetadata sets the given Compute Instance's metadata\nfunc (i *Instance) SetMetadata(t testing.TestingT, metadata map[string]string) {\n\terr := i.SetMetadataE(t, metadata)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n\n// SetMetadataE adds the given metadata map to the existing metadata of the given Compute Instance.\nfunc (i *Instance) SetMetadataE(t testing.TestingT, metadata map[string]string) error {\n\tlogger.Default.Logf(t, \"Adding metadata to instance %s in zone %s\", i.Name, i.Zone)\n\n\tctx := context.Background()\n\tservice, err := NewInstancesServiceE(t)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tmetadataItems := newMetadata(t, i.Metadata, metadata)\n\treq := service.SetMetadata(i.projectID, i.GetZone(t), i.Name, metadataItems)\n\tif _, err := req.Context(ctx).Do(); err != nil {\n\t\treturn fmt.Errorf(\"Instances.SetMetadata(%s) got error: %v\", i.Name, err)\n\t}\n\n\treturn nil\n}\n\n// newMetadata merges new key-value pairs into existing metadata, preserving unmodified items.\nfunc newMetadata(t testing.TestingT, oldMetadata *compute.Metadata, kvs map[string]string) *compute.Metadata {\n\titemsMap := make(map[string]*string)\n\n\tif oldMetadata != nil {\n\t\tfor _, item := range oldMetadata.Items {\n\t\t\titemsMap[item.Key] = item.Value\n\t\t}\n\t}\n\n\tfor key, val := range kvs {\n\t\tv := val\n\t\titemsMap[key] = &v\n\t}\n\n\titems := make([]*compute.MetadataItems, 0, len(itemsMap))\n\tfor key, val := range itemsMap {\n\t\titems = append(items, &compute.MetadataItems{Key: key, Value: val})\n\t}\n\n\tfingerprint := \"\"\n\tif oldMetadata != nil {\n\t\tfingerprint = oldMetadata.Fingerprint\n\t}\n\n\treturn &compute.Metadata{\n\t\tFingerprint: fingerprint,\n\t\tItems:       items,\n\t}\n}\n\n// Add the given public SSH key to the Compute Instance. Users can SSH in with the given username.\nfunc (i *Instance) AddSshKey(t testing.TestingT, username string, publicKey string) {\n\terr := i.AddSshKeyE(t, username, publicKey)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n\n// Add the given public SSH key to the Compute Instance. Users can SSH in with the given username.\nfunc (i *Instance) AddSshKeyE(t testing.TestingT, username string, publicKey string) error {\n\tlogger.Default.Logf(t, \"Adding SSH Key to Compute Instance %s for username %s\\n\", i.Name, username)\n\n\t// We represent the key in the format required per GCP docs (https://cloud.google.com/compute/docs/instances/adding-removing-ssh-keys)\n\tpublicKeyFormatted := strings.TrimSpace(publicKey)\n\tsshKeyFormatted := fmt.Sprintf(\"%s:%s %s\", username, publicKeyFormatted, username)\n\n\tmetadata := map[string]string{\n\t\t\"ssh-keys\": sshKeyFormatted,\n\t}\n\n\terr := i.SetMetadataE(t, metadata)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"Failed to add SSH key to Compute Instance: %s\", err)\n\t}\n\n\treturn nil\n}\n\n// DeleteImage deletes the given Compute Image.\nfunc (i *Image) DeleteImage(t testing.TestingT) {\n\terr := i.DeleteImageE(t)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n\n// DeleteImageE deletes the given Compute Image.\nfunc (i *Image) DeleteImageE(t testing.TestingT) error {\n\tlogger.Default.Logf(t, \"Destroying Image %s\", i.Name)\n\n\tctx := context.Background()\n\tservice, err := NewComputeServiceE(t)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif _, err := service.Images.Delete(i.projectID, i.Name).Context(ctx).Do(); err != nil {\n\t\treturn fmt.Errorf(\"Images.Delete(%s) got error: %v\", i.Name, err)\n\t}\n\n\treturn nil\n}\n\n// GetInstanceIds gets the IDs of Instances in the given Instance Group.\nfunc (ig *ZonalInstanceGroup) GetInstanceIds(t testing.TestingT) []string {\n\tids, err := ig.GetInstanceIdsE(t)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn ids\n}\n\n// GetInstanceIdsE gets the IDs of Instances in the given Zonal Instance Group.\nfunc (ig *ZonalInstanceGroup) GetInstanceIdsE(t testing.TestingT) ([]string, error) {\n\tlogger.Default.Logf(t, \"Get instances for Zonal Instance Group %s\", ig.Name)\n\n\tctx := context.Background()\n\tservice, err := NewComputeServiceE(t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\trequestBody := &compute.InstanceGroupsListInstancesRequest{\n\t\tInstanceState: \"ALL\",\n\t}\n\n\tinstanceIDs := []string{}\n\tzone := ZoneUrlToZone(ig.Zone)\n\n\treq := service.InstanceGroups.ListInstances(ig.projectID, zone, ig.Name, requestBody)\n\n\terr = req.Pages(ctx, func(page *compute.InstanceGroupsListInstances) error {\n\t\tfor _, instance := range page.Items {\n\t\t\t// For some reason service.InstanceGroups.ListInstances returns us a collection\n\t\t\t// with Instance URLs and we need only the Instance ID for the next call. Use\n\t\t\t// the path functions to chop the Instance ID off the end of the URL.\n\t\t\tinstanceID := path.Base(instance.Instance)\n\t\t\tinstanceIDs = append(instanceIDs, instanceID)\n\t\t}\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"InstanceGroups.ListInstances(%s) got error: %v\", ig.Name, err)\n\t}\n\n\treturn instanceIDs, nil\n}\n\n// GetInstanceIds gets the IDs of Instances in the given Regional Instance Group.\nfunc (ig *RegionalInstanceGroup) GetInstanceIds(t testing.TestingT) []string {\n\tids, err := ig.GetInstanceIdsE(t)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn ids\n}\n\n// GetInstanceIdsE gets the IDs of Instances in the given Regional Instance Group.\nfunc (ig *RegionalInstanceGroup) GetInstanceIdsE(t testing.TestingT) ([]string, error) {\n\tlogger.Default.Logf(t, \"Get instances for Regional Instance Group %s\", ig.Name)\n\n\tctx := context.Background()\n\n\tservice, err := NewComputeServiceE(t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\trequestBody := &compute.RegionInstanceGroupsListInstancesRequest{\n\t\tInstanceState: \"ALL\",\n\t}\n\n\tinstanceIDs := []string{}\n\tregion := RegionUrlToRegion(ig.Region)\n\n\treq := service.RegionInstanceGroups.ListInstances(ig.projectID, region, ig.Name, requestBody)\n\n\terr = req.Pages(ctx, func(page *compute.RegionInstanceGroupsListInstances) error {\n\t\tfor _, instance := range page.Items {\n\t\t\t// For some reason service.InstanceGroups.ListInstances returns us a collection\n\t\t\t// with Instance URLs and we need only the Instance ID for the next call. Use\n\t\t\t// the path functions to chop the Instance ID off the end of the URL.\n\t\t\tinstanceID := path.Base(instance.Instance)\n\t\t\tinstanceIDs = append(instanceIDs, instanceID)\n\t\t}\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"InstanceGroups.ListInstances(%s) got error: %v\", ig.Name, err)\n\t}\n\n\treturn instanceIDs, nil\n}\n\n// Return a collection of Instance structs from the given Instance Group\nfunc (ig *ZonalInstanceGroup) GetInstances(t testing.TestingT, projectId string) []*Instance {\n\treturn getInstances(t, ig, projectId)\n}\n\n// Return a collection of Instance structs from the given Instance Group\nfunc (ig *ZonalInstanceGroup) GetInstancesE(t testing.TestingT, projectId string) ([]*Instance, error) {\n\treturn getInstancesE(t, ig, projectId)\n}\n\n// Return a collection of Instance structs from the given Instance Group\nfunc (ig *RegionalInstanceGroup) GetInstances(t testing.TestingT, projectId string) []*Instance {\n\treturn getInstances(t, ig, projectId)\n}\n\n// Return a collection of Instance structs from the given Instance Group\nfunc (ig *RegionalInstanceGroup) GetInstancesE(t testing.TestingT, projectId string) ([]*Instance, error) {\n\treturn getInstancesE(t, ig, projectId)\n}\n\n// getInstancesE returns a collection of Instance structs from the given Instance Group\nfunc getInstances(t testing.TestingT, ig InstanceGroup, projectId string) []*Instance {\n\tinstances, err := getInstancesE(t, ig, projectId)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\treturn instances\n}\n\n// getInstancesE returns a collection of Instance structs from the given Instance Group\nfunc getInstancesE(t testing.TestingT, ig InstanceGroup, projectId string) ([]*Instance, error) {\n\tinstanceIds, err := ig.GetInstanceIdsE(t)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"Failed to get Instance Group IDs: %s\", err)\n\t}\n\n\tvar instances []*Instance\n\n\tfor _, instanceId := range instanceIds {\n\t\tinstance, err := FetchInstanceE(t, projectId, instanceId)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"Failed to get Instance: %s\", err)\n\t\t}\n\n\t\tinstances = append(instances, instance)\n\t}\n\n\treturn instances, nil\n}\n\n// GetPublicIps returns a slice of the public IPs from the given Instance Group\nfunc (ig *ZonalInstanceGroup) GetPublicIps(t testing.TestingT, projectId string) []string {\n\treturn getPublicIps(t, ig, projectId)\n}\n\n// GetPublicIpsE returns a slice of the public IPs from the given Instance Group\nfunc (ig *ZonalInstanceGroup) GetPublicIpsE(t testing.TestingT, projectId string) ([]string, error) {\n\treturn getPublicIpsE(t, ig, projectId)\n}\n\n// GetPublicIps returns a slice of the public IPs from the given Instance Group\nfunc (ig *RegionalInstanceGroup) GetPublicIps(t testing.TestingT, projectId string) []string {\n\treturn getPublicIps(t, ig, projectId)\n}\n\n// GetPublicIpsE returns a slice of the public IPs from the given Instance Group\nfunc (ig *RegionalInstanceGroup) GetPublicIpsE(t testing.TestingT, projectId string) ([]string, error) {\n\treturn getPublicIpsE(t, ig, projectId)\n}\n\n// getPublicIps a slice of the public IPs from the given Instance Group\nfunc getPublicIps(t testing.TestingT, ig InstanceGroup, projectId string) []string {\n\tips, err := getPublicIpsE(t, ig, projectId)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\treturn ips\n}\n\n// getPublicIpsE a slice of the public IPs from the given Instance Group\nfunc getPublicIpsE(t testing.TestingT, ig InstanceGroup, projectId string) ([]string, error) {\n\tinstances, err := getInstancesE(t, ig, projectId)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"Failed to get Compute Instances from Instance Group: %s\", err)\n\t}\n\n\tvar ips []string\n\n\tfor _, instance := range instances {\n\t\tip := instance.GetPublicIp(t)\n\t\tips = append(ips, ip)\n\t}\n\n\treturn ips, nil\n}\n\n// getRandomInstance returns a randomly selected Instance from the Regional Instance Group\nfunc (ig *ZonalInstanceGroup) GetRandomInstance(t testing.TestingT) *Instance {\n\treturn getRandomInstance(t, ig, ig.Name, ig.Region, ig.Size, ig.projectID)\n}\n\n// getRandomInstanceE returns a randomly selected Instance from the Regional Instance Group\nfunc (ig *ZonalInstanceGroup) GetRandomInstanceE(t testing.TestingT) (*Instance, error) {\n\treturn getRandomInstanceE(t, ig, ig.Name, ig.Region, ig.Size, ig.projectID)\n}\n\n// getRandomInstance returns a randomly selected Instance from the Regional Instance Group\nfunc (ig *RegionalInstanceGroup) GetRandomInstance(t testing.TestingT) *Instance {\n\treturn getRandomInstance(t, ig, ig.Name, ig.Region, ig.Size, ig.projectID)\n}\n\n// getRandomInstanceE returns a randomly selected Instance from the Regional Instance Group\nfunc (ig *RegionalInstanceGroup) GetRandomInstanceE(t testing.TestingT) (*Instance, error) {\n\treturn getRandomInstanceE(t, ig, ig.Name, ig.Region, ig.Size, ig.projectID)\n}\n\nfunc getRandomInstance(t testing.TestingT, ig InstanceGroup, name string, region string, size int64, projectID string) *Instance {\n\tinstance, err := getRandomInstanceE(t, ig, name, region, size, projectID)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\treturn instance\n}\n\nfunc getRandomInstanceE(t testing.TestingT, ig InstanceGroup, name string, region string, size int64, projectID string) (*Instance, error) {\n\tinstanceIDs := ig.GetInstanceIds(t)\n\tif len(instanceIDs) == 0 {\n\t\treturn nil, fmt.Errorf(\"Could not find any instances in Regional Instance Group or Zonal Instance Group %s in Region %s\", name, region)\n\t}\n\n\tclusterSize := int(size)\n\tif len(instanceIDs) != clusterSize {\n\t\treturn nil, fmt.Errorf(\"Expected Regional Instance Group or Zonal Instance Group %s in Region %s to have %d instances, but found %d\", name, region, clusterSize, len(instanceIDs))\n\t}\n\n\trandIndex := random.Random(0, clusterSize-1)\n\tinstanceID := instanceIDs[randIndex]\n\tinstance := FetchInstance(t, projectID, instanceID)\n\n\treturn instance, nil\n}\n\n// NewComputeService creates a new Compute service, which is used to make GCE API calls.\nfunc NewComputeService(t testing.TestingT) *compute.Service {\n\tclient, err := NewComputeServiceE(t)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn client\n}\n\n// NewComputeServiceE creates a new Compute service, which is used to make GCE API calls.\nfunc NewComputeServiceE(t testing.TestingT) (*compute.Service, error) {\n\tctx := context.Background()\n\n\tif ts, ok := getStaticTokenSource(); ok {\n\t\treturn compute.New(oauth2.NewClient(ctx, ts))\n\t}\n\n\t// Retrieve the Google OAuth token using a retry loop as it can sometimes return an error.\n\t// e.g: oauth2: cannot fetch token: Post https://oauth2.googleapis.com/token: net/http: TLS handshake timeout\n\t// This is loosely based on https://github.com/kubernetes/kubernetes/blob/7e8de5422cb5ad76dd0c147cf4336220d282e34b/pkg/cloudprovider/providers/gce/gce.go#L831.\n\n\tdescription := \"Attempting to request a Google OAuth2 token\"\n\tmaxRetries := 6\n\ttimeBetweenRetries := 10 * time.Second\n\n\tvar client *http.Client\n\n\tmsg, retryErr := retry.DoWithRetryE(t, description, maxRetries, timeBetweenRetries, func() (string, error) {\n\t\trawClient, err := google.DefaultClient(ctx, compute.CloudPlatformScope)\n\t\tif err != nil {\n\t\t\treturn \"Error retrieving default GCP client\", err\n\t\t}\n\t\tclient = rawClient\n\t\treturn \"Successfully retrieved default GCP client\", nil\n\t})\n\tlogger.Default.Logf(t, \"%s\", msg)\n\n\tif retryErr != nil {\n\t\treturn nil, retryErr\n\t}\n\n\treturn compute.New(client)\n}\n\n// NewInstancesService creates a new InstancesService service, which is used to make a subset of GCE API calls.\nfunc NewInstancesService(t testing.TestingT) *compute.InstancesService {\n\tclient, err := NewInstancesServiceE(t)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn client\n}\n\n// NewInstancesServiceE creates a new InstancesService service, which is used to make a subset of GCE API calls.\nfunc NewInstancesServiceE(t testing.TestingT) (*compute.InstancesService, error) {\n\tservice, err := NewComputeServiceE(t)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"Failed to get new Instances Service\\n\")\n\t}\n\n\treturn service.Instances, nil\n}\n\n// Return a random, valid name for GCP resources. Many resources in GCP requires lowercase letters only.\nfunc RandomValidGcpName() string {\n\tid := strings.ToLower(random.UniqueId())\n\tinstanceName := fmt.Sprintf(\"terratest-%s\", id)\n\n\treturn instanceName\n}\n"
  },
  {
    "path": "modules/gcp/compute_test.go",
    "content": "//go:build gcp\n// +build gcp\n\n// NOTE: We use build tags to differentiate GCP testing for better isolation and parallelism when executing our tests.\n\npackage gcp\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"reflect\"\n\t\"regexp\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/gruntwork-io/terratest/modules/retry\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"google.golang.org/api/compute/v1\"\n)\n\nconst DEFAULT_MACHINE_TYPE = \"f1-micro\"\nconst DEFAULT_IMAGE_FAMILY_PROJECT_NAME = \"ubuntu-os-cloud\"\nconst DEFAULT_IMAGE_FAMILY_NAME = \"family/ubuntu-2204-lts\"\n\n// Zones that support running f1-micro instances\nvar ZonesThatSupportF1Micro = []string{\"us-central1-a\", \"us-east1-b\", \"us-west1-a\", \"europe-north1-a\", \"europe-west1-b\", \"europe-central2-a\"}\n\nfunc TestGetPublicIpOfInstance(t *testing.T) {\n\tt.Parallel()\n\n\tinstanceName := RandomValidGcpName()\n\tprojectID := GetGoogleProjectIDFromEnvVar(t)\n\tzone := GetRandomZone(t, projectID, ZonesThatSupportF1Micro, nil, nil)\n\n\tcreateComputeInstance(t, projectID, zone, instanceName)\n\tdefer deleteComputeInstance(t, projectID, zone, instanceName)\n\n\t// Now that our Instance is launched, attempt to query the public IP\n\tmaxRetries := 10\n\tsleepBetweenRetries := 3 * time.Second\n\n\tip := retry.DoWithRetry(t, \"Read IP address of Compute Instance\", maxRetries, sleepBetweenRetries, func() (string, error) {\n\t\t// Consider attempting to connect to the Compute Instance at this IP in the future, but for now, we just call the\n\t\t// the function to ensure we don't have errors\n\t\tinstance := FetchInstance(t, projectID, instanceName)\n\t\tip := instance.GetPublicIp(t)\n\n\t\tif ip == \"\" {\n\t\t\treturn \"\", fmt.Errorf(\"Got blank IP. Retrying.\\n\")\n\t\t}\n\t\treturn ip, nil\n\t})\n\n\tfmt.Printf(\"Public IP of Compute Instance %s = %s\\n\", instanceName, ip)\n}\n\nfunc TestZoneUrlToZone(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tzoneUrl      string\n\t\texpectedZone string\n\t}{\n\t\t{\"https://www.googleapis.com/compute/v1/projects/terratest-123456/zones/asia-east1-b\", \"asia-east1-b\"},\n\t\t{\"https://www.googleapis.com/compute/v1/projects/terratest-123456/zones/us-east1-a\", \"us-east1-a\"},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tzone := ZoneUrlToZone(tc.zoneUrl)\n\t\tassert.Equal(t, zone, tc.expectedZone, \"Zone not extracted successfully from Zone URL\")\n\t}\n}\n\nfunc TestGetAndSetLabels(t *testing.T) {\n\tt.Parallel()\n\n\tinstanceName := RandomValidGcpName()\n\tprojectID := GetGoogleProjectIDFromEnvVar(t)\n\n\tzone := GetRandomZone(t, projectID, ZonesThatSupportF1Micro, nil, nil)\n\n\tcreateComputeInstance(t, projectID, zone, instanceName)\n\tdefer deleteComputeInstance(t, projectID, zone, instanceName)\n\n\t// Now that our Instance is launched, set the labels. Note that in GCP label keys and values can only contain\n\t// lowercase letters, numeric characters, underscores and dashes.\n\tinstance := FetchInstance(t, projectID, instanceName)\n\n\tlabelsToWrite := map[string]string{\n\t\t\"context\": \"terratest\",\n\t}\n\tinstance.SetLabels(t, labelsToWrite)\n\n\t// Now attempt to read the labels we just set.\n\tmaxRetries := 30\n\tsleepBetweenRetries := 3 * time.Second\n\n\tretry.DoWithRetry(t, \"Read newly set labels\", maxRetries, sleepBetweenRetries, func() (string, error) {\n\t\tinstance := FetchInstance(t, projectID, instanceName)\n\t\tlabelsFromRead := instance.GetLabels(t)\n\t\tif !reflect.DeepEqual(labelsFromRead, labelsToWrite) {\n\t\t\treturn \"\", fmt.Errorf(\"Labels that were written did not match labels that were read. Retrying.\\n\")\n\t\t}\n\n\t\treturn \"\", nil\n\t})\n}\n\n// Set custom metadata on a Compute Instance, and then verify it was set as expected\nfunc TestGetAndSetMetadata(t *testing.T) {\n\tt.Parallel()\n\n\tprojectID := GetGoogleProjectIDFromEnvVar(t)\n\tinstanceName := RandomValidGcpName()\n\n\tzone := GetRandomZone(t, projectID, ZonesThatSupportF1Micro, nil, nil)\n\n\t// Create a new Compute Instance\n\tcreateComputeInstance(t, projectID, zone, instanceName)\n\tdefer deleteComputeInstance(t, projectID, zone, instanceName)\n\n\t// Set the metadata\n\tinstance := FetchInstance(t, projectID, instanceName)\n\n\tmetadataToWrite := map[string]string{\n\t\t\"foo\": \"bar\",\n\t}\n\tinstance.SetMetadata(t, metadataToWrite)\n\n\t// Now attempt to read the metadata we just set\n\tmaxRetries := 30\n\tsleepBetweenRetries := 3 * time.Second\n\n\tretry.DoWithRetry(t, \"Read newly set metadata\", maxRetries, sleepBetweenRetries, func() (string, error) {\n\t\tinstance := FetchInstance(t, projectID, instanceName)\n\t\tmetadataFromRead := instance.GetMetadata(t)\n\t\tfor _, metadataItem := range metadataFromRead {\n\t\t\tfor key, val := range metadataToWrite {\n\t\t\t\tif metadataItem.Key == key && *metadataItem.Value == val {\n\t\t\t\t\treturn \"\", nil\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tfmt.Printf(\"Metadata to write: %+v\\nMetadata from read: %+v\\n\", metadataToWrite, metadataFromRead)\n\n\t\treturn \"\", fmt.Errorf(\"Metadata that was written was not found in metadata that was read. Retrying.\\n\")\n\t})\n}\n\n// Helper function to launch a Compute Instance. This function is useful for quickly iterating on automated tests. But\n// if you're writing a test that resembles real-world code that Terratest users may write, you should create a Compute\n// Instance using a Terraform apply, similar to the tests in /test.\nfunc createComputeInstance(t *testing.T, projectID string, zone string, name string) {\n\tt.Logf(\"Launching new Compute Instance %s\\n\", name)\n\n\t// This RegEx was pulled straight from the GCP API error messages that complained when it's not honored\n\tvalidNameExp := `^[a-z]([-a-z0-9]{0,61}[a-z0-9])?$`\n\tregEx := regexp.MustCompile(validNameExp)\n\n\tif !regEx.MatchString(name) {\n\t\tt.Fatalf(\"Invalid Compute Instance name: %s. Must match RegEx %s\\n\", name, validNameExp)\n\t}\n\n\tmachineType := DEFAULT_MACHINE_TYPE\n\tsourceImageFamilyProjectName := DEFAULT_IMAGE_FAMILY_PROJECT_NAME\n\tsourceImageFamilyName := DEFAULT_IMAGE_FAMILY_NAME\n\n\t// Per GCP docs (https://cloud.google.com/compute/docs/reference/rest/v1/instances/setMachineType), the MachineType\n\t// is actually specified as a partial URL\n\tmachineTypeURL := fmt.Sprintf(\"zones/%s/machineTypes/%s\", zone, machineType)\n\tsourceImageURL := fmt.Sprintf(\"https://www.googleapis.com/compute/v1/projects/%s/global/images/%s\", sourceImageFamilyProjectName, sourceImageFamilyName)\n\n\t// Based on the properties listed as required at https://cloud.google.com/compute/docs/reference/rest/v1/instances/insert\n\t// plus a somewhat painful cycle of add-next-property-try-fix-error-message-repeat.\n\tinstanceConfig := &compute.Instance{\n\t\tName:        name,\n\t\tMachineType: machineTypeURL,\n\t\tNetworkInterfaces: []*compute.NetworkInterface{\n\t\t\t&compute.NetworkInterface{\n\t\t\t\tAccessConfigs: []*compute.AccessConfig{\n\t\t\t\t\t&compute.AccessConfig{},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tDisks: []*compute.AttachedDisk{\n\t\t\t&compute.AttachedDisk{\n\t\t\t\tAutoDelete: true,\n\t\t\t\tBoot:       true,\n\t\t\t\tInitializeParams: &compute.AttachedDiskInitializeParams{\n\t\t\t\t\tSourceImage: sourceImageURL,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tservice, err := NewComputeServiceE(t)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t// Create the Compute Instance\n\tctx := context.Background()\n\t_, err = service.Instances.Insert(projectID, zone, instanceConfig).Context(ctx).Do()\n\tif err != nil {\n\t\tt.Fatalf(\"Error launching new Compute Instance: %s\", err)\n\t}\n}\n\n// Helper function that destroys the given Compute Instance and all of its attached disks.\nfunc deleteComputeInstance(t *testing.T, projectID string, zone string, name string) {\n\tt.Logf(\"Deleting Compute Instance %s\\n\", name)\n\n\tservice, err := NewComputeServiceE(t)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t// Delete the Compute Instance\n\tctx := context.Background()\n\t_, err = service.Instances.Delete(projectID, zone, name).Context(ctx).Do()\n\tif err != nil {\n\t\tt.Fatalf(\"Error deleting Compute Instance: %s\", err)\n\t}\n}\n\n// TODO: Add additional automated tests to cover remaining functions in compute.go\n"
  },
  {
    "path": "modules/gcp/compute_unit_test.go",
    "content": "package gcp\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"google.golang.org/api/compute/v1\"\n)\n\n// TestNewMetadataPreservesExisting is a regression test for issue #1655.\n// Verifies that existing metadata is preserved when adding new key-value pairs.\nfunc TestNewMetadataPreservesExisting(t *testing.T) {\n\tt.Parallel()\n\n\texistingVal := \"existing-value\"\n\toldMetadata := &compute.Metadata{\n\t\tFingerprint: \"test-fingerprint\",\n\t\tItems:       []*compute.MetadataItems{{Key: \"existing-key\", Value: &existingVal}},\n\t}\n\n\tresult := newMetadata(t, oldMetadata, map[string]string{\"new-key\": \"new-value\"})\n\n\t// Convert to map for easier assertion\n\tgot := make(map[string]string)\n\tfor _, item := range result.Items {\n\t\tgot[item.Key] = *item.Value\n\t}\n\n\tassert.Equal(t, \"test-fingerprint\", result.Fingerprint)\n\tassert.Equal(t, \"existing-value\", got[\"existing-key\"], \"existing metadata should be preserved\")\n\tassert.Equal(t, \"new-value\", got[\"new-key\"], \"new metadata should be added\")\n}\n"
  },
  {
    "path": "modules/gcp/gcp.go",
    "content": "// Package gcp allows interaction with Google Cloud Platform resources.\npackage gcp\n\nimport (\n\t\"google.golang.org/api/option\"\n)\n\nfunc withOptions() (opts []option.ClientOption) {\n\tv, ok := getStaticTokenSource()\n\tif ok {\n\t\topts = append(opts, option.WithTokenSource(v))\n\t}\n\n\treturn\n}\n"
  },
  {
    "path": "modules/gcp/gcr.go",
    "content": "package gcp\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/google/go-containerregistry/pkg/authn\"\n\tgcrname \"github.com/google/go-containerregistry/pkg/name\"\n\tgcrgoogle \"github.com/google/go-containerregistry/pkg/v1/google\"\n\tgcrremote \"github.com/google/go-containerregistry/pkg/v1/remote\"\n\t\"github.com/gruntwork-io/terratest/modules/logger\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// DeleteGCRRepo deletes a GCR repository including all tagged images\nfunc DeleteGCRRepo(t testing.TestingT, repo string) {\n\terr := DeleteGCRRepoE(t, repo)\n\trequire.NoError(t, err)\n}\n\n// DeleteGCRRepoE deletes a GCR repository including all tagged images\nfunc DeleteGCRRepoE(t testing.TestingT, repo string) error {\n\t// create a new auther for the API calls\n\tauther, err := newGCRAuther()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"Failed to create auther. Got error: %v\", err)\n\t}\n\n\tgcrrepo, err := gcrname.NewRepository(repo)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"Failed to get repo. Got error: %v\", err)\n\t}\n\n\tlogger.Default.Logf(t, \"Retrieving Image Digests %s\", gcrrepo)\n\ttags, err := gcrgoogle.List(gcrrepo, gcrgoogle.WithAuth(auther))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"Failed to list tags for repo %s. Got error: %v\", repo, err)\n\t}\n\n\t// attempt to delete the latest image tag\n\tlatestRef := repo + \":latest\"\n\tlogger.Default.Logf(t, \"Deleting Image Ref %s\", latestRef)\n\tif err := DeleteGCRImageRefE(t, latestRef); err != nil {\n\t\treturn fmt.Errorf(\"Failed to delete GCR Image Reference %s. Got error: %v\", latestRef, err)\n\t}\n\n\t// delete image references sequentially\n\tfor k := range tags.Manifests {\n\t\tref := repo + \"@\" + k\n\t\tlogger.Default.Logf(t, \"Deleting Image Ref %s\", ref)\n\n\t\tif err := DeleteGCRImageRefE(t, ref); err != nil {\n\t\t\treturn fmt.Errorf(\"Failed to delete GCR Image Reference %s. Got error: %v\", ref, err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// DeleteGCRImageRef deletes a single repo image ref/digest\nfunc DeleteGCRImageRef(t testing.TestingT, ref string) {\n\terr := DeleteGCRImageRefE(t, ref)\n\trequire.NoError(t, err)\n}\n\n// DeleteGCRImageRefE deletes a single repo image ref/digest\nfunc DeleteGCRImageRefE(t testing.TestingT, ref string) error {\n\tname, err := gcrname.ParseReference(ref)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"Failed to parse reference %s. Got error: %v\", ref, err)\n\t}\n\n\t// create a new auther for the API calls\n\tauther, err := newGCRAuther()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"Failed to create auther. Got error: %v\", err)\n\t}\n\n\topts := gcrremote.WithAuth(auther)\n\n\tif err := gcrremote.Delete(name, opts); err != nil {\n\t\treturn fmt.Errorf(\"Failed to delete %s. Got error: %v\", name, err)\n\t}\n\n\treturn nil\n}\n\nfunc newGCRAuther() (authn.Authenticator, error) {\n\tif ts, ok := getStaticTokenSource(); ok {\n\t\treturn gcrgoogle.NewTokenSourceAuthenticator(ts), nil\n\t}\n\n\treturn gcrgoogle.NewEnvAuthenticator(context.Background())\n}\n"
  },
  {
    "path": "modules/gcp/oslogin.go",
    "content": "package gcp\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/gruntwork-io/terratest/modules/logger\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/stretchr/testify/require\"\n\t\"golang.org/x/oauth2\"\n\t\"golang.org/x/oauth2/google\"\n\t\"google.golang.org/api/compute/v1\"\n\t\"google.golang.org/api/oslogin/v1\"\n)\n\n// ImportSSHKey will import an SSH key to GCP under the provided user identity.\n// The `user` parameter should be the email address of the user.\n// The `key` parameter should be the public key of the SSH key being uploaded.\n// This will fail the test if there is an error.\nfunc ImportSSHKey(t testing.TestingT, user, key string) {\n\trequire.NoErrorf(t, ImportSSHKeyE(t, user, key), \"Could not add SSH Key to user %s\", user)\n}\n\n// ImportSSHKeyE will import an SSH key to GCP under the provided user identity.\n// The `user` parameter should be the email address of the user.\n// The `key` parameter should be the public key of the SSH key being uploaded.\nfunc ImportSSHKeyE(t testing.TestingT, user, key string) error {\n\treturn importProjectSSHKeyE(t, user, key, nil)\n}\n\n// ImportProjectSSHKey will import an SSH key to GCP under the provided user identity.\n// The `user` parameter should be the email address of the user.\n// The `key` parameter should be the public key of the SSH key being uploaded.\n// The `projectID` parameter should be the chosen project ID.\n// This will fail the test if there is an error.\nfunc ImportProjectSSHKey(t testing.TestingT, user, key, projectID string) {\n\trequire.NoErrorf(t, ImportProjectSSHKeyE(t, user, key, projectID), \"Could not add SSH Key to user %s\", user)\n}\n\n// ImportProjectSSHKeyE will import an SSH key to GCP under the provided user identity.\n// The `user` parameter should be the email address of the user.\n// The `key` parameter should be the public key of the SSH key being uploaded.\n// The `projectID` parameter should be the chosen project ID.\nfunc ImportProjectSSHKeyE(t testing.TestingT, user, key, projectID string) error {\n\treturn importProjectSSHKeyE(t, user, key, &projectID)\n}\n\nfunc importProjectSSHKeyE(t testing.TestingT, user, key string, projectID *string) error {\n\tlogger.Default.Logf(t, \"Importing SSH key for user %s\", user)\n\n\tctx := context.Background()\n\tservice, err := NewOSLoginServiceE(t)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tparent := fmt.Sprintf(\"users/%s\", user)\n\n\tsshPublicKey := &oslogin.SshPublicKey{\n\t\tKey: key,\n\t}\n\n\treq := service.Users.ImportSshPublicKey(parent, sshPublicKey)\n\tif projectID != nil {\n\t\treq = req.ProjectId(*projectID)\n\t}\n\t_, err = req.Context(ctx).Do()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// DeleteSSHKey will delete an SSH key attached to the provided user identity.\n// The `user` parameter should be the email address of the user.\n// The `key` parameter should be the public key of the SSH key that was uploaded.\n// This will fail the test if there is an error.\nfunc DeleteSSHKey(t testing.TestingT, user, key string) {\n\trequire.NoErrorf(t, DeleteSSHKeyE(t, user, key), \"Could not delete SSH Key for user %s\", user)\n}\n\n// DeleteSSHKeyE will delete an SSH key attached to the provided user identity.\n// The `user` parameter should be the email address of the user.\n// The `key` parameter should be the public key of the SSH key that was uploaded.\nfunc DeleteSSHKeyE(t testing.TestingT, user, key string) error {\n\tlogger.Default.Logf(t, \"Deleting SSH key for user %s\", user)\n\n\tctx := context.Background()\n\tservice, err := NewOSLoginServiceE(t)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tloginProfile := GetLoginProfile(t, user)\n\n\tfor _, v := range loginProfile.SshPublicKeys {\n\t\tif key == v.Key {\n\t\t\tpath := fmt.Sprintf(\"users/%s/sshPublicKeys/%s\", user, v.Fingerprint)\n\t\t\t_, err = service.Users.SshPublicKeys.Delete(path).Context(ctx).Do()\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// GetLoginProfile will retrieve the login profile for a user's Google identity. The login profile is a combination of OS Login + gcloud SSH keys and POSIX\n// accounts the user will appear as. Generally, this will only be the OS Login key + account, but `gcloud compute ssh` could create temporary keys and profiles.\n// The `user` parameter should be the email address of the user.\n// This will fail the test if there is an error.\nfunc GetLoginProfile(t testing.TestingT, user string) *oslogin.LoginProfile {\n\tprofile, err := GetLoginProfileE(t, user)\n\trequire.NoErrorf(t, err, \"Could not get login profile for user %s\", user)\n\n\treturn profile\n}\n\n// GetLoginProfileE will retrieve the login profile for a user's Google identity. The login profile is a combination of OS Login + gcloud SSH keys and POSIX\n// accounts the user will appear as. Generally, this will only be the OS Login key + account, but `gcloud compute ssh` could create temporary keys and profiles.\n// The `user` parameter should be the email address of the user.\nfunc GetLoginProfileE(t testing.TestingT, user string) (*oslogin.LoginProfile, error) {\n\tlogger.Default.Logf(t, \"Getting login profile for user %s\", user)\n\n\tctx := context.Background()\n\tservice, err := NewOSLoginServiceE(t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tname := fmt.Sprintf(\"users/%s\", user)\n\n\tprofile, err := service.Users.GetLoginProfile(name).Context(ctx).Do()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn profile, nil\n}\n\n// NewOSLoginServiceE creates a new OS Login service, which is used to make OS Login API calls.\nfunc NewOSLoginServiceE(t testing.TestingT) (*oslogin.Service, error) {\n\tctx := context.Background()\n\n\tif ts, ok := getStaticTokenSource(); ok {\n\t\treturn oslogin.New(oauth2.NewClient(ctx, ts))\n\t}\n\n\tclient, err := google.DefaultClient(ctx, compute.CloudPlatformScope)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"Failed to get default client: %v\", err)\n\t}\n\n\tservice, err := oslogin.New(client)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn service, nil\n}\n"
  },
  {
    "path": "modules/gcp/oslogin_test.go",
    "content": "//go:build gcp\n// +build gcp\n\n// NOTE: We use build tags to differentiate GCP testing for better isolation and parallelism when executing our tests.\n\npackage gcp\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/logger\"\n\t\"github.com/gruntwork-io/terratest/modules/ssh\"\n)\n\n// TestOSLogin groups all OS Login tests that mutate SSH keys for the same user.\n// These tests cannot run in parallel with each other because Google's OS Login API\n// returns \"409: Multiple concurrent mutations\" errors when multiple operations\n// modify the same user's SSH keys simultaneously.\n//\n// By grouping them in a single test function with subtests (without t.Parallel()),\n// we ensure they run sequentially while still allowing other GCP tests to run in parallel.\nfunc TestOSLogin(t *testing.T) {\n\tt.Parallel() // This test can run in parallel with OTHER GCP tests\n\n\t// Clean up any stale SSH keys from previous test runs to avoid\n\t// \"Login profile size exceeds 32 KiB\" errors.\n\tuser := GetGoogleIdentityEmailEnvVar(t)\n\tpurgeAllSSHKeys(t, user)\n\n\t// Subtests run sequentially (no t.Parallel() on subtests) to avoid 409 conflicts\n\tt.Run(\"ImportSSHKey\", func(t *testing.T) {\n\t\tkeyPair := ssh.GenerateRSAKeyPair(t, 2048)\n\t\tkey := keyPair.PublicKey\n\n\t\tuser := GetGoogleIdentityEmailEnvVar(t)\n\n\t\tdefer DeleteSSHKey(t, user, key)\n\t\tImportSSHKey(t, user, key)\n\t})\n\n\tt.Run(\"ImportProjectSSHKey\", func(t *testing.T) {\n\t\tkeyPair := ssh.GenerateRSAKeyPair(t, 2048)\n\t\tkey := keyPair.PublicKey\n\n\t\tuser := GetGoogleIdentityEmailEnvVar(t)\n\t\tprojectID := GetGoogleProjectIDFromEnvVar(t)\n\n\t\tdefer DeleteSSHKey(t, user, key)\n\t\tImportProjectSSHKey(t, user, key, projectID)\n\t})\n\n\tt.Run(\"GetLoginProfile\", func(t *testing.T) {\n\t\tuser := GetGoogleIdentityEmailEnvVar(t)\n\t\tGetLoginProfile(t, user)\n\t})\n\n\tt.Run(\"SetOSLoginKey\", func(t *testing.T) {\n\t\tkeyPair := ssh.GenerateRSAKeyPair(t, 2048)\n\t\tkey := keyPair.PublicKey\n\n\t\tuser := GetGoogleIdentityEmailEnvVar(t)\n\n\t\tdefer DeleteSSHKey(t, user, key)\n\t\tImportSSHKey(t, user, key)\n\t\tloginProfile := GetLoginProfile(t, user)\n\n\t\tfound := false\n\t\tfor _, v := range loginProfile.SshPublicKeys {\n\t\t\tif key == v.Key {\n\t\t\t\tfound = true\n\t\t\t}\n\t\t}\n\n\t\tif found != true {\n\t\t\tt.Fatalf(\"Did not find key in login profile for user %s\", user)\n\t\t}\n\t})\n}\n\n// purgeAllSSHKeys deletes all SSH keys from the user's OS Login profile.\n// This prevents \"Login profile size exceeds 32 KiB\" errors caused by\n// stale keys accumulating from previous test runs.\nfunc purgeAllSSHKeys(t *testing.T, user string) {\n\tprofile, err := GetLoginProfileE(t, user)\n\tif err != nil {\n\t\tt.Logf(\"Warning: could not get login profile to purge keys: %v\", err)\n\t\treturn\n\t}\n\n\tif len(profile.SshPublicKeys) == 0 {\n\t\treturn\n\t}\n\n\tlogger.Default.Logf(t, \"Purging %d stale SSH keys from OS Login profile for user %s\", len(profile.SshPublicKeys), user)\n\n\tservice, err := NewOSLoginServiceE(t)\n\tif err != nil {\n\t\tt.Logf(\"Warning: could not create OS Login service to purge keys: %v\", err)\n\t\treturn\n\t}\n\n\tctx := context.Background()\n\tfor fingerprint := range profile.SshPublicKeys {\n\t\tpath := fmt.Sprintf(\"users/%s/sshPublicKeys/%s\", user, fingerprint)\n\t\tif _, err := service.Users.SshPublicKeys.Delete(path).Context(ctx).Do(); err != nil {\n\t\t\tt.Logf(\"Warning: could not delete SSH key %s: %v\", fingerprint, err)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "modules/gcp/provider.go",
    "content": "package gcp\n\nimport (\n\t\"github.com/gruntwork-io/terratest/modules/environment\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n)\n\nvar credsEnvVars = []string{\n\t\"GOOGLE_APPLICATION_CREDENTIALS\",\n\t\"GOOGLE_CREDENTIALS\",\n\t\"GOOGLE_CLOUD_KEYFILE_JSON\",\n\t\"GCLOUD_KEYFILE_JSON\",\n\t\"GOOGLE_USE_DEFAULT_CREDENTIALS\",\n}\n\nvar projectEnvVars = []string{\n\t\"GOOGLE_PROJECT\",\n\t\"GOOGLE_CLOUD_PROJECT\",\n\t\"GOOGLE_CLOUD_PROJECT_ID\",\n\t\"GCLOUD_PROJECT\",\n\t\"CLOUDSDK_CORE_PROJECT\",\n}\n\nvar regionEnvVars = []string{\n\t\"GOOGLE_REGION\",\n\t\"GCLOUD_REGION\",\n\t\"CLOUDSDK_COMPUTE_REGION\",\n}\n\nvar googleIdentityEmailEnvVars = []string{\n\t\"GOOGLE_IDENTITY_EMAIL\",\n}\n\n// GetGoogleCredentialsFromEnvVar returns the Credentials for use with testing.\nfunc GetGoogleCredentialsFromEnvVar(t testing.TestingT) string {\n\treturn environment.GetFirstNonEmptyEnvVarOrEmptyString(t, credsEnvVars)\n}\n\n// GetGoogleProjectIDFromEnvVar returns the Project Id for use with testing.\nfunc GetGoogleProjectIDFromEnvVar(t testing.TestingT) string {\n\treturn environment.GetFirstNonEmptyEnvVarOrFatal(t, projectEnvVars)\n}\n\n// GetGoogleRegionFromEnvVar returns the Region for use with testing.\nfunc GetGoogleRegionFromEnvVar(t testing.TestingT) string {\n\treturn environment.GetFirstNonEmptyEnvVarOrFatal(t, regionEnvVars)\n}\n\n// GetGoogleIdentityEmailEnvVar returns a Google identity (user) for use with testing.\nfunc GetGoogleIdentityEmailEnvVar(t testing.TestingT) string {\n\treturn environment.GetFirstNonEmptyEnvVarOrFatal(t, googleIdentityEmailEnvVars)\n}\n"
  },
  {
    "path": "modules/gcp/region.go",
    "content": "package gcp\n\nimport (\n\t\"context\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/gruntwork-io/terratest/modules/collections\"\n\t\"github.com/gruntwork-io/terratest/modules/logger\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"google.golang.org/api/compute/v1\"\n)\n\n// You can set this environment variable to force Terratest to use a specific Region rather than a random one. This is\n// convenient when iterating locally.\nconst regionOverrideEnvVarName = \"TERRATEST_GCP_REGION\"\n\n// You can set this environment variable to force Terratest to use a specific Zone rather than a random one. This is\n// convenient when iterating locally.\nconst zoneOverrideEnvVarName = \"TERRATEST_GCP_ZONE\"\n\n// Some GCP API calls require a GCP Region. We typically require the user to set one explicitly, but in some\n// cases, this doesn't make sense (e.g., for fetching the list of regions in an account), so for those cases, we use\n// this Region as a default.\nconst defaultRegion = \"us-west1\"\n\n// Some GCP API calls require a GCP Zone. We typically require the user to set one explicitly, but in some\n// cases, this doesn't make sense (e.g., for fetching the list of regions in an account), so for those cases, we use\n// this Zone as a default.\nconst defaultZone = \"us-west1-b\"\n\n// GetRandomRegion gets a randomly chosen GCP Region. If approvedRegions is not empty, this will be a Region from the approvedRegions\n// list; otherwise, this method will fetch the latest list of regions from the GCP APIs and pick one of those. If\n// forbiddenRegions is not empty, this method will make sure the returned Region is not in the forbiddenRegions list.\nfunc GetRandomRegion(t testing.TestingT, projectID string, approvedRegions []string, forbiddenRegions []string) string {\n\tregion, err := GetRandomRegionE(t, projectID, approvedRegions, forbiddenRegions)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn region\n}\n\n// GetRandomRegionE gets a randomly chosen GCP Region. If approvedRegions is not empty, this will be a Region from the approvedRegions\n// list; otherwise, this method will fetch the latest list of regions from the GCP APIs and pick one of those. If\n// forbiddenRegions is not empty, this method will make sure the returned Region is not in the forbiddenRegions list.\nfunc GetRandomRegionE(t testing.TestingT, projectID string, approvedRegions []string, forbiddenRegions []string) (string, error) {\n\tregionFromEnvVar := os.Getenv(regionOverrideEnvVarName)\n\tif regionFromEnvVar != \"\" {\n\t\tlogger.Default.Logf(t, \"Using GCP Region %s from environment variable %s\", regionFromEnvVar, regionOverrideEnvVarName)\n\t\treturn regionFromEnvVar, nil\n\t}\n\n\tregionsToPickFrom := approvedRegions\n\n\tif len(regionsToPickFrom) == 0 {\n\t\tallRegions, err := GetAllGcpRegionsE(t, projectID)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\tregionsToPickFrom = allRegions\n\t}\n\n\tregionsToPickFrom = collections.ListSubtract(regionsToPickFrom, forbiddenRegions)\n\tregion := random.RandomString(regionsToPickFrom)\n\n\tlogger.Default.Logf(t, \"Using Region %s\", region)\n\treturn region, nil\n}\n\n// GetRandomZone gets a randomly chosen GCP Zone. If approvedRegions is not empty, this will be a Zone from the approvedZones\n// list; otherwise, this method will fetch the latest list of Zones from the GCP APIs and pick one of those. If\n// forbiddenZones is not empty, this method will make sure the returned Region is not in the forbiddenZones list.\nfunc GetRandomZone(t testing.TestingT, projectID string, approvedZones []string, forbiddenZones []string, forbiddenRegions []string) string {\n\tzone, err := GetRandomZoneE(t, projectID, approvedZones, forbiddenZones, forbiddenRegions)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn zone\n}\n\n// GetRandomZoneE gets a randomly chosen GCP Zone. If approvedRegions is not empty, this will be a Zone from the approvedZones\n// list; otherwise, this method will fetch the latest list of Zones from the GCP APIs and pick one of those. If\n// forbiddenZones is not empty, this method will make sure the returned Region is not in the forbiddenZones list.\nfunc GetRandomZoneE(t testing.TestingT, projectID string, approvedZones []string, forbiddenZones []string, forbiddenRegions []string) (string, error) {\n\tzoneFromEnvVar := os.Getenv(zoneOverrideEnvVarName)\n\tif zoneFromEnvVar != \"\" {\n\t\tlogger.Default.Logf(t, \"Using GCP Zone %s from environment variable %s\", zoneFromEnvVar, zoneOverrideEnvVarName)\n\t\treturn zoneFromEnvVar, nil\n\t}\n\n\tzonesToPickFrom := approvedZones\n\n\tif len(zonesToPickFrom) == 0 {\n\t\tallZones, err := GetAllGcpZonesE(t, projectID)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\tzonesToPickFrom = allZones\n\t}\n\n\tzonesToPickFrom = collections.ListSubtract(zonesToPickFrom, forbiddenZones)\n\n\tvar zonesToPickFromFiltered []string\n\tfor _, zone := range zonesToPickFrom {\n\t\tif !isInRegions(zone, forbiddenRegions) {\n\t\t\tzonesToPickFromFiltered = append(zonesToPickFromFiltered, zone)\n\t\t}\n\t}\n\n\tzone := random.RandomString(zonesToPickFromFiltered)\n\n\treturn zone, nil\n}\n\n// GetRandomZoneForRegion gets a randomly chosen GCP Zone in the given Region.\nfunc GetRandomZoneForRegion(t testing.TestingT, projectID string, region string) string {\n\tzone, err := GetRandomZoneForRegionE(t, projectID, region)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn zone\n}\n\n// GetRandomZoneForRegionE gets a randomly chosen GCP Zone in the given Region.\nfunc GetRandomZoneForRegionE(t testing.TestingT, projectID string, region string) (string, error) {\n\tzoneFromEnvVar := os.Getenv(zoneOverrideEnvVarName)\n\tif zoneFromEnvVar != \"\" {\n\t\tlogger.Default.Logf(t, \"Using GCP Zone %s from environment variable %s\", zoneFromEnvVar, zoneOverrideEnvVarName)\n\t\treturn zoneFromEnvVar, nil\n\t}\n\n\tallZones, err := GetAllGcpZonesE(t, projectID)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tzonesToPickFrom := []string{}\n\n\tfor _, zone := range allZones {\n\t\tif strings.Contains(zone, region) {\n\t\t\tzonesToPickFrom = append(zonesToPickFrom, zone)\n\t\t}\n\t}\n\n\tzone := random.RandomString(zonesToPickFrom)\n\n\tlogger.Default.Logf(t, \"Using Zone %s\", zone)\n\treturn zone, nil\n}\n\n// GetAllGcpRegions gets the list of GCP regions available in this account.\nfunc GetAllGcpRegions(t testing.TestingT, projectID string) []string {\n\tout, err := GetAllGcpRegionsE(t, projectID)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn out\n}\n\n// GetAllGcpRegionsE gets the list of GCP regions available in this account.\nfunc GetAllGcpRegionsE(t testing.TestingT, projectID string) ([]string, error) {\n\tlogger.Default.Logf(t, \"Looking up all GCP regions available in this account\")\n\n\t// Note that NewComputeServiceE creates a context, but it appears to be empty so we keep the code simpler by\n\t// creating a new one here\n\tctx := context.Background()\n\n\tservice, err := NewComputeServiceE(t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treq := service.Regions.List(projectID)\n\n\tregions := []string{}\n\terr = req.Pages(ctx, func(page *compute.RegionList) error {\n\t\tfor _, region := range page.Items {\n\t\t\tregions = append(regions, region.Name)\n\t\t}\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn regions, nil\n}\n\n// GetAllGcpZones gets the list of GCP Zones available in this account.\nfunc GetAllGcpZones(t testing.TestingT, projectID string) []string {\n\tout, err := GetAllGcpZonesE(t, projectID)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn out\n}\n\n// GetAllGcpZonesE gets the list of GCP Zones available in this account.\nfunc GetAllGcpZonesE(t testing.TestingT, projectID string) ([]string, error) {\n\t// Note that NewComputeServiceE creates a context, but it appears to be empty so we keep the code simpler by\n\t// creating a new one here\n\tctx := context.Background()\n\n\tservice, err := NewComputeServiceE(t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treq := service.Zones.List(projectID)\n\n\tzones := []string{}\n\terr = req.Pages(ctx, func(page *compute.ZoneList) error {\n\t\tfor _, zone := range page.Items {\n\t\t\tzones = append(zones, zone.Name)\n\t\t}\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn zones, nil\n}\n\n// Given a GCP Zone URL formatted like https://www.googleapis.com/compute/v1/projects/project-123456/zones/asia-east1-b,\n// return \"asia-east1-b\".\n// Todo: Improve sanity checking on this function by using a RegEx with capture groups\nfunc ZoneUrlToZone(zoneUrl string) string {\n\ttokens := strings.Split(zoneUrl, \"/\")\n\treturn tokens[len(tokens)-1]\n}\n\n// Given a GCP Zone URL formatted like https://www.googleapis.com/compute/v1/projects/project-123456/regions/southamerica-east1,\n// return \"southamerica-east1\".\n// Todo: Improve sanity checking on this function by using a RegEx with capture groups\nfunc RegionUrlToRegion(zoneUrl string) string {\n\ttokens := strings.Split(zoneUrl, \"/\")\n\treturn tokens[len(tokens)-1]\n}\n\n// Returns true if the given zone is in any of the given regions\nfunc isInRegions(zone string, regions []string) bool {\n\tfor _, region := range regions {\n\t\tif isInRegion(zone, region) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// Returns true if the given zone is in the given region\nfunc isInRegion(zone string, region string) bool {\n\treturn strings.Contains(zone, region)\n}\n"
  },
  {
    "path": "modules/gcp/region_test.go",
    "content": "//go:build gcp\n// +build gcp\n\n// NOTE: We use build tags to differentiate GCP testing for better isolation and parallelism when executing our tests.\n\npackage gcp\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestGetRandomRegion(t *testing.T) {\n\tt.Parallel()\n\n\tprojectID := GetGoogleProjectIDFromEnvVar(t)\n\n\trandomRegion := GetRandomRegion(t, projectID, nil, nil)\n\tassertLooksLikeRegionName(t, randomRegion)\n}\n\nfunc TestGetRandomZone(t *testing.T) {\n\tt.Parallel()\n\n\tprojectID := GetGoogleProjectIDFromEnvVar(t)\n\n\trandomZone := GetRandomZone(t, projectID, nil, nil, nil)\n\tassertLooksLikeZoneName(t, randomZone)\n}\n\nfunc TestGetRandomRegionExcludesForbiddenRegions(t *testing.T) {\n\tt.Parallel()\n\n\tprojectID := GetGoogleProjectIDFromEnvVar(t)\n\n\tapprovedRegions := []string{\"asia-east1\", \"asia-northeast1\", \"asia-south1\", \"asia-southeast1\", \"australia-southeast1\", \"europe-north1\", \"europe-west1\", \"europe-west2\", \"europe-west3\", \"northamerica-northeast1\", \"southamerica-east1\", \"us-central1\", \"us-east1\", \"us-east4\", \"us-west2\"}\n\tforbiddenRegions := []string{\"europe-west4\", \"us-west1\"}\n\n\tfor i := 0; i < 1000; i++ {\n\t\trandomRegion := GetRandomRegion(t, projectID, approvedRegions, forbiddenRegions)\n\t\tassert.NotContains(t, forbiddenRegions, randomRegion)\n\t}\n}\n\nfunc TestGetRandomZoneExcludesForbiddenZones(t *testing.T) {\n\tt.Parallel()\n\n\tprojectID := GetGoogleProjectIDFromEnvVar(t)\n\n\tapprovedZones := []string{\"us-east1-b\", \"us-east1-c\", \"us-east1-d\", \"us-east4-a\", \"us-east4-b\", \"us-east4-c\", \"us-west2-a\", \"us-west2-b\", \"us-west2-c\", \"us-central1-f\", \"europe-west2-b\"}\n\tforbiddenZones := []string{\"us-east1-a\", \"europe-west1-a\", \"europe-west2-a\", \"europe-west2-c\"}\n\n\tfor i := 0; i < 1000; i++ {\n\t\trandomZone := GetRandomZone(t, projectID, approvedZones, forbiddenZones, nil)\n\t\tassert.NotContains(t, forbiddenZones, randomZone)\n\t}\n}\n\nfunc TestGetRandomZoneExcludesForbiddenRegions(t *testing.T) {\n\tt.Parallel()\n\n\tprojectID := GetGoogleProjectIDFromEnvVar(t)\n\n\tapprovedZones := []string{\"us-east1-b\", \"us-east1-c\", \"us-east1-d\", \"us-east4-a\", \"us-east4-b\", \"us-east4-c\", \"us-west2-a\", \"us-west2-b\", \"us-west2-c\", \"us-central1-f\", \"europe-west2-b\"}\n\tforbiddenRegions := []string{\"europe-west2\"}\n\n\tfor i := 0; i < 1000; i++ {\n\t\trandomZone := GetRandomZone(t, projectID, approvedZones, nil, forbiddenRegions)\n\n\t\tfor _, forbiddenRegion := range forbiddenRegions {\n\t\t\tassert.True(t, !isInRegion(randomZone, forbiddenRegion), \"Expected that selected zone %s would not be in region %s, but it is.\", randomZone, forbiddenRegion)\n\t\t}\n\t}\n}\n\nfunc TestGetAllGcpRegions(t *testing.T) {\n\tt.Parallel()\n\n\tprojectID := GetGoogleProjectIDFromEnvVar(t)\n\n\tregions := GetAllGcpRegions(t, projectID)\n\n\t// The typical account had access to 17 regions as of August, 2018: https://cloud.google.com/compute/docs/regions-zones/\n\tassert.True(t, len(regions) >= 17, \"Number of regions: %d\", len(regions))\n\tfor _, region := range regions {\n\t\tassertLooksLikeRegionName(t, region)\n\t}\n}\n\nfunc TestGetAllGcpZones(t *testing.T) {\n\tt.Parallel()\n\n\tprojectID := GetGoogleProjectIDFromEnvVar(t)\n\n\tzones := GetAllGcpZones(t, projectID)\n\n\t// The typical account had access to 52 zones as of August, 2018: https://cloud.google.com/compute/docs/regions-zones/\n\tassert.True(t, len(zones) >= 52, \"Number of zones: %d\", len(zones))\n\tfor _, zone := range zones {\n\t\tassertLooksLikeZoneName(t, zone)\n\t}\n}\n\nfunc TestGetRandomZoneForRegion(t *testing.T) {\n\tt.Parallel()\n\n\tprojectID := GetGoogleProjectIDFromEnvVar(t)\n\n\tregions := []string{\n\t\t\"us-west1\",\n\t\t\"us-west2\",\n\t\t\"us-central1\",\n\t}\n\n\tfor _, region := range regions {\n\t\tzone, err := GetRandomZoneForRegionE(t, projectID, region)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tassert.True(t, strings.Contains(zone, region), \"Expected zone %s to be in region %s\", zone, region)\n\t}\n}\n\nfunc TestGetInRegion(t *testing.T) {\n\tt.Parallel()\n\n\ttestData := []struct {\n\t\tzone     string\n\t\tregion   string\n\t\texpected bool\n\t}{\n\t\t{\"us-west2a\", \"us-west2\", true},\n\t\t{\"us-west2b\", \"us-west2\", true},\n\t\t{\"us-west2a\", \"us-east1\", false},\n\t}\n\n\tfor _, td := range testData {\n\t\tactual := isInRegion(td.zone, td.region)\n\t\tassert.Equal(t, td.expected, actual, \"Expected %t for isInRegion(%s, %s) but got %t\", td.expected, td.zone, td.region, actual)\n\t}\n}\n\nfunc TestGetInRegions(t *testing.T) {\n\tt.Parallel()\n\n\ttestData := []struct {\n\t\tzone     string\n\t\tregions  []string\n\t\texpected bool\n\t}{\n\t\t{\"us-west2a\", []string{\"us-west2\", \"us-east1\"}, true},\n\t\t{\"us-west2b\", []string{\"us-west2\", \"us-east1\"}, true},\n\t\t{\"us-west2a\", []string{\"us-west2\", \"us-east1\"}, true},\n\t\t{\"us-west2a\", []string{\"us-east1\", \"europe-west1\"}, false},\n\t}\n\n\tfor _, td := range testData {\n\t\tactual := isInRegions(td.zone, td.regions)\n\t\tassert.Equal(t, td.expected, actual, \"Expected %t for isInRegions(%s, %v) but got %t\", td.expected, td.zone, td.regions, actual)\n\t}\n}\n\nfunc assertLooksLikeRegionName(t *testing.T, regionName string) {\n\tassert.Regexp(t, \"[a-z]+-[a-z]+[[:digit:]]+\", regionName)\n}\n\nfunc assertLooksLikeZoneName(t *testing.T, zoneName string) {\n\tassert.Regexp(t, \"[a-z]+-[a-z]+[[:digit:]]+-[a-z]{1}\", zoneName)\n}\n"
  },
  {
    "path": "modules/gcp/static_token.go",
    "content": "package gcp\n\nimport (\n\t\"os\"\n\n\t\"golang.org/x/oauth2\"\n)\n\nfunc getStaticTokenSource() (oauth2.TokenSource, bool) {\n\tv, ok := os.LookupEnv(\"GOOGLE_OAUTH_ACCESS_TOKEN\")\n\tif ok {\n\t\treturn oauth2.StaticTokenSource(&oauth2.Token{AccessToken: v}), true\n\t}\n\treturn nil, false\n}\n"
  },
  {
    "path": "modules/gcp/static_token_test.go",
    "content": "//go:build gcp\n// +build gcp\n\npackage gcp\n\nimport (\n\t\"context\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/stretchr/testify/require\"\n\t\"golang.org/x/oauth2/google\"\n)\n\nfunc TestStaticTokenClient(t *testing.T) {\n\tctx := context.Background()\n\tcreds, err := google.FindDefaultCredentials(ctx, \"https://www.googleapis.com/auth/cloud-platform\")\n\trequire.NoError(t, err)\n\ttoken, err := creds.TokenSource.Token()\n\trequire.NoError(t, err)\n\tprojectID := GetGoogleProjectIDFromEnvVar(t)\n\n\t// we poison the default client instantiation with invalid file so that if it is used, it fails\n\tt.Setenv(\"GOOGLE_APPLICATION_CREDENTIALS\", \"non-existent-credentials.json\")\n\t_, err = NewCloudBuildServiceE(t)\n\trequire.Error(t, err)\n\t_, err = NewComputeServiceE(t)\n\trequire.Error(t, err)\n\t_, err = newGCRAuther()\n\trequire.Error(t, err)\n\t_, err = NewOSLoginServiceE(t)\n\trequire.Error(t, err)\n\t_, err = newStorageClient()\n\trequire.Error(t, err)\n\n\t// now we instantiate client with oauth2 token\n\t// and run several function to make sure the new client is correctly configured with access token\n\tt.Setenv(\"GOOGLE_OAUTH_ACCESS_TOKEN\", token.AccessToken)\n\tGetAllGcpRegions(t, projectID)\n\tGetBuilds(t, projectID)\n\tGetLoginProfile(t, GetGoogleIdentityEmailEnvVar(t))\n\t_, err = newGCRAuther()\n\trequire.NoError(t, err)\n\tbucket := \"gruntwork-terratest-\" + strings.ToLower(random.UniqueId())\n\tCreateStorageBucket(t, projectID, bucket, nil)\n\tdefer DeleteStorageBucket(t, bucket)\n}\n"
  },
  {
    "path": "modules/gcp/storage.go",
    "content": "package gcp\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\n\t\"cloud.google.com/go/storage\"\n\t\"github.com/gruntwork-io/terratest/modules/logger\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"google.golang.org/api/iterator\"\n)\n\n// CreateStorageBucket creates a Google Cloud bucket with the given BucketAttrs. Note that Google Storage bucket names must be globally unique.\nfunc CreateStorageBucket(t testing.TestingT, projectID string, name string, attr *storage.BucketAttrs) {\n\terr := CreateStorageBucketE(t, projectID, name, attr)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n\n// CreateStorageBucketE creates a Google Cloud bucket with the given BucketAttrs. Note that Google Storage bucket names must be globally unique.\nfunc CreateStorageBucketE(t testing.TestingT, projectID string, name string, attr *storage.BucketAttrs) error {\n\tlogger.Default.Logf(t, \"Creating bucket %s\", name)\n\n\tctx := context.Background()\n\n\t// Creates a client.\n\tclient, err := newStorageClient()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Creates a Bucket instance.\n\tbucket := client.Bucket(name)\n\n\t// Creates the new bucket.\n\treturn bucket.Create(ctx, projectID, attr)\n}\n\n// DeleteStorageBucket destroys the Google Storage bucket.\nfunc DeleteStorageBucket(t testing.TestingT, name string) {\n\terr := DeleteStorageBucketE(t, name)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n\n// DeleteStorageBucketE destroys the Google Cloud Storage bucket in the given region with the given name.\nfunc DeleteStorageBucketE(t testing.TestingT, name string) error {\n\tlogger.Default.Logf(t, \"Deleting bucket %s\", name)\n\n\tctx := context.Background()\n\n\tclient, err := newStorageClient()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn client.Bucket(name).Delete(ctx)\n}\n\n// ReadBucketObject reads an object from the given Storage Bucket and returns its contents.\nfunc ReadBucketObject(t testing.TestingT, bucketName string, filePath string) io.Reader {\n\tout, err := ReadBucketObjectE(t, bucketName, filePath)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn out\n}\n\n// ReadBucketObjectE reads an object from the given Storage Bucket and returns its contents.\nfunc ReadBucketObjectE(t testing.TestingT, bucketName string, filePath string) (io.Reader, error) {\n\tlogger.Default.Logf(t, \"Reading object from bucket %s using path %s\", bucketName, filePath)\n\n\tctx := context.Background()\n\n\tclient, err := newStorageClient()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tbucket := client.Bucket(bucketName)\n\tr, err := bucket.Object(filePath).NewReader(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn r, nil\n}\n\n// WriteBucketObject writes an object to the given Storage Bucket and returns its URL.\nfunc WriteBucketObject(t testing.TestingT, bucketName string, filePath string, body io.Reader, contentType string) string {\n\tout, err := WriteBucketObjectE(t, bucketName, filePath, body, contentType)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn out\n}\n\n// WriteBucketObjectE writes an object to the given Storage Bucket and returns its URL.\nfunc WriteBucketObjectE(t testing.TestingT, bucketName string, filePath string, body io.Reader, contentType string) (string, error) {\n\t// set a default content type\n\tif contentType == \"\" {\n\t\tcontentType = \"application/octet-stream\"\n\t}\n\n\tlogger.Default.Logf(t, \"Writing object to bucket %s using path %s and content type %s\", bucketName, filePath, contentType)\n\n\tctx := context.Background()\n\n\tclient, err := newStorageClient()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tw := client.Bucket(bucketName).Object(filePath).NewWriter(ctx)\n\tw.ContentType = contentType\n\n\t// Don't set any ACL or cache control properties for now\n\t//w.ACL = []storage.ACLRule{{Entity: storage.AllAuthenticatedUsers, Role: storage.RoleReader}}\n\t// set a default cache control (1 day)\n\t//w.CacheControl = \"public, max-age=86400\"\n\n\tif _, err := io.Copy(w, body); err != nil {\n\t\treturn \"\", err\n\t}\n\tif err := w.Close(); err != nil {\n\t\treturn \"\", err\n\t}\n\n\tconst publicURL = \"https://storage.googleapis.com/%s/%s\"\n\treturn fmt.Sprintf(publicURL, bucketName, filePath), nil\n}\n\n// EmptyStorageBucket removes the contents of a storage bucket with the given name.\nfunc EmptyStorageBucket(t testing.TestingT, name string) {\n\terr := EmptyStorageBucketE(t, name)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n\n// EmptyStorageBucketE removes the contents of a storage bucket with the given name.\nfunc EmptyStorageBucketE(t testing.TestingT, name string) error {\n\tlogger.Default.Logf(t, \"Emptying storage bucket %s\", name)\n\n\tctx := context.Background()\n\n\tclient, err := newStorageClient()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// List all objects in the bucket\n\t//\n\t// TODO - we should really do a bulk delete call here, but I couldn't find\n\t// anything in the SDK.\n\tbucket := client.Bucket(name)\n\tit := bucket.Objects(ctx, nil)\n\tfor {\n\t\tobjectAttrs, err := it.Next()\n\n\t\tif err == iterator.Done {\n\t\t\tbreak\n\t\t}\n\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t// purge the object\n\t\tlogger.Default.Logf(t, \"Deleting storage bucket object %s\", objectAttrs.Name)\n\t\tif err := bucket.Object(objectAttrs.Name).Delete(ctx); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// AssertStorageBucketExists checks if the given storage bucket exists and fails the test if it does not.\nfunc AssertStorageBucketExists(t testing.TestingT, name string) {\n\terr := AssertStorageBucketExistsE(t, name)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n\n// AssertStorageBucketExistsE checks if the given storage bucket exists and returns an error if it does not.\nfunc AssertStorageBucketExistsE(t testing.TestingT, name string) error {\n\tlogger.Default.Logf(t, \"Finding bucket %s\", name)\n\n\tctx := context.Background()\n\n\t// Creates a client.\n\tclient, err := newStorageClient()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Creates a Bucket instance.\n\tbucket := client.Bucket(name)\n\n\t// TODO - the code below attempts to determine whether the storage bucket\n\t// exists by making a number of API calls, then attempting to\n\t// list the contents of the bucket. It was adapted from Google's own integration\n\t// tests and should be improved once the appropriate API call is added.\n\t// For more info see: https://github.com/GoogleCloudPlatform/google-cloud-go/blob/de879f7be552d57556875b8aaa383bce9396cc8c/storage/integration_test.go#L1231\n\tif _, err := bucket.Attrs(ctx); err != nil {\n\t\t// ErrBucketNotExist\n\t\treturn err\n\t}\n\n\tit := bucket.Objects(ctx, nil)\n\tif _, err := it.Next(); err == storage.ErrBucketNotExist {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc newStorageClient() (*storage.Client, error) {\n\tctx := context.Background()\n\tclient, err := storage.NewClient(ctx, withOptions()...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn client, nil\n}\n"
  },
  {
    "path": "modules/gcp/storage_test.go",
    "content": "//go:build gcp\n// +build gcp\n\n// NOTE: We use build tags to differentiate GCP testing for better isolation and parallelism when executing our tests.\n\npackage gcp\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/logger\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestCreateAndDestroyStorageBucket(t *testing.T) {\n\tt.Parallel()\n\n\tprojectID := GetGoogleProjectIDFromEnvVar(t)\n\tid := random.UniqueId()\n\tgsBucketName := \"gruntwork-terratest-\" + strings.ToLower(id)\n\ttestFilePath := fmt.Sprintf(\"test-file-%s.txt\", random.UniqueId())\n\ttestFileBody := \"test file text\"\n\n\tlogger.Logf(t, \"Random values selected Bucket Name = %s, Test Filepath: %s\\n\", gsBucketName, testFilePath)\n\n\tCreateStorageBucket(t, projectID, gsBucketName, nil)\n\tdefer DeleteStorageBucket(t, gsBucketName)\n\n\t// Write a test file to the storage bucket\n\tobjectURL := WriteBucketObject(t, gsBucketName, testFilePath, strings.NewReader(testFileBody), \"text/plain\")\n\tlogger.Logf(t, \"Got URL: %s\", objectURL)\n\n\t// Then verify its contents matches the expected result\n\tfileReader := ReadBucketObject(t, gsBucketName, testFilePath)\n\n\tbuf := new(bytes.Buffer)\n\tbuf.ReadFrom(fileReader)\n\tresult := buf.String()\n\n\trequire.Equal(t, testFileBody, result)\n\n\t// Empty the storage bucket so we can delete it\n\tdefer EmptyStorageBucket(t, gsBucketName)\n}\n\nfunc TestAssertStorageBucketExistsNoFalseNegative(t *testing.T) {\n\tt.Parallel()\n\n\tprojectID := GetGoogleProjectIDFromEnvVar(t)\n\tid := random.UniqueId()\n\tgsBucketName := \"gruntwork-terratest-\" + strings.ToLower(id)\n\tlogger.Logf(t, \"Random values selected Id = %s\\n\", id)\n\n\tCreateStorageBucket(t, projectID, gsBucketName, nil)\n\tdefer DeleteStorageBucket(t, gsBucketName)\n\n\tAssertStorageBucketExists(t, gsBucketName)\n}\n\nfunc TestAssertStorageBucketExistsNoFalsePositive(t *testing.T) {\n\tt.Parallel()\n\n\tid := random.UniqueId()\n\tgsBucketName := \"gruntwork-terratest-\" + strings.ToLower(id)\n\tlogger.Logf(t, \"Random values selected Id = %s\\n\", id)\n\n\t// Don't create a new storage bucket so we can confirm that our function works as expected.\n\n\terr := AssertStorageBucketExistsE(t, gsBucketName)\n\tif err == nil {\n\t\tt.Fatalf(\"Function claimed that the Storage Bucket '%s' exists, but in fact it does not.\", gsBucketName)\n\t}\n}\n"
  },
  {
    "path": "modules/git/git.go",
    "content": "// Package git allows to interact with Git.\npackage git\n\nimport (\n\t\"context\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// GetCurrentBranchName retrieves the current branch name or an empty string\n// in case of detached state. Fails the test if an error occurs.\n//\n// Deprecated: Use [GetCurrentBranchNameContext] instead, which supports context\n// cancellation and accepts an explicit working directory rather than relying on\n// the process working directory.\nfunc GetCurrentBranchName(t testing.TestingT) string {\n\treturn GetCurrentBranchNameContext(t, context.Background(), \"\")\n}\n\n// GetCurrentBranchNameContext retrieves the current branch name or an empty\n// string in case of detached state. The dir parameter specifies the working\n// directory for the git command; if empty, the process working directory is\n// used. Fails the test if an error occurs.\nfunc GetCurrentBranchNameContext(t testing.TestingT, ctx context.Context, dir string) string {\n\tout, err := GetCurrentBranchNameContextE(t, ctx, dir)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\treturn out\n}\n\n// GetCurrentBranchNameE retrieves the current branch name or an empty string\n// in case of detached state. Uses git branch --show-current, which was\n// introduced in git v2.22. Falls back to git rev-parse for older versions.\n//\n// Deprecated: Use [GetCurrentBranchNameContextE] instead, which supports\n// context cancellation and accepts an explicit working directory rather than\n// relying on the process working directory.\nfunc GetCurrentBranchNameE(t testing.TestingT) (string, error) {\n\treturn GetCurrentBranchNameContextE(t, context.Background(), \"\")\n}\n\n// GetCurrentBranchNameContextE retrieves the current branch name or an empty\n// string in case of detached state. Uses git branch --show-current, which was\n// introduced in git v2.22. Falls back to git rev-parse for older versions.\n// The dir parameter specifies the working directory for the git command; if\n// empty, the process working directory is used.\nfunc GetCurrentBranchNameContextE(t testing.TestingT, ctx context.Context, dir string) (string, error) {\n\tcmd := exec.CommandContext(ctx, \"git\", \"branch\", \"--show-current\")\n\tcmd.Dir = dir\n\n\tbytes, err := cmd.Output()\n\tif err != nil {\n\t\treturn GetCurrentBranchNameOldContextE(t, ctx, dir)\n\t}\n\n\tname := strings.TrimSpace(string(bytes))\n\tif name == \"HEAD\" {\n\t\treturn \"\", nil\n\t}\n\n\treturn name, nil\n}\n\n// GetCurrentBranchNameOldE retrieves the current branch name or an empty\n// string in case of detached state using git rev-parse --abbrev-ref HEAD.\n//\n// Deprecated: Use [GetCurrentBranchNameOldContextE] instead, which supports\n// context cancellation and accepts an explicit working directory rather than\n// relying on the process working directory.\nfunc GetCurrentBranchNameOldE(t testing.TestingT) (string, error) {\n\treturn GetCurrentBranchNameOldContextE(t, context.Background(), \"\")\n}\n\n// GetCurrentBranchNameOldContextE retrieves the current branch name or an\n// empty string in case of detached state using git rev-parse --abbrev-ref HEAD.\n// This is a fallback for git versions older than v2.22 that lack\n// git branch --show-current. The dir parameter specifies the working directory\n// for the git command; if empty, the process working directory is used.\nfunc GetCurrentBranchNameOldContextE(t testing.TestingT, ctx context.Context, dir string) (string, error) {\n\tcmd := exec.CommandContext(ctx, \"git\", \"rev-parse\", \"--abbrev-ref\", \"HEAD\")\n\tcmd.Dir = dir\n\n\tbytes, err := cmd.Output()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tname := strings.TrimSpace(string(bytes))\n\tif name == \"HEAD\" {\n\t\treturn \"\", nil\n\t}\n\n\treturn name, nil\n}\n\n// GetCurrentGitRef retrieves the current branch name, lightweight\n// (non-annotated) tag, or exact tag value if the tag points to the current\n// commit. Fails the test if an error occurs.\n//\n// Deprecated: Use [GetCurrentGitRefContext] instead, which supports context\n// cancellation and accepts an explicit working directory rather than relying on\n// the process working directory.\nfunc GetCurrentGitRef(t testing.TestingT) string {\n\treturn GetCurrentGitRefContext(t, context.Background(), \"\")\n}\n\n// GetCurrentGitRefContext retrieves the current branch name, lightweight\n// (non-annotated) tag, or exact tag value if the tag points to the current\n// commit. The dir parameter specifies the working directory for the git\n// command; if empty, the process working directory is used. Fails the test if\n// an error occurs.\nfunc GetCurrentGitRefContext(t testing.TestingT, ctx context.Context, dir string) string {\n\tout, err := GetCurrentGitRefContextE(t, ctx, dir)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\treturn out\n}\n\n// GetCurrentGitRefE retrieves the current branch name, lightweight\n// (non-annotated) tag, or exact tag value if the tag points to the current\n// commit.\n//\n// Deprecated: Use [GetCurrentGitRefContextE] instead, which supports context\n// cancellation and accepts an explicit working directory rather than relying on\n// the process working directory.\nfunc GetCurrentGitRefE(t testing.TestingT) (string, error) {\n\treturn GetCurrentGitRefContextE(t, context.Background(), \"\")\n}\n\n// GetCurrentGitRefContextE retrieves the current branch name, lightweight\n// (non-annotated) tag, or exact tag value if the tag points to the current\n// commit. The dir parameter specifies the working directory for the git\n// command; if empty, the process working directory is used.\nfunc GetCurrentGitRefContextE(t testing.TestingT, ctx context.Context, dir string) (string, error) {\n\tout, err := GetCurrentBranchNameContextE(t, ctx, dir)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif out != \"\" {\n\t\treturn out, nil\n\t}\n\n\tout, err = GetTagContextE(t, ctx, dir)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn out, nil\n}\n\n// GetTagE retrieves the lightweight (non-annotated) tag or exact tag value if\n// the tag points to the current commit.\n//\n// Deprecated: Use [GetTagContextE] instead, which supports context\n// cancellation and accepts an explicit working directory rather than relying on\n// the process working directory.\nfunc GetTagE(t testing.TestingT) (string, error) {\n\treturn GetTagContextE(t, context.Background(), \"\")\n}\n\n// GetTagContextE retrieves the lightweight (non-annotated) tag or exact tag\n// value if the tag points to the current commit. The dir parameter specifies\n// the working directory for the git command; if empty, the process working\n// directory is used.\nfunc GetTagContextE(t testing.TestingT, ctx context.Context, dir string) (string, error) {\n\tcmd := exec.CommandContext(ctx, \"git\", \"describe\", \"--tags\")\n\tcmd.Dir = dir\n\n\tbytes, err := cmd.Output()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn strings.TrimSpace(string(bytes)), nil\n}\n\n// GetRepoRoot retrieves the path to the root directory of the repo. Fails the\n// test if there is an error.\n//\n// Deprecated: Use [GetRepoRootContext] instead, which supports context\n// cancellation and accepts an explicit working directory rather than relying on\n// the process working directory.\nfunc GetRepoRoot(t testing.TestingT) string {\n\treturn GetRepoRootContext(t, context.Background(), \"\")\n}\n\n// GetRepoRootContext retrieves the path to the root directory of the repo. The\n// dir parameter specifies the working directory for the git command; if empty,\n// the process working directory is used. Fails the test if there is an error.\nfunc GetRepoRootContext(t testing.TestingT, ctx context.Context, dir string) string {\n\tout, err := GetRepoRootContextE(t, ctx, dir)\n\trequire.NoError(t, err)\n\n\treturn out\n}\n\n// GetRepoRootE retrieves the path to the root directory of the repo.\n//\n// Deprecated: Use [GetRepoRootContextE] instead, which supports context\n// cancellation and accepts an explicit working directory rather than relying on\n// the process working directory.\nfunc GetRepoRootE(t testing.TestingT) (string, error) {\n\treturn GetRepoRootContextE(t, context.Background(), \"\")\n}\n\n// GetRepoRootContextE retrieves the path to the root directory of the repo.\n// The dir parameter specifies the working directory for the git command; if\n// empty, the process working directory is used.\nfunc GetRepoRootContextE(t testing.TestingT, ctx context.Context, dir string) (string, error) {\n\tif dir == \"\" {\n\t\tcwd, err := os.Getwd()\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\tdir = cwd\n\t}\n\n\treturn GetRepoRootForDirContextE(t, ctx, dir)\n}\n\n// GetRepoRootForDir retrieves the path to the root directory of the repo in\n// which dir resides. Fails the test if there is an error.\n//\n// Deprecated: Use [GetRepoRootForDirContext] instead, which supports context\n// cancellation.\nfunc GetRepoRootForDir(t testing.TestingT, dir string) string {\n\treturn GetRepoRootForDirContext(t, context.Background(), dir)\n}\n\n// GetRepoRootForDirContext retrieves the path to the root directory of the\n// repo in which dir resides. Fails the test if there is an error.\nfunc GetRepoRootForDirContext(t testing.TestingT, ctx context.Context, dir string) string {\n\tout, err := GetRepoRootForDirContextE(t, ctx, dir)\n\trequire.NoError(t, err)\n\n\treturn out\n}\n\n// GetRepoRootForDirE retrieves the path to the root directory of the repo in\n// which dir resides.\n//\n// Deprecated: Use [GetRepoRootForDirContextE] instead, which supports context\n// cancellation.\nfunc GetRepoRootForDirE(t testing.TestingT, dir string) (string, error) {\n\treturn GetRepoRootForDirContextE(t, context.Background(), dir)\n}\n\n// GetRepoRootForDirContextE retrieves the path to the root directory of the\n// repo in which dir resides.\nfunc GetRepoRootForDirContextE(t testing.TestingT, ctx context.Context, dir string) (string, error) {\n\tcmd := exec.CommandContext(ctx, \"git\", \"rev-parse\", \"--show-toplevel\")\n\tcmd.Dir = dir\n\n\tbytes, err := cmd.Output()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn strings.TrimSpace(string(bytes)), nil\n}\n"
  },
  {
    "path": "modules/git/git_test.go",
    "content": "package git_test\n\nimport (\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/git\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n//nolint:paralleltest,tparallel // subtests mutate shared git state (checkout) and must run sequentially\nfunc TestGitRefChecks(t *testing.T) {\n\tt.Parallel()\n\n\ttmpdir := t.TempDir()\n\tgitWorkDir := filepath.Join(tmpdir, \"terratest\")\n\n\turl := \"https://github.com/gruntwork-io/terratest.git\"\n\terr := exec.CommandContext(t.Context(), \"git\", \"clone\", url, gitWorkDir).Run()\n\trequire.NoError(t, err)\n\n\tt.Run(\"GetCurrentBranchNameReturnsBranchName\", func(t *testing.T) {\n\t\terr := exec.CommandContext(t.Context(), \"git\", \"-C\", gitWorkDir, \"checkout\", \"main\").Run()\n\t\trequire.NoError(t, err)\n\n\t\tname := git.GetCurrentBranchNameContext(t, t.Context(), gitWorkDir)\n\n\t\tassert.Equal(t, \"main\", name)\n\t})\n\n\tt.Run(\"GetCurrentBranchNameReturnsEmptyForDetachedState\", func(t *testing.T) {\n\t\terr := exec.CommandContext(t.Context(), \"git\", \"-C\", gitWorkDir, \"checkout\", \"v0.0.1\").Run()\n\t\trequire.NoError(t, err)\n\n\t\tname := git.GetCurrentBranchNameContext(t, t.Context(), gitWorkDir)\n\n\t\tassert.Empty(t, name)\n\t})\n\n\tt.Run(\"GetCurrentRefReturnsBranchName\", func(t *testing.T) {\n\t\terr := exec.CommandContext(t.Context(), \"git\", \"-C\", gitWorkDir, \"checkout\", \"main\").Run()\n\t\trequire.NoError(t, err)\n\n\t\tname := git.GetCurrentGitRefContext(t, t.Context(), gitWorkDir)\n\n\t\tassert.Equal(t, \"main\", name)\n\t})\n\n\tt.Run(\"GetCurrentRefReturnsTagValue\", func(t *testing.T) {\n\t\terr := exec.CommandContext(t.Context(), \"git\", \"-C\", gitWorkDir, \"checkout\", \"v0.0.1\").Run()\n\t\trequire.NoError(t, err)\n\n\t\tname := git.GetCurrentGitRefContext(t, t.Context(), gitWorkDir)\n\n\t\tassert.Equal(t, \"v0.0.1\", name)\n\t})\n\n\tt.Run(\"GetCurrentRefReturnsLightTagValue\", func(t *testing.T) {\n\t\terr := exec.CommandContext(t.Context(), \"git\", \"-C\", gitWorkDir, \"checkout\", \"58d3ea8\").Run()\n\t\trequire.NoError(t, err)\n\n\t\tname := git.GetCurrentGitRefContext(t, t.Context(), gitWorkDir)\n\n\t\tassert.Equal(t, \"v0.0.1-1-g58d3ea8f\", name)\n\t})\n}\n\nfunc TestGetRepoRoot(t *testing.T) {\n\tt.Parallel()\n\n\tcwd, err := os.Getwd()\n\trequire.NoError(t, err)\n\n\texpectedRepoRoot, err := filepath.Abs(filepath.Join(cwd, \"..\", \"..\"))\n\trequire.NoError(t, err)\n\n\trepoRoot := git.GetRepoRootContext(t, t.Context(), cwd)\n\tassert.Equal(t, expectedRepoRoot, repoRoot)\n}\n"
  },
  {
    "path": "modules/helm/cmd.go",
    "content": "package helm\n\nimport (\n\t\"slices\"\n\n\t\"github.com/gruntwork-io/go-commons/errors\"\n\t\"github.com/gruntwork-io/terratest/modules/shell\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n)\n\n// getCommonArgs extracts common helm options. In this case, these are:\n// - kubeconfig path\n// - kubeconfig context\n// - helm home path\nfunc getCommonArgs(options *Options, args ...string) []string {\n\tif options.KubectlOptions != nil && options.KubectlOptions.ContextName != \"\" {\n\t\targs = append(args, \"--kube-context\", options.KubectlOptions.ContextName)\n\t}\n\tif options.KubectlOptions != nil && options.KubectlOptions.ConfigPath != \"\" {\n\t\targs = append(args, \"--kubeconfig\", options.KubectlOptions.ConfigPath)\n\t}\n\tif options.HomePath != \"\" {\n\t\targs = append(args, \"--home\", options.HomePath)\n\t}\n\treturn args\n}\n\n// getNamespaceArgs returns the args to append for the namespace, if set in the helm Options struct.\nfunc getNamespaceArgs(options *Options) []string {\n\tif options.KubectlOptions != nil && options.KubectlOptions.Namespace != \"\" {\n\t\treturn []string{\"--namespace\", options.KubectlOptions.Namespace}\n\t}\n\treturn []string{}\n}\n\n// getValuesArgsE computes the args to pass in for setting values\nfunc getValuesArgsE(t testing.TestingT, options *Options, args ...string) ([]string, error) {\n\targs = append(args, formatSetValuesAsArgs(options.SetValues, \"--set\")...)\n\targs = append(args, formatSetValuesAsArgs(options.SetStrValues, \"--set-string\")...)\n\targs = append(args, formatSetValuesAsArgs(options.SetJsonValues, \"--set-json\")...)\n\n\tvaluesFilesArgs, err := formatValuesFilesAsArgsE(t, options.ValuesFiles)\n\tif err != nil {\n\t\treturn args, errors.WithStackTrace(err)\n\t}\n\targs = append(args, valuesFilesArgs...)\n\n\tsetFilesArgs, err := formatSetFilesAsArgsE(t, options.SetFiles)\n\tif err != nil {\n\t\treturn args, errors.WithStackTrace(err)\n\t}\n\targs = append(args, setFilesArgs...)\n\treturn args, nil\n}\n\n// RunHelmCommandAndGetOutputE runs helm with the given arguments and options and returns combined, interleaved stdout/stderr.\nfunc RunHelmCommandAndGetOutputE(t testing.TestingT, options *Options, cmd string, additionalArgs ...string) (string, error) {\n\thelmCmd := prepareHelmCommand(t, options, cmd, additionalArgs...)\n\treturn shell.RunCommandAndGetOutputE(t, helmCmd)\n}\n\n// RunHelmCommandAndGetStdOutE runs helm with the given arguments and options and returns stdout.\nfunc RunHelmCommandAndGetStdOutE(t testing.TestingT, options *Options, cmd string, additionalArgs ...string) (string, error) {\n\thelmCmd := prepareHelmCommand(t, options, cmd, additionalArgs...)\n\treturn shell.RunCommandAndGetStdOutE(t, helmCmd)\n}\n\n// RunHelmCommandAndGetStdOutErrE runs helm with the given arguments and options and returns stdout and stderr separately.\nfunc RunHelmCommandAndGetStdOutErrE(t testing.TestingT, options *Options, cmd string, additionalArgs ...string) (string, string, error) {\n\thelmCmd := prepareHelmCommand(t, options, cmd, additionalArgs...)\n\treturn shell.RunCommandAndGetStdOutErrE(t, helmCmd)\n}\n\nfunc prepareHelmCommand(t testing.TestingT, options *Options, cmd string, additionalArgs ...string) shell.Command {\n\targs := []string{cmd}\n\targs = getCommonArgs(options, args...)\n\t// name space arg only append if it is not there\n\tif !slices.Contains(additionalArgs, \"--namespace\") {\n\t\targs = append(args, getNamespaceArgs(options)...)\n\t}\n\targs = append(args, additionalArgs...)\n\n\thelmCmd := shell.Command{\n\t\tCommand:    \"helm\",\n\t\tArgs:       args,\n\t\tWorkingDir: \".\",\n\t\tEnv:        options.EnvVars,\n\t\tLogger:     options.Logger,\n\t}\n\treturn helmCmd\n}\n"
  },
  {
    "path": "modules/helm/cmd_test.go",
    "content": "package helm\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/k8s\"\n\t\"github.com/gruntwork-io/terratest/modules/logger\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestPrepareHelmCommand(t *testing.T) {\n\tt.Parallel()\n\n\toptions := &Options{\n\t\tKubectlOptions: &k8s.KubectlOptions{\n\t\t\tNamespace: \"test-namespace\",\n\t\t},\n\t\tEnvVars: map[string]string{\"SampleEnv\": \"test_value\"},\n\t\tLogger:  logger.Default,\n\t}\n\tt.Run(\"command without additional args\", func(t *testing.T) {\n\t\tcmd := prepareHelmCommand(t, options, \"install\")\n\t\tassert.Equal(t, \"helm\", cmd.Command)\n\t\tassert.Contains(t, cmd.Args, \"install\")\n\t\tassert.Contains(t, cmd.Args, \"--namespace\")\n\t\tassert.Contains(t, cmd.Args, \"test-namespace\")\n\t\tassert.Equal(t, \".\", cmd.WorkingDir)\n\t\tassert.Equal(t, options.EnvVars, cmd.Env)\n\t\tassert.Equal(t, options.Logger, cmd.Logger)\n\t})\n\tt.Run(\"Command with additional args\", func(t *testing.T) {\n\t\tcmd := prepareHelmCommand(t, options, \"upgrade\", \"--install\", \"my-release\", \"my-chart\")\n\t\tassert.Equal(t, \"helm\", cmd.Command)\n\t\tassert.Contains(t, cmd.Args, \"upgrade\")\n\t\tassert.Contains(t, cmd.Args, \"--install\")\n\t\tassert.Contains(t, cmd.Args, \"my-release\")\n\t\tassert.Contains(t, cmd.Args, \"my-chart\")\n\t\tassert.Contains(t, cmd.Args, \"--namespace\")\n\t\tassert.Contains(t, cmd.Args, \"test-namespace\")\n\t})\n\n\tt.Run(\"Command with namespace in additional args\", func(t *testing.T) {\n\t\tcmd := prepareHelmCommand(t, options, \"install\", \"--namespace\", \"custom-namespace\")\n\t\tassert.Equal(t, \"helm\", cmd.Command)\n\t\tassert.Contains(t, cmd.Args, \"install\")\n\t\tassert.Contains(t, cmd.Args, \"--namespace\")\n\t\tassert.Contains(t, cmd.Args, \"custom-namespace\")\n\t\tassert.NotContains(t, cmd.Args, \"test-namespace\")\n\t})\n}\n"
  },
  {
    "path": "modules/helm/delete.go",
    "content": "package helm\n\nimport (\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// Delete will delete the provided release from Tiller. If you set purge to true, Tiller will delete the release object\n// as well so that the release name can be reused. This will fail the test if there is an error.\nfunc Delete(t testing.TestingT, options *Options, releaseName string, purge bool) {\n\trequire.NoError(t, DeleteE(t, options, releaseName, purge))\n}\n\n// DeleteE will delete the provided release from Tiller. If you set purge to true, Tiller will delete the release object\n// as well so that the release name can be reused.\nfunc DeleteE(t testing.TestingT, options *Options, releaseName string, purge bool) error {\n\targs := []string{}\n\tif !purge {\n\t\targs = append(args, \"--keep-history\")\n\t}\n\tif options.ExtraArgs != nil {\n\t\tif deleteArgs, ok := options.ExtraArgs[\"delete\"]; ok {\n\t\t\targs = append(args, deleteArgs...)\n\t\t}\n\t}\n\targs = append(args, releaseName)\n\t_, err := RunHelmCommandAndGetOutputE(t, options, \"delete\", args...)\n\treturn err\n}\n"
  },
  {
    "path": "modules/helm/errors.go",
    "content": "package helm\n\nimport (\n\t\"fmt\"\n)\n\n// ValuesFileNotFoundError is returned when a provided values file input is not found on the host path.\ntype ValuesFileNotFoundError struct {\n\tPath string\n}\n\nfunc (err ValuesFileNotFoundError) Error() string {\n\treturn fmt.Sprintf(\"Could not resolve values file %s\", err.Path)\n}\n\n// SetFileNotFoundError is returned when a provided set file input is not found on the host path.\ntype SetFileNotFoundError struct {\n\tPath string\n}\n\nfunc (err SetFileNotFoundError) Error() string {\n\treturn fmt.Sprintf(\"Could not resolve set file path %s\", err.Path)\n}\n\n// TemplateFileNotFoundError is returned when a provided template file input is not found in the chart\ntype TemplateFileNotFoundError struct {\n\tPath     string\n\tChartDir string\n}\n\nfunc (err TemplateFileNotFoundError) Error() string {\n\treturn fmt.Sprintf(\"Could not resolve template file %s relative to chart path %s\", err.Path, err.ChartDir)\n}\n\n// ChartNotFoundError is returned when a provided chart dir is not found\ntype ChartNotFoundError struct {\n\tPath string\n}\n\nfunc (err ChartNotFoundError) Error() string {\n\treturn fmt.Sprintf(\"Could not chart path %s\", err.Path)\n}\n"
  },
  {
    "path": "modules/helm/format.go",
    "content": "package helm\n\nimport (\n\t\"fmt\"\n\t\"path/filepath\"\n\n\t\"github.com/gruntwork-io/go-commons/collections\"\n\t\"github.com/gruntwork-io/go-commons/errors\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/gruntwork-io/terratest/modules/files\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n)\n\n// formatSetValuesAsArgs formats the given values as command line args for helm using the given flag (e.g flags of\n// the format \"--set\"/\"--set-string\"/\"--set-json\" resulting in args like --set/set-string/set-json key=value...)\nfunc formatSetValuesAsArgs(setValues map[string]string, flag string) []string {\n\targs := []string{}\n\n\t// To make it easier to test, go through the keys in sorted order\n\tkeys := collections.Keys(setValues)\n\tfor _, key := range keys {\n\t\tvalue := setValues[key]\n\t\targValue := fmt.Sprintf(\"%s=%s\", key, value)\n\t\targs = append(args, flag, argValue)\n\t}\n\n\treturn args\n}\n\n// formatValuesFilesAsArgs formats the given list of values file paths as command line args for helm (e.g of the format\n// -f path). This will fail the test if one of the paths do not exist or the absolute path can not be determined.\nfunc formatValuesFilesAsArgs(t testing.TestingT, valuesFiles []string) []string {\n\targs, err := formatValuesFilesAsArgsE(t, valuesFiles)\n\trequire.NoError(t, err)\n\treturn args\n}\n\n// formatValuesFilesAsArgsE formats the given list of values file paths as command line args for helm (e.g of the format\n// -f path). This will error if the file does not exist.\nfunc formatValuesFilesAsArgsE(t testing.TestingT, valuesFiles []string) ([]string, error) {\n\targs := []string{}\n\n\tfor _, valuesFilePath := range valuesFiles {\n\t\t// Pass through filepath.Abs to clean the path, and then make sure this file exists\n\t\tabsValuesFilePath, err := filepath.Abs(valuesFilePath)\n\t\tif err != nil {\n\t\t\treturn args, errors.WithStackTrace(err)\n\t\t}\n\t\tif !files.FileExists(absValuesFilePath) {\n\t\t\treturn args, errors.WithStackTrace(ValuesFileNotFoundError{valuesFilePath})\n\t\t}\n\t\targs = append(args, \"-f\", absValuesFilePath)\n\t}\n\n\treturn args, nil\n}\n\n// formatSetFilesAsArgs formats the given list of keys and file paths as command line args for helm to set from file\n// (e.g of the format --set-file key=path). This will fail the test if one of the paths do not exist or the absolute\n// path can not be determined.\nfunc formatSetFilesAsArgs(t testing.TestingT, setFiles map[string]string) []string {\n\targs, err := formatSetFilesAsArgsE(t, setFiles)\n\trequire.NoError(t, err)\n\treturn args\n}\n\n// formatSetFilesAsArgsE formats the given list of keys and file paths as command line args for helm to set from file\n// (e.g of the format --set-file key=path)\nfunc formatSetFilesAsArgsE(t testing.TestingT, setFiles map[string]string) ([]string, error) {\n\targs := []string{}\n\n\t// To make it easier to test, go through the keys in sorted order\n\tkeys := collections.Keys(setFiles)\n\tfor _, key := range keys {\n\t\tsetFilePath := setFiles[key]\n\t\t// Pass through filepath.Abs to clean the path, and then make sure this file exists\n\t\tabsSetFilePath, err := filepath.Abs(setFilePath)\n\t\tif err != nil {\n\t\t\treturn args, errors.WithStackTrace(err)\n\t\t}\n\t\tif !files.FileExists(absSetFilePath) {\n\t\t\treturn args, errors.WithStackTrace(SetFileNotFoundError{setFilePath})\n\t\t}\n\t\targValue := fmt.Sprintf(\"%s=%s\", key, absSetFilePath)\n\t\targs = append(args, \"--set-file\", argValue)\n\t}\n\n\treturn args, nil\n}\n"
  },
  {
    "path": "modules/helm/format_test.go",
    "content": "package helm\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestFormatSetValuesAsArgs(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tname          string\n\t\tsetValues     map[string]string\n\t\tsetStrValues  map[string]string\n\t\tsetJsonValues map[string]string\n\t\texpected      []string\n\t\texpectedStr   []string\n\t\texpectedJson  []string\n\t}{\n\t\t{\n\t\t\t\"EmptyValue\",\n\t\t\tmap[string]string{},\n\t\t\tmap[string]string{},\n\t\t\tmap[string]string{},\n\t\t\t[]string{},\n\t\t\t[]string{},\n\t\t\t[]string{},\n\t\t},\n\t\t{\n\t\t\t\"SingleValue\",\n\t\t\tmap[string]string{\"containerImage\": \"null\"},\n\t\t\tmap[string]string{\"numericString\": \"123123123123\"},\n\t\t\tmap[string]string{\"limits\": `{\"cpu\": 1}`},\n\t\t\t[]string{\"--set\", \"containerImage=null\"},\n\t\t\t[]string{\"--set-string\", \"numericString=123123123123\"},\n\t\t\t[]string{\"--set-json\", fmt.Sprintf(\"limits=%s\", `{\"cpu\": 1}`)},\n\t\t},\n\t\t{\n\t\t\t\"MultipleValues\",\n\t\t\tmap[string]string{\n\t\t\t\t\"containerImage.repository\": \"nginx\",\n\t\t\t\t\"containerImage.tag\":        \"v1.15.4\",\n\t\t\t},\n\t\t\tmap[string]string{\n\t\t\t\t\"numericString\": \"123123123123\",\n\t\t\t\t\"otherString\":   \"null\",\n\t\t\t},\n\t\t\tmap[string]string{\n\t\t\t\t\"containerImage\": `{\"repository\": \"nginx\", \"tag\": \"v1.15.4\"}`,\n\t\t\t\t\"otherString\":    \"{}\",\n\t\t\t},\n\t\t\t[]string{\n\t\t\t\t\"--set\", \"containerImage.repository=nginx\",\n\t\t\t\t\"--set\", \"containerImage.tag=v1.15.4\",\n\t\t\t},\n\t\t\t[]string{\n\t\t\t\t\"--set-string\", \"numericString=123123123123\",\n\t\t\t\t\"--set-string\", \"otherString=null\",\n\t\t\t},\n\t\t\t[]string{\n\t\t\t\t\"--set-json\", fmt.Sprintf(\"containerImage=%s\", `{\"repository\": \"nginx\", \"tag\": \"v1.15.4\"}`),\n\t\t\t\t\"--set-json\", \"otherString={}\",\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, testCase := range testCases {\n\t\t// Capture the range value and force it into this scope. Otherwise, it is defined outside this block so it can\n\t\t// change when the subtests parallelize and switch contexts.\n\t\ttestCase := testCase\n\n\t\tt.Run(testCase.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tassert.Equal(t, formatSetValuesAsArgs(testCase.setValues, \"--set\"), testCase.expected)\n\t\t\tassert.Equal(t, formatSetValuesAsArgs(testCase.setStrValues, \"--set-string\"), testCase.expectedStr)\n\t\t\tassert.Equal(t, formatSetValuesAsArgs(testCase.setJsonValues, \"--set-json\"), testCase.expectedJson)\n\t\t})\n\t}\n}\n\nfunc TestFormatSetFilesAsArgs(t *testing.T) {\n\tt.Parallel()\n\n\tpaths, err := createTempFiles(2)\n\tdefer deleteTempFiles(paths)\n\trequire.NoError(t, err)\n\tabsPathList := absPaths(t, paths)\n\n\ttestCases := []struct {\n\t\tname     string\n\t\tsetFiles map[string]string\n\t\texpected []string\n\t}{\n\t\t{\n\t\t\t\"EmptyValue\",\n\t\t\tmap[string]string{},\n\t\t\t[]string{},\n\t\t},\n\t\t{\n\t\t\t\"SingleValue\",\n\t\t\tmap[string]string{\"containerImage\": paths[0]},\n\t\t\t[]string{\"--set-file\", fmt.Sprintf(\"containerImage=%s\", absPathList[0])},\n\t\t},\n\t\t{\n\t\t\t\"MultipleValues\",\n\t\t\tmap[string]string{\n\t\t\t\t\"containerImage.repository\": paths[0],\n\t\t\t\t\"containerImage.tag\":        paths[1],\n\t\t\t},\n\t\t\t[]string{\n\t\t\t\t\"--set-file\", fmt.Sprintf(\"containerImage.repository=%s\", absPathList[0]),\n\t\t\t\t\"--set-file\", fmt.Sprintf(\"containerImage.tag=%s\", absPathList[1]),\n\t\t\t},\n\t\t},\n\t}\n\n\t// We create a subtest group that is NOT parallel, so the main test waits for all the tests to finish. This way, we\n\t// don't delete the files until the subtests finish.\n\tt.Run(\"group\", func(t *testing.T) {\n\t\tfor _, testCase := range testCases {\n\t\t\t// Capture the range value and force it into this scope. Otherwise, it is defined outside this block so it can\n\t\t\t// change when the subtests parallelize and switch contexts.\n\t\t\ttestCase := testCase\n\n\t\t\tt.Run(testCase.name, func(t *testing.T) {\n\t\t\t\tt.Parallel()\n\t\t\t\tassert.Equal(t, formatSetFilesAsArgs(t, testCase.setFiles), testCase.expected)\n\t\t\t})\n\t\t}\n\t})\n}\n\nfunc TestFormatValuesFilesAsArgs(t *testing.T) {\n\tt.Parallel()\n\n\tpaths, err := createTempFiles(2)\n\tdefer deleteTempFiles(paths)\n\trequire.NoError(t, err)\n\tabsPathList := absPaths(t, paths)\n\n\ttestCases := []struct {\n\t\tname        string\n\t\tvaluesFiles []string\n\t\texpected    []string\n\t}{\n\t\t{\n\t\t\t\"EmptyValue\",\n\t\t\t[]string{},\n\t\t\t[]string{},\n\t\t},\n\t\t{\n\t\t\t\"SingleValue\",\n\t\t\t[]string{paths[0]},\n\t\t\t[]string{\"-f\", absPathList[0]},\n\t\t},\n\t\t{\n\t\t\t\"MultipleValues\",\n\t\t\tpaths,\n\t\t\t[]string{\n\t\t\t\t\"-f\", absPathList[0],\n\t\t\t\t\"-f\", absPathList[1],\n\t\t\t},\n\t\t},\n\t}\n\n\t// We create a subtest group that is NOT parallel, so the main test waits for all the tests to finish. This way, we\n\t// don't delete the files until the subtests finish.\n\tt.Run(\"group\", func(t *testing.T) {\n\t\tfor _, testCase := range testCases {\n\t\t\t// Capture the range value and force it into this scope. Otherwise, it is defined outside this block so it can\n\t\t\t// change when the subtests parallelize and switch contexts.\n\t\t\ttestCase := testCase\n\n\t\t\tt.Run(testCase.name, func(t *testing.T) {\n\t\t\t\tt.Parallel()\n\t\t\t\tassert.Equal(t, formatValuesFilesAsArgs(t, testCase.valuesFiles), testCase.expected)\n\t\t\t})\n\t\t}\n\t})\n}\n\n// createTempFiles will create numFiles temporary files that can pass the abspath checks.\nfunc createTempFiles(numFiles int) ([]string, error) {\n\tpaths := []string{}\n\tfor i := 0; i < numFiles; i++ {\n\t\ttmpFile, err := os.CreateTemp(\"\", \"\")\n\t\tdefer tmpFile.Close()\n\t\t// We don't use require or t.Fatal here so that we give a chance to delete any temp files that were created\n\t\t// before this error\n\t\tif err != nil {\n\t\t\treturn paths, err\n\t\t}\n\t\tpaths = append(paths, tmpFile.Name())\n\t}\n\treturn paths, nil\n}\n\n// deleteTempFiles will delete all the given temp file paths\nfunc deleteTempFiles(paths []string) {\n\tfor _, path := range paths {\n\t\tos.Remove(path)\n\t}\n}\n\n// absPaths will return the absolute paths of each path in the list\nfunc absPaths(t *testing.T, paths []string) []string {\n\tout := []string{}\n\tfor _, path := range paths {\n\t\tabsPath, err := filepath.Abs(path)\n\t\trequire.NoError(t, err)\n\t\tout = append(out, absPath)\n\t}\n\treturn out\n}\n"
  },
  {
    "path": "modules/helm/helm.go",
    "content": "// Package helm provides common functionalities for testing helm charts, such as calling out to the helm client.\npackage helm\n"
  },
  {
    "path": "modules/helm/install.go",
    "content": "package helm\n\nimport (\n\t\"path/filepath\"\n\n\t\"github.com/gruntwork-io/go-commons/errors\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/gruntwork-io/terratest/modules/files\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n)\n\n// Install will install the selected helm chart with the provided options under the given release name. This will fail\n// the test if there is an error.\nfunc Install(t testing.TestingT, options *Options, chart string, releaseName string) {\n\trequire.NoError(t, InstallE(t, options, chart, releaseName))\n}\n\n// InstallE will install the selected helm chart with the provided options under the given release name.\nfunc InstallE(t testing.TestingT, options *Options, chart string, releaseName string) error {\n\t// If the chart refers to a path, convert to absolute path. Otherwise, pass straight through as it may be a remote\n\t// chart.\n\tif files.FileExists(chart) {\n\t\tabsChartDir, err := filepath.Abs(chart)\n\t\tif err != nil {\n\t\t\treturn errors.WithStackTrace(err)\n\t\t}\n\t\tchart = absChartDir\n\t}\n\n\t// build chart dependencies\n\tif options.BuildDependencies {\n\t\tif _, err := RunHelmCommandAndGetOutputE(t, options, \"dependency\", \"build\", chart); err != nil {\n\t\t\treturn errors.WithStackTrace(err)\n\t\t}\n\t}\n\t// Now call out to helm install to install the charts with the provided options\n\t// Declare err here so that we can update args later\n\tvar err error\n\targs := []string{}\n\tif options.ExtraArgs != nil {\n\t\tif installArgs, ok := options.ExtraArgs[\"install\"]; ok {\n\t\t\targs = append(args, installArgs...)\n\t\t}\n\t}\n\tif options.Version != \"\" {\n\t\targs = append(args, \"--version\", options.Version)\n\t}\n\targs, err = getValuesArgsE(t, options, args...)\n\tif err != nil {\n\t\treturn err\n\t}\n\targs = append(args, releaseName, chart)\n\t_, err = RunHelmCommandAndGetOutputE(t, options, \"install\", args...)\n\treturn err\n}\n"
  },
  {
    "path": "modules/helm/install_test.go",
    "content": "//go:build kubeall || helm\n// +build kubeall helm\n\n// NOTE: we have build tags to differentiate kubernetes tests from non-kubernetes tests, and further differentiate helm\n// tests. This is done because minikube is heavy and can interfere with docker related tests in terratest. Similarly,\n// helm can overload the minikube system and thus interfere with the other kubernetes tests. To avoid overloading the\n// system, we run the kubernetes tests and helm tests separately from the others.\n\npackage helm\n\nimport (\n\t\"crypto/tls\"\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\n\thttp_helper \"github.com/gruntwork-io/terratest/modules/http-helper\"\n\t\"github.com/gruntwork-io/terratest/modules/k8s\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst (\n\tremoteChartSource  = \"https://charts.bitnami.com/bitnami\"\n\tremoteChartName    = \"nginx\"\n\tremoteChartVersion = \"22.4.0\"\n)\n\n// Test that we can install a remote chart (e.g bitnami/nginx)\nfunc TestRemoteChartInstall(t *testing.T) {\n\tt.Parallel()\n\n\tnamespaceName := fmt.Sprintf(\n\t\t\"%s-%s\",\n\t\tstrings.ToLower(t.Name()),\n\t\tstrings.ToLower(random.UniqueId()),\n\t)\n\n\t// Use default kubectl options to create a new namespace for this test, and then update the namespace for kubectl\n\tkubectlOptions := k8s.NewKubectlOptions(\"\", \"\", namespaceName)\n\n\tdefer k8s.DeleteNamespace(t, kubectlOptions, namespaceName)\n\tk8s.CreateNamespace(t, kubectlOptions, namespaceName)\n\n\t// Override service type to node port and disable PDB (requires policy/v1 API\n\t// which may not be available on older k8s clusters)\n\toptions := &Options{\n\t\tKubectlOptions: kubectlOptions,\n\t\tSetValues: map[string]string{\n\t\t\t\"service.type\": \"NodePort\",\n\t\t\t\"pdb.create\":   \"false\",\n\t\t},\n\t\tVersion: remoteChartVersion,\n\t}\n\n\t// Add the stable repo under a random name so as not to touch existing repo configs\n\tuniqueName := strings.ToLower(fmt.Sprintf(\"terratest-%s\", random.UniqueId()))\n\tdefer RemoveRepo(t, options, uniqueName)\n\tAddRepo(t, options, uniqueName, remoteChartSource)\n\thelmChart := fmt.Sprintf(\"%s/%s\", uniqueName, remoteChartName)\n\n\t// Generate a unique release name so we can defer the delete before installing\n\treleaseName := fmt.Sprintf(\n\t\t\"%s-%s\",\n\t\tremoteChartName, strings.ToLower(random.UniqueId()),\n\t)\n\tdefer Delete(t, options, releaseName, true)\n\n\t// Test if helm.install will return an error if the chart version is incorrect\n\toptions.Version = \"notValidVersion.0.0.0\"\n\trequire.Error(t, InstallE(t, options, helmChart, releaseName))\n\n\t// Fix chart version and retry install\n\toptions.Version = remoteChartVersion\n\t// Test that passing extra arguments doesn't error, by changing default timeout\n\toptions.ExtraArgs = map[string][]string{\"install\": []string{\"--timeout\", \"5m1s\"}}\n\toptions.ExtraArgs[\"delete\"] = []string{\"--timeout\", \"5m1s\"}\n\trequire.NoError(t, InstallE(t, options, helmChart, releaseName))\n\twaitForRemoteChartPods(t, kubectlOptions, releaseName, 1)\n\n\t// Verify service is accessible. Wait for it to become available and then hit the endpoint.\n\tserviceName := releaseName\n\tk8s.WaitUntilServiceAvailable(t, kubectlOptions, serviceName, 10, 1*time.Second)\n\tservice := k8s.GetService(t, kubectlOptions, serviceName)\n\tendpoint := k8s.GetServiceEndpoint(t, kubectlOptions, service, 80)\n\n\t// Setup a TLS configuration to submit with the helper, a blank struct is acceptable\n\ttlsConfig := tls.Config{}\n\n\thttp_helper.HttpGetWithRetryWithCustomValidation(\n\t\tt,\n\t\tfmt.Sprintf(\"http://%s\", endpoint),\n\t\t&tlsConfig,\n\t\t30,\n\t\t10*time.Second,\n\t\tfunc(statusCode int, body string) bool {\n\t\t\treturn statusCode == 200\n\t\t},\n\t)\n}\n\n// Test deployment of helm chart with dependencies.\nfunc TestHelmDependencyInstall(t *testing.T) {\n\tt.Parallel()\n\n\t// Path to the helm chart with dependencies which we will test\n\thelmChartPath, err := filepath.Abs(\"../../examples/helm-dependency-example\")\n\trequire.NoError(t, err)\n\n\t// Custom namespace name.\n\tnamespaceName := fmt.Sprintf(\"helm-dependency-example-%s\", strings.ToLower(random.UniqueId()))\n\n\t// Setup the kubectl config and context. Here we choose to use the defaults, which is:\n\t// - HOME/.kube/config for the kubectl config file\n\t// - Current context of the kubectl config file\n\tkubectlOptions := k8s.NewKubectlOptions(\"\", \"\", namespaceName)\n\n\tk8s.CreateNamespace(t, kubectlOptions, namespaceName)\n\tdefer k8s.DeleteNamespace(t, kubectlOptions, namespaceName)\n\n\t// Helm chart deployment options.\n\toptions := &Options{\n\t\tKubectlOptions: kubectlOptions,\n\t\tSetValues: map[string]string{\n\t\t\t\"containerImageRepo\":       \"nginx\",\n\t\t\t\"containerImageTag\":        \"1.15.8\",\n\t\t\t\"basic.containerImageRepo\": \"nginx\",\n\t\t\t\"basic.containerImageTag\":  \"1.15.8\",\n\t\t},\n\t\tBuildDependencies: true,\n\t}\n\t// We generate a unique release name so that we can refer to after deployment.\n\t// By doing so, we can schedule the delete call here so that at the end of the test, we run\n\t// `helm delete RELEASE_NAME` to clean up any resources that were created.\n\treleaseName := fmt.Sprintf(\n\t\t\"helm-dependency-example-%s\",\n\t\tstrings.ToLower(random.UniqueId()),\n\t)\n\tdefer Delete(t, options, releaseName, true)\n\n\t// Deploy the chart using `helm install`.\n\terr = InstallE(t, options, helmChartPath, releaseName)\n\tassert.NoError(t, err)\n\n\t// Verify that Kubernetes service is available after helm chart deployment.\n\t_, err = k8s.GetServiceE(t, kubectlOptions, releaseName)\n\tassert.NoError(t, err)\n}\n\nfunc waitForRemoteChartPods(t *testing.T, kubectlOptions *k8s.KubectlOptions, releaseName string, podCount int) {\n\t// Get pod and wait for it to be avaialable\n\t// To get the pod, we need to filter it using the labels that the helm chart creates\n\tfilters := metav1.ListOptions{\n\t\tLabelSelector: fmt.Sprintf(\n\t\t\t\"app.kubernetes.io/name=%s,app.kubernetes.io/instance=%s\",\n\t\t\tremoteChartName, releaseName,\n\t\t),\n\t}\n\t// Use longer timeout (60 retries * 10s = 10 min) to handle slower CI environments\n\t// and potential resource contention when multiple helm tests run in parallel\n\tk8s.WaitUntilNumPodsCreated(t, kubectlOptions, filters, podCount, 60, 10*time.Second)\n\tpods := k8s.ListPods(t, kubectlOptions, filters)\n\tfor _, pod := range pods {\n\t\tk8s.WaitUntilPodAvailable(t, kubectlOptions, pod.Name, 60, 10*time.Second)\n\t}\n}\n"
  },
  {
    "path": "modules/helm/options.go",
    "content": "package helm\n\nimport (\n\t\"github.com/gruntwork-io/terratest/modules/k8s\"\n\t\"github.com/gruntwork-io/terratest/modules/logger\"\n)\n\ntype Options struct {\n\tValuesFiles       []string            // List of values files to render.\n\tSetValues         map[string]string   // Values that should be set via the command line.\n\tSetStrValues      map[string]string   // Values that should be set via the command line explicitly as `string` types.\n\tSetJsonValues     map[string]string   // Values that should be set via the command line in JSON format.\n\tSetFiles          map[string]string   // Values that should be set from a file. These should be file paths. Use to avoid logging secrets.\n\tKubectlOptions    *k8s.KubectlOptions // KubectlOptions to control how to authenticate to kubernetes cluster. `nil` => use defaults.\n\tHomePath          string              // The path to the helm home to use when calling out to helm. Empty string means use default ($HOME/.helm).\n\tEnvVars           map[string]string   // Environment variables to set when running helm\n\tVersion           string              // Version of chart\n\tLogger            *logger.Logger      // Set a non-default logger that should be used. See the logger package for more info. Use logger.Discard to not print the output while executing the command.\n\tExtraArgs         map[string][]string // Extra arguments to pass to the helm install/upgrade/rollback/delete and helm repo add commands. The key signals the command (e.g., install) while the values are the extra arguments to pass through.\n\tBuildDependencies bool                // If true, helm dependencies will be built before rendering template, installing or upgrade the chart.\n\tSnapshotPath      string              // The path to the snapshot directory when using snapshot based testing. Empty string means use default ($PWD/__snapshot__).\n}\n"
  },
  {
    "path": "modules/helm/repo.go",
    "content": "package helm\n\nimport (\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n)\n\n// AddRepo will setup the provided helm repository to the local helm client configuration. This will fail the test if\n// there is an error.\nfunc AddRepo(t testing.TestingT, options *Options, repoName string, repoURL string) {\n\trequire.NoError(t, AddRepoE(t, options, repoName, repoURL))\n}\n\n// AddRepoE will setup the provided helm repository to the local helm client configuration.\nfunc AddRepoE(t testing.TestingT, options *Options, repoName string, repoURL string) error {\n\t// Set required args\n\targs := []string{\"add\", repoName, repoURL}\n\n\t// Append helm repo add ExtraArgs if available\n\tif options.ExtraArgs != nil {\n\t\tif repoAddArgs, ok := options.ExtraArgs[\"repoAdd\"]; ok {\n\t\t\targs = append(args, repoAddArgs...)\n\t\t}\n\t}\n\t_, err := RunHelmCommandAndGetOutputE(t, options, \"repo\", args...)\n\treturn err\n}\n\n// RemoveRepo will remove the provided helm repository from the local helm client configuration. This will fail the test\n// if there is an error.\nfunc RemoveRepo(t testing.TestingT, options *Options, repoName string) {\n\trequire.NoError(t, RemoveRepoE(t, options, repoName))\n}\n\n// RemoveRepoE will remove the provided helm repository from the local helm client configuration.\nfunc RemoveRepoE(t testing.TestingT, options *Options, repoName string) error {\n\t_, err := RunHelmCommandAndGetOutputE(t, options, \"repo\", \"remove\", repoName)\n\treturn err\n}\n"
  },
  {
    "path": "modules/helm/rollback.go",
    "content": "package helm\n\nimport (\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// Rollback will downgrade the release to the specified version. This will fail\n// the test if there is an error.\nfunc Rollback(t testing.TestingT, options *Options, releaseName string, revision string) {\n\trequire.NoError(t, RollbackE(t, options, releaseName, revision))\n}\n\n// RollbackE will downgrade the release to the specified version\nfunc RollbackE(t testing.TestingT, options *Options, releaseName string, revision string) error {\n\tvar err error\n\targs := []string{}\n\tif options.ExtraArgs != nil {\n\t\tif rollbackArgs, ok := options.ExtraArgs[\"rollback\"]; ok {\n\t\t\targs = append(args, rollbackArgs...)\n\t\t}\n\t}\n\targs = append(args, releaseName)\n\tif revision != \"\" {\n\t\targs = append(args, revision)\n\t}\n\t_, err = RunHelmCommandAndGetOutputE(t, options, \"rollback\", args...)\n\treturn err\n}\n"
  },
  {
    "path": "modules/helm/template.go",
    "content": "package helm\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"reflect\"\n\t\"strings\"\n\n\t\"github.com/gonvenience/ytbx\"\n\t\"github.com/gruntwork-io/go-commons/errors\"\n\t\"github.com/gruntwork-io/terratest/modules/files\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/homeport/dyff/pkg/dyff\"\n\t\"github.com/stretchr/testify/require\"\n\tgoyaml \"gopkg.in/yaml.v3\"\n)\n\n// RenderTemplate runs `helm template` to render the template given the provided options and returns stdout/stderr from\n// the template command. If you pass in templateFiles, this will only render those templates. This function will fail\n// the test if there is an error rendering the template.\nfunc RenderTemplate(t testing.TestingT, options *Options, chartDir string, releaseName string, templateFiles []string, extraHelmArgs ...string) string {\n\tout, err := RenderTemplateE(t, options, chartDir, releaseName, templateFiles, extraHelmArgs...)\n\trequire.NoError(t, err)\n\treturn out\n}\n\n// RenderTemplateE runs `helm template` to render the template given the provided options and returns stdout/stderr from\n// the template command. If you pass in templateFiles, this will only render those templates.\nfunc RenderTemplateE(t testing.TestingT, options *Options, chartDir string, releaseName string, templateFiles []string, extraHelmArgs ...string) (string, error) {\n\t// Get render arguments\n\targs, err := getRenderArgs(t, options, chartDir, releaseName, templateFiles, extraHelmArgs...)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// Finally, call out to helm template command\n\treturn RunHelmCommandAndGetStdOutE(t, options, \"template\", args...)\n}\n\n// RenderTemplateAndGetStdOutErrE runs `helm template` to render the template given the provided options and returns stdout and stderr separately from\n// the template command. If you pass in templateFiles, this will only render those templates.\nfunc RenderTemplateAndGetStdOutErrE(t testing.TestingT, options *Options, chartDir string, releaseName string, templateFiles []string, extraHelmArgs ...string) (string, string, error) {\n\targs, err := getRenderArgs(t, options, chartDir, releaseName, templateFiles, extraHelmArgs...)\n\tif err != nil {\n\t\treturn \"\", \"\", err\n\t}\n\n\t// Finally, call out to helm template command\n\treturn RunHelmCommandAndGetStdOutErrE(t, options, \"template\", args...)\n}\n\nfunc getRenderArgs(t testing.TestingT, options *Options, chartDir string, releaseName string, templateFiles []string, extraHelmArgs ...string) ([]string, error) {\n\t// First, verify the charts dir exists\n\tabsChartDir, err := filepath.Abs(chartDir)\n\tif err != nil {\n\t\treturn nil, errors.WithStackTrace(err)\n\t}\n\tif !files.FileExists(chartDir) {\n\t\treturn nil, errors.WithStackTrace(ChartNotFoundError{chartDir})\n\t}\n\n\t// check chart dependencies\n\tif options.BuildDependencies {\n\t\tif _, err := RunHelmCommandAndGetOutputE(t, options, \"dependency\", \"build\", chartDir); err != nil {\n\t\t\treturn nil, errors.WithStackTrace(err)\n\t\t}\n\t}\n\n\t// Now construct the args\n\t// We first construct the template args\n\targs := []string{}\n\tif options.KubectlOptions != nil && options.KubectlOptions.Namespace != \"\" {\n\t\targs = append(args, \"--namespace\", options.KubectlOptions.Namespace)\n\t}\n\targs, err = getValuesArgsE(t, options, args...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tfor _, templateFile := range templateFiles {\n\t\t// validate this is a valid template file\n\t\tabsTemplateFile := filepath.Join(absChartDir, templateFile)\n\t\tif !strings.HasPrefix(templateFile, \"charts\") && !files.FileExists(absTemplateFile) {\n\t\t\treturn nil, errors.WithStackTrace(TemplateFileNotFoundError{Path: templateFile, ChartDir: absChartDir})\n\t\t}\n\n\t\t// Note: we only get the abs template file path to check it actually exists, but the `helm template` command\n\t\t// expects the relative path from the chart.\n\t\targs = append(args, \"--show-only\", templateFile)\n\t}\n\t// deal extraHelmArgs\n\targs = append(args, extraHelmArgs...)\n\n\t// ... and add the name and chart at the end as the command expects\n\targs = append(args, releaseName, chartDir)\n\treturn args, nil\n}\n\n// RenderRemoteTemplate runs `helm template` to render a *remote* chart  given the provided options and returns stdout/stderr from\n// the template command. If you pass in templateFiles, this will only render those templates. This function will fail\n// the test if there is an error rendering the template.\nfunc RenderRemoteTemplate(t testing.TestingT, options *Options, chartURL string, releaseName string, templateFiles []string, extraHelmArgs ...string) string {\n\tout, err := RenderRemoteTemplateE(t, options, chartURL, releaseName, templateFiles, extraHelmArgs...)\n\trequire.NoError(t, err)\n\treturn out\n}\n\n// RenderRemoteTemplateE runs `helm template` to render a *remote* helm chart  given the provided options and returns stdout/stderr from\n// the template command. If you pass in templateFiles, this will only render those templates.\nfunc RenderRemoteTemplateE(t testing.TestingT, options *Options, chartURL string, releaseName string, templateFiles []string, extraHelmArgs ...string) (string, error) {\n\t// Now construct the args\n\t// We first construct the template args\n\targs := []string{}\n\tif options.KubectlOptions != nil && options.KubectlOptions.Namespace != \"\" {\n\t\targs = append(args, \"--namespace\", options.KubectlOptions.Namespace)\n\t}\n\targs, err := getValuesArgsE(t, options, args...)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tfor _, templateFile := range templateFiles {\n\t\t// As the helm command fails if a non valid template is given as input\n\t\t// we do not check if the template file exists or not as we do for local charts\n\t\t// as it would add unecessary networking calls\n\t\targs = append(args, \"--show-only\", templateFile)\n\t}\n\t// deal extraHelmArgs\n\targs = append(args, extraHelmArgs...)\n\n\t// ... and add the helm chart name, the remote repo and chart URL at the end\n\targs = append(args, releaseName, \"--repo\", chartURL)\n\tif options.Version != \"\" {\n\t\targs = append(args, \"--version\", options.Version)\n\t}\n\n\t// Finally, call out to helm template command\n\treturn RunHelmCommandAndGetStdOutE(t, options, \"template\", args...)\n}\n\n// UnmarshalK8SYamls is the same as UnmarshalK8SYamlsE, but will fail the test if there is an error.\nfunc UnmarshalK8SYamls[T any](t testing.TestingT, yamlData string, destinationObj *[]T, check func(v T) bool) {\n\trequire.NoError(t, UnmarshalK8SYamlsE(t, yamlData, destinationObj, check))\n}\n\n// UnmarshalK8SYamlsE try to unmarshal yaml that contains multiple k8s objects into slice of concrete type.\n// It requires user to pass `check` function to determine whether the unmarshaled object is valid or not.\n// It will ignore error or invalid object but if no valid object were found, it will return error.\nfunc UnmarshalK8SYamlsE[T any](t testing.TestingT, yamlData string, destinationObj *[]T, check func(v T) bool) error {\n\toriginalLen := len(*destinationObj)\n\n\traws := []json.RawMessage{}\n\tif err := UnmarshalK8SYamlE(t, yamlData, &raws); err != nil {\n\t\treturn err\n\t}\n\n\tfor _, raw := range raws {\n\t\tvar v T\n\t\terr := json.Unmarshal(raw, &v)\n\t\tif err != nil || !check(v) {\n\t\t\tcontinue\n\t\t}\n\t\t*destinationObj = append(*destinationObj, v)\n\t}\n\n\tif len(*destinationObj) == originalLen {\n\t\treturn fmt.Errorf(\"no matching raw data were found for the concrete type\")\n\t}\n\treturn nil\n}\n\n// UnmarshalK8SYaml is the same as UnmarshalK8SYamlE, but will fail the test if there is an error.\nfunc UnmarshalK8SYaml(t testing.TestingT, yamlData string, destinationObj interface{}) {\n\trequire.NoError(t, UnmarshalK8SYamlE(t, yamlData, destinationObj))\n}\n\n// UnmarshalK8SYamlE can be used to take template outputs and unmarshal them into the corresponding client-go struct. For\n// example, suppose you render the template into a Deployment object. You can unmarshal the yaml as follows:\n//\n// var deployment appsv1.Deployment\n// UnmarshalK8SYamlE(t, renderedOutput, &deployment)\n//\n// At the end of this, the deployment variable will be populated.\nfunc UnmarshalK8SYamlE(t testing.TestingT, yamlData string, destinationObj interface{}) error {\n\tdecoder := goyaml.NewDecoder(strings.NewReader(yamlData))\n\n\t// Ensure destinationObj is a pointer\n\tdestVal := reflect.ValueOf(destinationObj)\n\tif destVal.Kind() != reflect.Ptr {\n\t\treturn fmt.Errorf(\"destinationObj must be a pointer\")\n\t}\n\tdestElem := destVal.Elem()\n\n\t// Handle single object or list as root\n\tif destElem.Kind() != reflect.Slice {\n\t\t// Decode only the first document\n\t\tvar rawYaml interface{}\n\t\tif err := decoder.Decode(&rawYaml); err != nil {\n\t\t\treturn errors.WithStackTrace(err)\n\t\t}\n\t\t// If the root is an array but destinationObj is a single object, return an error\n\t\tif reflect.TypeOf(rawYaml).Kind() == reflect.Slice {\n\t\t\treturn fmt.Errorf(\"YAML root is an array, but destinationObj is a single object\")\n\t\t}\n\n\t\tjsonData, err := json.Marshal(rawYaml)\n\t\tif err != nil {\n\t\t\treturn errors.WithStackTrace(err)\n\t\t}\n\n\t\tif err := json.Unmarshal(jsonData, destinationObj); err != nil {\n\t\t\treturn errors.WithStackTrace(err)\n\t\t}\n\t\treturn nil\n\t}\n\n\t// Handle multiple YAML documents (destinationObj is a slice)\n\tslicePtr := destVal\n\tsliceVal := slicePtr.Elem()\n\n\tfor {\n\t\tvar rawYaml interface{}\n\t\tif err := decoder.Decode(&rawYaml); err != nil {\n\t\t\tif err == io.EOF {\n\t\t\t\tbreak // No more documents\n\t\t\t}\n\t\t\treturn errors.WithStackTrace(err)\n\t\t}\n\n\t\tjsonData, err := json.Marshal(rawYaml)\n\t\tif err != nil {\n\t\t\treturn errors.WithStackTrace(err)\n\t\t}\n\n\t\t// If root object is a slice, append elements individually\n\t\tif reflect.TypeOf(rawYaml).Kind() == reflect.Slice {\n\t\t\tvar items []json.RawMessage\n\t\t\tif err := json.Unmarshal(jsonData, &items); err != nil {\n\t\t\t\treturn errors.WithStackTrace(err)\n\t\t\t}\n\n\t\t\tfor _, item := range items {\n\t\t\t\tnewElem := reflect.New(sliceVal.Type().Elem()) // Create new element\n\t\t\t\tif err := json.Unmarshal(item, newElem.Interface()); err != nil {\n\t\t\t\t\treturn errors.WithStackTrace(err)\n\t\t\t\t}\n\t\t\t\tsliceVal.Set(reflect.Append(sliceVal, newElem.Elem()))\n\t\t\t}\n\n\t\t} else {\n\t\t\tnewElem := reflect.New(sliceVal.Type().Elem()) // Create new element\n\t\t\tif err := json.Unmarshal(jsonData, newElem.Interface()); err != nil {\n\t\t\t\treturn errors.WithStackTrace(err)\n\t\t\t}\n\t\t\tsliceVal.Set(reflect.Append(sliceVal, newElem.Elem()))\n\t\t}\n\t}\n\treturn nil\n}\n\n// UpdateSnapshot creates or updates the k8s manifest snapshot of a chart (e.g bitnami/nginx).\n// It is one of the two functions needed to implement snapshot based testing for helm.\n// see https://github.com/gruntwork-io/terratest/issues/1377\n// A snapshot is used to compare the current manifests of a chart with the previous manifests.\n// A global diff is run against the two snapshosts and the number of differences is returned.\nfunc UpdateSnapshot(t testing.TestingT, options *Options, yamlData string, releaseName string) {\n\trequire.NoError(t, UpdateSnapshotE(t, options, yamlData, releaseName))\n}\n\n// UpdateSnapshotE creates or updates the k8s manifest snapshot of a chart (e.g bitnami/nginx).\n// It is one of the two functions needed to implement snapshot based testing for helm.\n// see https://github.com/gruntwork-io/terratest/issues/1377\n// A snapshot is used to compare the current manifests of a chart with the previous manifests.\n// A global diff is run against the two snapshosts and the number of differences is returned.\n// It will failed the test if there is an error while writing the manifests' snapshot in the file system\nfunc UpdateSnapshotE(t testing.TestingT, options *Options, yamlData string, releaseName string) error {\n\n\tvar snapshotDir = \"__snapshot__\"\n\tif options.SnapshotPath != \"\" {\n\t\tsnapshotDir = options.SnapshotPath\n\t}\n\t// Create a directory if not exists\n\tif !files.FileExists(snapshotDir) {\n\t\tif err := os.Mkdir(snapshotDir, 0755); err != nil {\n\t\t\treturn errors.WithStackTrace(err)\n\t\t}\n\t}\n\n\tfilename := filepath.Join(snapshotDir, releaseName+\".yaml\")\n\t// Open a file in write mode\n\tfile, err := os.Create(filename)\n\tif err != nil {\n\t\treturn errors.WithStackTrace(err)\n\t}\n\tdefer file.Close()\n\n\t// Write the k8s manifest into the file\n\tif _, err = file.WriteString(yamlData); err != nil {\n\t\treturn errors.WithStackTrace(err)\n\t}\n\n\tif options.Logger != nil {\n\t\toptions.Logger.Logf(t, \"helm chart manifest written into file: %s\", filename)\n\t}\n\treturn nil\n}\n\n// DiffAgainstSnapshot compare the current manifests of a chart (e.g bitnami/nginx)\n// with the previous manifests stored in the snapshot.\n// see https://github.com/gruntwork-io/terratest/issues/1377\n// It returns the number of difference between the two manifests or -1 in case of error\n// It will fail the test if there is an error while reading or writing the two manifests in the file system\nfunc DiffAgainstSnapshot(t testing.TestingT, options *Options, yamlData string, releaseName string) int {\n\tnumberOfDiffs, err := DiffAgainstSnapshotE(t, options, yamlData, releaseName)\n\trequire.NoError(t, err)\n\treturn numberOfDiffs\n}\n\n// DiffAgainstSnapshotE compare the current manifests of a chart (e.g bitnami/nginx)\n// with the previous manifests stored in the snapshot.\n// see https://github.com/gruntwork-io/terratest/issues/1377\n// It returns the number of difference between the manifests or -1 in case of error\nfunc DiffAgainstSnapshotE(t testing.TestingT, options *Options, yamlData string, releaseName string) (int, error) {\n\n\tvar snapshotDir = \"__snapshot__\"\n\tif options.SnapshotPath != \"\" {\n\t\tsnapshotDir = options.SnapshotPath\n\t}\n\n\t// load the yaml snapshot file\n\tsnapshot := filepath.Join(snapshotDir, releaseName+\".yaml\")\n\tfrom, err := ytbx.LoadFile(snapshot)\n\tif err != nil {\n\t\treturn -1, errors.WithStackTrace(err)\n\t}\n\n\t// write the current manifest into a file as `dyff` does not support string input\n\tcurrentManifests := releaseName + \".yaml\"\n\tfile, err := os.Create(currentManifests)\n\tif err != nil {\n\t\treturn -1, errors.WithStackTrace(err)\n\t}\n\n\tif _, err = file.WriteString(yamlData); err != nil {\n\t\treturn -1, errors.WithStackTrace(err)\n\t}\n\tdefer file.Close()\n\tdefer os.Remove(currentManifests)\n\n\tto, err := ytbx.LoadFile(currentManifests)\n\tif err != nil {\n\t\treturn -1, errors.WithStackTrace(err)\n\t}\n\n\t// compare the two manifests using `dyff`\n\tcompOpt := dyff.KubernetesEntityDetection(false)\n\n\t// create a report\n\treport, err := dyff.CompareInputFiles(from, to, compOpt)\n\tif err != nil {\n\t\treturn -1, errors.WithStackTrace(err)\n\t}\n\n\t// write any difference to stdout\n\treportWriter := &dyff.HumanReport{\n\t\tReport:            report,\n\t\tDoNotInspectCerts: false,\n\t\tNoTableStyle:      false,\n\t\tOmitHeader:        false,\n\t\tUseGoPatchPaths:   false,\n\t}\n\n\terr = reportWriter.WriteReport(os.Stdout)\n\tif err != nil {\n\t\treturn -1, errors.WithStackTrace(err)\n\t}\n\t// return the number of diffs to use in assertion while testing: 0 = no differences\n\treturn len(reportWriter.Diffs), nil\n}\n"
  },
  {
    "path": "modules/helm/template_test.go",
    "content": "//go:build kubeall || helm\n// +build kubeall helm\n\n// NOTE: we have build tags to differentiate kubernetes tests from non-kubernetes tests, and further differentiate helm\n// tests. This is done because minikube is heavy and can interfere with docker related tests in terratest. Similarly,\n// helm can overload the minikube system and thus interfere with the other kubernetes tests. To avoid overloading the\n// system, we run the kubernetes tests and helm tests separately from the others.\n\npackage helm\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\tappsv1 \"k8s.io/api/apps/v1\"\n\tcorev1 \"k8s.io/api/core/v1\"\n\n\t\"github.com/gruntwork-io/terratest/modules/k8s\"\n\t\"github.com/gruntwork-io/terratest/modules/logger\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n)\n\n// Test that we can render locally a remote chart (e.g bitnami/nginx)\nfunc TestRemoteChartRender(t *testing.T) {\n\tconst (\n\t\tremoteChartSource  = \"https://charts.bitnami.com/bitnami\"\n\t\tremoteChartName    = \"nginx\"\n\t\tremoteChartVersion = \"13.2.24\"\n\t\tregistry           = \"registry-1.docker.io\"\n\t)\n\n\tt.Parallel()\n\n\tnamespaceName := fmt.Sprintf(\n\t\t\"%s-%s\",\n\t\tstrings.ToLower(t.Name()),\n\t\tstrings.ToLower(random.UniqueId()),\n\t)\n\n\treleaseName := remoteChartName\n\n\toptions := &Options{\n\t\tSetValues: map[string]string{\n\t\t\t\"image.repository\": remoteChartName,\n\t\t\t\"image.registry\":   registry,\n\t\t\t\"image.tag\":        remoteChartVersion,\n\t\t},\n\t\tKubectlOptions: k8s.NewKubectlOptions(\"\", \"\", namespaceName),\n\t\tLogger:         logger.Discard,\n\t\tVersion:        remoteChartVersion,\n\t}\n\n\t// Run RenderTemplate to render the template and capture the output. Note that we use the version without `E`, since\n\t// we want to assert that the template renders without any errors.\n\toutput := RenderRemoteTemplate(t, options, remoteChartSource, releaseName, []string{\"templates/deployment.yaml\"})\n\n\t// Now we use kubernetes/client-go library to render the template output into the Deployment struct. This will\n\t// ensure the Deployment resource is rendered correctly.\n\tvar deployment appsv1.Deployment\n\tUnmarshalK8SYaml(t, output, &deployment)\n\n\t// Verify the namespace matches the expected supplied namespace.\n\trequire.Equal(t, namespaceName, deployment.Namespace)\n\n\t// Finally, we verify the deployment pod template spec is set to the expected container image value\n\texpectedContainerImage := registry + \"/\" + remoteChartName + \":\" + remoteChartVersion\n\tdeploymentContainers := deployment.Spec.Template.Spec.Containers\n\trequire.Equal(t, len(deploymentContainers), 1)\n\trequire.Equal(t, expectedContainerImage, deploymentContainers[0].Image)\n}\n\n// Test that we can dump all the manifest locally a remote chart (e.g bitnami/nginx)\n// so that I can use them later to compare between two versions of the same chart for example\nfunc TestRemoteChartRenderDump(t *testing.T) {\n\tt.Parallel()\n\trenderChartDump(t, \"13.2.20\", t.TempDir())\n}\n\n// Test that we can diff all the manifest to a local snapshot using a remote chart (e.g bitnami/nginx)\nfunc TestRemoteChartRenderDiff(t *testing.T) {\n\tt.Parallel()\n\n\tinitialSnapshot := t.TempDir()\n\tupdatedSnapshot := t.TempDir()\n\trenderChartDump(t, \"13.2.20\", initialSnapshot)\n\toutput := renderChartDump(t, \"13.2.24\", updatedSnapshot)\n\n\toptions := &Options{\n\t\tLogger:       logger.Default,\n\t\tSnapshotPath: initialSnapshot,\n\t}\n\t// diff in: spec.initContainers.preserve-logs-symlinks.imag, spec.containers.nginx.image, tls certificates\n\trequire.Equal(t, 4, DiffAgainstSnapshot(t, options, output, \"nginx\"))\n}\n\n// render chart dump and return the rendered output\nfunc renderChartDump(t *testing.T, remoteChartVersion, snapshotDir string) string {\n\tconst (\n\t\tremoteChartSource = \"https://charts.bitnami.com/bitnami\"\n\t\tremoteChartName   = \"nginx\"\n\t\t// need to set a fix name for the namespace, so it is not flag as a difference\n\t\tnamespaceName = \"dump-ns\"\n\t)\n\n\treleaseName := remoteChartName\n\n\toptions := &Options{\n\t\tSetValues: map[string]string{\n\t\t\t\"image.repository\": remoteChartName,\n\t\t\t\"image.registry\":   \"\",\n\t\t\t\"image.tag\":        remoteChartVersion,\n\t\t},\n\t\tKubectlOptions: k8s.NewKubectlOptions(\"\", \"\", namespaceName),\n\t\tLogger:         logger.Discard,\n\t\tVersion:        remoteChartVersion,\n\t}\n\n\t// Run RenderTemplate to render the template and capture the output. Note that we use the version without `E`, since\n\t// we want to assert that the template renders without any errors.\n\toutput := RenderRemoteTemplate(t, options, remoteChartSource, releaseName, []string{})\n\n\t// Now we use kubernetes/client-go library to render the template output into the Deployment struct. This will\n\t// ensure the Deployment resource is rendered correctly.\n\tvar deployment appsv1.Deployment\n\tUnmarshalK8SYaml(t, output, &deployment)\n\n\t// Verify the namespace matches the expected supplied namespace.\n\trequire.Equal(t, namespaceName, deployment.Namespace)\n\n\t// write chart manifest to a local filesystem directory\n\toptions = &Options{\n\t\tLogger:       logger.Default,\n\t\tSnapshotPath: snapshotDir,\n\t}\n\tUpdateSnapshot(t, options, output, releaseName)\n\treturn output\n}\n\nfunc TestUnmarshall(t *testing.T) {\n\tt.Run(\"Single\", func(t *testing.T) {\n\t\tb, err := os.ReadFile(\"testdata/deployment.yaml\")\n\t\trequire.NoError(t, err)\n\t\tvar deployment appsv1.Deployment\n\t\tUnmarshalK8SYaml(t, string(b), &deployment)\n\t\tassert.Equal(t, deployment.Name, \"nginx-deployment\")\n\t})\n\tt.Run(\"Multiple\", func(t *testing.T) {\n\t\tfor _, f := range []string{\"testdata/deployments.yaml\", \"testdata/deployments-array.yaml\"} {\n\t\t\tb, err := os.ReadFile(f)\n\t\t\trequire.NoError(t, err)\n\t\t\tvar deployment []appsv1.Deployment\n\t\t\tUnmarshalK8SYaml(t, string(b), &deployment)\n\t\t\trequire.Len(t, deployment, 2)\n\t\t\tassert.Equal(t, deployment[0].Name, \"nginx-deployment-1\")\n\t\t\tassert.Equal(t, deployment[1].Name, \"nginx-deployment-2\")\n\n\t\t\t// overwrite for equality check\n\t\t\tdeployment[1].Name = deployment[0].Name\n\t\t\tassert.Equal(t, deployment[0], deployment[1])\n\t\t}\n\t})\n\tt.Run(\"Invalid\", func(t *testing.T) {\n\t\tb, err := os.ReadFile(\"testdata/invalid-duplicate.yaml\")\n\t\trequire.NoError(t, err)\n\t\tvar deployment appsv1.Deployment\n\t\terr = UnmarshalK8SYamlE(t, string(b), &deployment)\n\t\tassert.Error(t, err)\n\t\tassert.Regexp(t, regexp.MustCompile(`mapping key \".+\" already defined at line \\d+`), err.Error())\n\t})\n\tt.Run(\"LiteralBlock\", func(t *testing.T) {\n\t\tb, err := os.ReadFile(\"testdata/configmap-literalblock.yaml\")\n\t\trequire.NoError(t, err)\n\t\tvar configmap corev1.ConfigMap\n\t\terr = UnmarshalK8SYamlE(t, string(b), &configmap)\n\t\tassert.NoError(t, err)\n\t\tdata := `configmap-data-value-1;      \nconfigmap-data-value-2;\n`\n\t\tassert.Equal(t, data, configmap.Data[\"thisIsSomeDataKey\"])\n\t})\n}\n\nfunc TestRenderWarning(t *testing.T) {\n\tchart, err := filepath.Abs(\"testdata/deprecated-chart\")\n\trequire.NoError(t, err)\n\n\tstdout, stderr, err := RenderTemplateAndGetStdOutErrE(t, &Options{}, chart, \"test\", nil)\n\trequire.NoError(t, err)\n\n\tassert.Contains(t, stderr, \"WARNING:\")\n\n\tvar deployment appsv1.Deployment\n\tUnmarshalK8SYaml(t, string(stdout), &deployment)\n\tassert.Equal(t, deployment.Name, \"nginx-deployment\")\n}\n\nfunc TestRenderMultipleManifests(t *testing.T) {\n\tchart, err := filepath.Abs(\"testdata/multiple-manifests\")\n\trequire.NoError(t, err)\n\n\tout := RenderTemplate(t, &Options{}, chart, \"test\", []string{})\n\n\tvar configs []corev1.ConfigMap\n\tUnmarshalK8SYamlsE(t, out, &configs, func(v corev1.ConfigMap) bool {\n\t\treturn v.Kind == \"ConfigMap\"\n\t})\n\trequire.Len(t, configs, 1)\n\tassert.Equal(t, configs[0].Name, \"test-configmap\")\n\n\tvar deploys []appsv1.Deployment\n\tUnmarshalK8SYamlsE(t, out, &deploys, func(v appsv1.Deployment) bool {\n\t\treturn v.Kind == \"Deployment\"\n\t})\n\trequire.Len(t, deploys, 1)\n\tassert.Equal(t, deploys[0].Name, \"test-deployment\")\n}\n"
  },
  {
    "path": "modules/helm/testdata/configmap-literalblock.yaml",
    "content": "apiVersion: v1\nkind: ConfigMap\nmetadata:\n  labels:\n    app: release-name\n  name: release-name\ndata:\n  thisIsSomeDataKey: |\n    configmap-data-value-1;      \n    configmap-data-value-2;\n"
  },
  {
    "path": "modules/helm/testdata/deployment.yaml",
    "content": "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: nginx-deployment\n  labels:\n    app: nginx\nspec:\n  replicas: 3\n  selector:\n    matchLabels:\n      app: nginx\n  template:\n    metadata:\n      labels:\n        app: nginx\n    spec:\n      containers:\n        - name: nginx\n          image: nginx:latest\n          ports:\n            - containerPort: 80\n"
  },
  {
    "path": "modules/helm/testdata/deployments-array.yaml",
    "content": "- apiVersion: apps/v1\n  kind: Deployment\n  metadata:\n    name: nginx-deployment-1\n    labels:\n      app: nginx\n  spec:\n    replicas: 3\n    selector:\n      matchLabels:\n        app: nginx\n    template:\n      metadata:\n        labels:\n          app: nginx\n      spec:\n        containers:\n          - name: nginx\n            image: nginx:latest\n            ports:\n              - containerPort: 80\n\n- apiVersion: apps/v1\n  kind: Deployment\n  metadata:\n    name: nginx-deployment-2\n    labels:\n      app: nginx\n  spec:\n    replicas: 3\n    selector:\n      matchLabels:\n        app: nginx\n    template:\n      metadata:\n        labels:\n          app: nginx\n      spec:\n        containers:\n          - name: nginx\n            image: nginx:latest\n            ports:\n              - containerPort: 80\n"
  },
  {
    "path": "modules/helm/testdata/deployments.yaml",
    "content": "---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: nginx-deployment-1\n  labels:\n    app: nginx\nspec:\n  replicas: 3\n  selector:\n    matchLabels:\n      app: nginx\n  template:\n    metadata:\n      labels:\n        app: nginx\n    spec:\n      containers:\n        - name: nginx\n          image: nginx:latest\n          ports:\n            - containerPort: 80\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: nginx-deployment-2\n  labels:\n    app: nginx\nspec:\n  replicas: 3\n  selector:\n    matchLabels:\n      app: nginx\n  template:\n    metadata:\n      labels:\n        app: nginx\n    spec:\n      containers:\n        - name: nginx\n          image: nginx:latest\n          ports:\n            - containerPort: 80\n"
  },
  {
    "path": "modules/helm/testdata/deprecated-chart/Chart.yaml",
    "content": "name: test\nversion: 0.1.0\napiVersion: v1\ndeprecated: true\n"
  },
  {
    "path": "modules/helm/testdata/deprecated-chart/templates/deployment.yaml",
    "content": "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: nginx-deployment\n  labels:\n    app: nginx\nspec:\n  replicas: 3\n  selector:\n    matchLabels:\n      app: nginx\n  template:\n    metadata:\n      labels:\n        app: nginx\n    spec:\n      containers:\n        - name: nginx\n          image: nginx:latest\n          ports:\n            - containerPort: 80\n"
  },
  {
    "path": "modules/helm/testdata/invalid-duplicate.yaml",
    "content": "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: nginx-deployment\n  labels:\n    app: nginx\n    app: nginx2\nspec:\n  replicas: 3\n  selector:\n    matchLabels:\n      app: nginx\n  template:\n    metadata:\n      labels:\n        app: nginx\n        app: nginx2\n    spec:\n      containers:\n        - name: nginx\n          image: nginx:latest\n          ports:\n            - containerPort: 80\n"
  },
  {
    "path": "modules/helm/testdata/multiple-manifests/Chart.yaml",
    "content": "name: test\nversion: 0.1.0\napiVersion: v1\n"
  },
  {
    "path": "modules/helm/testdata/multiple-manifests/templates/configmap.yaml",
    "content": "apiVersion: v1\nkind: ConfigMap\nmetadata:\n  labels:\n    app: test\n  name: test-configmap\ndata:\n  hello: world\n  foo: bar\n"
  },
  {
    "path": "modules/helm/testdata/multiple-manifests/templates/deployment.yaml",
    "content": "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: test-deployment\n  labels:\n    app: test\nspec:\n  replicas: 3\n  selector:\n    matchLabels:\n      app: test\n  template:\n    metadata:\n      labels:\n        app: test\n    spec:\n      containers:\n        - name: nginx\n          image: nginx:latest\n          ports:\n            - containerPort: 80\n"
  },
  {
    "path": "modules/helm/upgrade.go",
    "content": "package helm\n\nimport (\n\t\"path/filepath\"\n\n\t\"github.com/gruntwork-io/go-commons/errors\"\n\t\"github.com/gruntwork-io/terratest/modules/files\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// Upgrade will upgrade the release and chart will be deployed with the lastest configuration. This will fail\n// the test if there is an error.\nfunc Upgrade(t testing.TestingT, options *Options, chart string, releaseName string) {\n\trequire.NoError(t, UpgradeE(t, options, chart, releaseName))\n}\n\n// UpgradeE will upgrade the release and chart will be deployed with the lastest configuration.\nfunc UpgradeE(t testing.TestingT, options *Options, chart string, releaseName string) error {\n\t// If the chart refers to a path, convert to absolute path. Otherwise, pass straight through as it may be a remote\n\t// chart.\n\tif files.FileExists(chart) {\n\t\tabsChartDir, err := filepath.Abs(chart)\n\t\tif err != nil {\n\t\t\treturn errors.WithStackTrace(err)\n\t\t}\n\t\tchart = absChartDir\n\t}\n\n\t// build chart dependencies\n\tif options.BuildDependencies {\n\t\tif _, err := RunHelmCommandAndGetOutputE(t, options, \"dependency\", \"build\", chart); err != nil {\n\t\t\treturn errors.WithStackTrace(err)\n\t\t}\n\t}\n\tvar err error\n\targs := []string{}\n\tif options.ExtraArgs != nil {\n\t\tif upgradeArgs, ok := options.ExtraArgs[\"upgrade\"]; ok {\n\t\t\targs = append(args, upgradeArgs...)\n\t\t}\n\t}\n\targs, err = getValuesArgsE(t, options, args...)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\targs = append(args, \"--install\", releaseName, chart)\n\tif options.Version != \"\" {\n\t\targs = append(args, \"--version\", options.Version)\n\t}\n\t_, err = RunHelmCommandAndGetOutputE(t, options, \"upgrade\", args...)\n\treturn err\n}\n"
  },
  {
    "path": "modules/helm/upgrade_test.go",
    "content": "//go:build kubeall || helm\n// +build kubeall helm\n\n// NOTE: we have build tags to differentiate kubernetes tests from non-kubernetes tests, and further differentiate helm\n// tests. This is done because minikube is heavy and can interfere with docker related tests in terratest. Similarly,\n// helm can overload the minikube system and thus interfere with the other kubernetes tests. To avoid overloading the\n// system, we run the kubernetes tests and helm tests separately from the others.\n\npackage helm\n\nimport (\n\t\"crypto/tls\"\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\thttp_helper \"github.com/gruntwork-io/terratest/modules/http-helper\"\n\t\"github.com/gruntwork-io/terratest/modules/k8s\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n)\n\n// Test that we can install, upgrade, and rollback a remote chart (e.g stable/chartmuseum)\nfunc TestRemoteChartInstallUpgradeRollback(t *testing.T) {\n\tt.Parallel()\n\n\tnamespaceName := fmt.Sprintf(\n\t\t\"%s-%s\",\n\t\tstrings.ToLower(t.Name()),\n\t\tstrings.ToLower(random.UniqueId()),\n\t)\n\n\t// Use default kubectl options to create a new namespace for this test, and then update the namespace for kubectl\n\tkubectlOptions := k8s.NewKubectlOptions(\"\", \"\", namespaceName)\n\n\tdefer k8s.DeleteNamespace(t, kubectlOptions, namespaceName)\n\tk8s.CreateNamespace(t, kubectlOptions, namespaceName)\n\n\t// Override service type to node port and disable PDB (requires policy/v1 API\n\t// which may not be available on older k8s clusters)\n\toptions := &Options{\n\t\tKubectlOptions: kubectlOptions,\n\t\tSetValues: map[string]string{\n\t\t\t\"service.type\": \"NodePort\",\n\t\t\t\"pdb.create\":   \"false\",\n\t\t},\n\t\tVersion: remoteChartVersion,\n\t}\n\n\t// Add the stable repo under a random name so as not to touch existing repo configs\n\tuniqueName := strings.ToLower(fmt.Sprintf(\"terratest-%s\", random.UniqueId()))\n\tdefer RemoveRepo(t, options, uniqueName)\n\tAddRepo(t, options, uniqueName, remoteChartSource)\n\thelmChart := fmt.Sprintf(\"%s/%s\", uniqueName, remoteChartName)\n\n\t// Generate a unique release name so we can defer the delete before installing\n\treleaseName := fmt.Sprintf(\n\t\t\"%s-%s\",\n\t\tremoteChartName, strings.ToLower(random.UniqueId()),\n\t)\n\tdefer Delete(t, options, releaseName, true)\n\tInstall(t, options, helmChart, releaseName)\n\twaitForRemoteChartPods(t, kubectlOptions, releaseName, 1)\n\n\t// Setting replica count to 2 to check the upgrade functionality.\n\t// After successful upgrade, the count of pods should be equal to 2.\n\toptions.SetValues = map[string]string{\n\t\t\"replicaCount\": \"2\",\n\t\t\"service.type\": \"NodePort\",\n\t\t\"pdb.create\":   \"false\",\n\t}\n\t// Test that passing extra arguments doesn't error, by changing default timeout\n\toptions.ExtraArgs = map[string][]string{\"upgrade\": []string{\"--timeout\", \"5m1s\"}}\n\toptions.ExtraArgs[\"rollback\"] = []string{\"--timeout\", \"5m1s\"}\n\tUpgrade(t, options, helmChart, releaseName)\n\twaitForRemoteChartPods(t, kubectlOptions, releaseName, 2)\n\n\t// Verify service is accessible. Wait for it to become available and then hit the endpoint.\n\tserviceName := releaseName\n\tk8s.WaitUntilServiceAvailable(t, kubectlOptions, serviceName, 10, 1*time.Second)\n\tservice := k8s.GetService(t, kubectlOptions, serviceName)\n\tendpoint := k8s.GetServiceEndpoint(t, kubectlOptions, service, 80)\n\n\t// Setup a TLS configuration to submit with the helper, a blank struct is acceptable\n\ttlsConfig := tls.Config{}\n\n\thttp_helper.HttpGetWithRetryWithCustomValidation(\n\t\tt,\n\t\tfmt.Sprintf(\"http://%s\", endpoint),\n\t\t&tlsConfig,\n\t\t30,\n\t\t10*time.Second,\n\t\tfunc(statusCode int, body string) bool {\n\t\t\treturn statusCode == 200\n\t\t},\n\t)\n\n\t// Finally, test rollback functionality. When rolling back, we should see the pods go back down to 1.\n\tRollback(t, options, releaseName, \"\")\n\twaitForRemoteChartPods(t, kubectlOptions, releaseName, 1)\n}\n\n// Test deployment of helm chart with dependencies.\nfunc TestHelmDependencyUpgrade(t *testing.T) {\n\tt.Parallel()\n\n\t// Path to the helm chart with dependencies which we will test\n\thelmChartPath, err := filepath.Abs(\"../../examples/helm-dependency-example\")\n\trequire.NoError(t, err)\n\n\t// Custom namespace name.\n\tnamespaceName := fmt.Sprintf(\"helm-dependency-example-%s\", strings.ToLower(random.UniqueId()))\n\n\t// Setup the kubectl config and context. Here we choose to use the defaults, which is:\n\t// - HOME/.kube/config for the kubectl config file\n\t// - Current context of the kubectl config file\n\tkubectlOptions := k8s.NewKubectlOptions(\"\", \"\", namespaceName)\n\n\tk8s.CreateNamespace(t, kubectlOptions, namespaceName)\n\tdefer k8s.DeleteNamespace(t, kubectlOptions, namespaceName)\n\n\t// Helm chart deployment options.\n\toptions := &Options{\n\t\tKubectlOptions: kubectlOptions,\n\t\tSetValues: map[string]string{\n\t\t\t\"containerImageRepo\":       \"nginx\",\n\t\t\t\"containerImageTag\":        \"1.15.8\",\n\t\t\t\"basic.containerImageRepo\": \"nginx\",\n\t\t\t\"basic.containerImageTag\":  \"1.15.8\",\n\t\t},\n\t\tBuildDependencies: true,\n\t}\n\t// We generate a unique release name so that we can refer to after deployment.\n\t// By doing so, we can schedule the delete call here so that at the end of the test, we run\n\t// `helm delete RELEASE_NAME` to clean up any resources that were created.\n\treleaseName := fmt.Sprintf(\n\t\t\"helm-dependency-example-%s\",\n\t\tstrings.ToLower(random.UniqueId()),\n\t)\n\tdefer Delete(t, options, releaseName, true)\n\n\t// Deploy the chart using `helm install`.\n\terr = InstallE(t, options, helmChartPath, releaseName)\n\tassert.NoError(t, err)\n\n\t// Verify that upgrade is working as expected.\n\terr = UpgradeE(t, options, helmChartPath, releaseName)\n\tassert.NoError(t, err)\n}\n"
  },
  {
    "path": "modules/http-helper/continuous.go",
    "content": "package http_helper\n\nimport (\n\t\"crypto/tls\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/gruntwork-io/terratest/modules/logger\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n)\n\ntype GetResponse struct {\n\tStatusCode int\n\tBody       string\n}\n\n// Continuously check the given URL every 1 second until the stopChecking channel receives a signal to stop.\n// This function will return a sync.WaitGroup that can be used to wait for the checking to stop, and a read only channel\n// to stream the responses for each check.\n// Note that the channel has a buffer of 1000, after which it will start to drop the send events\nfunc ContinuouslyCheckUrl(\n\tt testing.TestingT,\n\turl string,\n\tstopChecking <-chan bool,\n\tsleepBetweenChecks time.Duration,\n) (*sync.WaitGroup, <-chan GetResponse) {\n\tvar wg sync.WaitGroup\n\twg.Add(1)\n\tresponses := make(chan GetResponse, 1000)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\tdefer close(responses)\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-stopChecking:\n\t\t\t\tlogger.Default.Logf(t, \"Got signal to stop downtime checks for URL %s.\\n\", url)\n\t\t\t\treturn\n\t\t\tcase <-time.After(sleepBetweenChecks):\n\t\t\t\tstatusCode, body, err := HttpGetE(t, url, &tls.Config{})\n\t\t\t\t// Non-blocking send, defaulting to logging a warning if there is no channel reader\n\t\t\t\tselect {\n\t\t\t\tcase responses <- GetResponse{StatusCode: statusCode, Body: body}:\n\t\t\t\t\t// do nothing since all we want to do is send the response\n\t\t\t\tdefault:\n\t\t\t\t\tlogger.Default.Logf(t, \"WARNING: ContinuouslyCheckUrl responses channel buffer is full\")\n\t\t\t\t}\n\t\t\t\tlogger.Default.Logf(t, \"Got response %d and err %v from URL at %s\", statusCode, err, url)\n\t\t\t\tif err != nil {\n\t\t\t\t\t// We use Errorf instead of Fatalf here because Fatalf is not goroutine safe, while Errorf is. Refer\n\t\t\t\t\t// to the docs on `T`: https://godoc.org/testing#T\n\t\t\t\t\tt.Errorf(\"Failed to make HTTP request to the URL at %s: %s\\n\", url, err.Error())\n\t\t\t\t} else if statusCode != 200 {\n\t\t\t\t\t// We use Errorf instead of Fatalf here because Fatalf is not goroutine safe, while Errorf is. Refer\n\t\t\t\t\t// to the docs on `T`: https://godoc.org/testing#T\n\t\t\t\t\tt.Errorf(\"Got a non-200 response (%d) from the URL at %s, which means there was downtime! Response body: %s\", statusCode, url, body)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}()\n\treturn &wg, responses\n}\n"
  },
  {
    "path": "modules/http-helper/dummy_server.go",
    "content": "package http_helper\n\nimport (\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"sync/atomic\"\n\n\t\"github.com/gruntwork-io/terratest/modules/logger\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n)\n\n// RunDummyServer runs a dummy HTTP server on a unique port that will return the given text. Returns the Listener for the server, the\n// port it's listening on, or an error if something went wrong while trying to start the listener. Make sure to call\n// the Close() method on the Listener when you're done!\nfunc RunDummyServer(t testing.TestingT, text string) (net.Listener, int) {\n\tlistener, port, err := RunDummyServerE(t, text)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn listener, port\n}\n\n// RunDummyServerE runs a dummy HTTP server on a unique port that will return the given text. Returns the Listener for the server, the\n// port it's listening on, or an error if something went wrong while trying to start the listener. Make sure to call\n// the Close() method on the Listener when you're done!\nfunc RunDummyServerE(t testing.TestingT, text string) (net.Listener, int, error) {\n\tport := getNextPort()\n\n\t// Create new serve mux so that multiple handlers can be created\n\tserver := http.NewServeMux()\n\tserver.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) {\n\t\tfmt.Fprintf(w, \"%s\", text)\n\t})\n\n\tlogger.Default.Logf(t, \"Starting dummy HTTP server in port %d that will return the text '%s'\", port, text)\n\n\tlistener, err := net.Listen(\"tcp\", \":\"+strconv.Itoa(port))\n\tif err != nil {\n\t\treturn nil, 0, fmt.Errorf(\"error listening: %s\", err)\n\t}\n\n\tgo http.Serve(listener, server)\n\n\treturn listener, port, err\n}\n\n// RunDummyServerWithHandlers runs a dummy HTTP server on a unique port that will serve given handlers. Returns the Listener for the server,\n// the port it's listening on, or an error if something went wrong while trying to start the listener. Make sure to call\n// the Close() method on the Listener when you're done!\nfunc RunDummyServerWithHandlers(t testing.TestingT, handlers map[string]func(http.ResponseWriter, *http.Request)) (net.Listener, int) {\n\tlistener, port, err := RunDummyServerWithHandlersE(t, handlers)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn listener, port\n}\n\n// RunDummyServerWithHandlersE runs a dummy HTTP server on a unique port that will server given handlers. Returns the Listener for the server,\n// the port it's listening on, or an error if something went wrong while trying to start the listener. Make sure to call\n// the Close() method on the Listener when you're done!\nfunc RunDummyServerWithHandlersE(t testing.TestingT, handlers map[string]func(http.ResponseWriter, *http.Request)) (net.Listener, int, error) {\n\tport := getNextPort()\n\n\tserver := http.NewServeMux()\n\tfor path, handler := range handlers {\n\t\tserver.HandleFunc(path, handler)\n\t}\n\n\tlogger.Default.Logf(t, \"Starting dummy HTTP server in port %d\", port)\n\n\tlistener, err := net.Listen(\"tcp\", \":\"+strconv.Itoa(port))\n\tif err != nil {\n\t\treturn nil, 0, fmt.Errorf(\"error listening: %s\", err)\n\t}\n\n\tgo http.Serve(listener, server)\n\n\treturn listener, port, err\n}\n\n// DO NOT ACCESS THIS VARIABLE DIRECTLY. See getNextPort() below.\nvar testServerPort int32 = 8080\n\n// Since we run tests in parallel, we need to ensure that each test runs on a different port. This function returns a\n// unique port by atomically incrementing the testServerPort variable.\nfunc getNextPort() int {\n\treturn int(atomic.AddInt32(&testServerPort, 1))\n}\n"
  },
  {
    "path": "modules/http-helper/dummy_server_test.go",
    "content": "package http_helper\n\nimport (\n\t\"crypto/tls\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestRunDummyServer(t *testing.T) {\n\tt.Parallel()\n\n\tuniqueID := random.UniqueId()\n\ttext := fmt.Sprintf(\"dummy-server-%s\", uniqueID)\n\n\tlistener, port := RunDummyServer(t, text)\n\tdefer shutDownServer(t, listener)\n\n\turl := fmt.Sprintf(\"http://localhost:%d\", port)\n\tHttpGetWithValidation(t, url, &tls.Config{}, 200, text)\n}\n\nfunc TestContinuouslyCheck(t *testing.T) {\n\tt.Parallel()\n\n\tuniqueID := random.UniqueId()\n\ttext := fmt.Sprintf(\"dummy-server-%s\", uniqueID)\n\tstopChecking := make(chan bool, 1)\n\n\tlistener, port := RunDummyServer(t, text)\n\n\turl := fmt.Sprintf(\"http://localhost:%d\", port)\n\twg, responses := ContinuouslyCheckUrl(t, url, stopChecking, 1*time.Second)\n\tdefer func() {\n\t\tstopChecking <- true\n\t\tcounts := 0\n\t\tfor response := range responses {\n\t\t\tcounts++\n\t\t\tassert.Equal(t, response.StatusCode, 200)\n\t\t\tassert.Equal(t, response.Body, text)\n\t\t}\n\t\twg.Wait()\n\t\t// Make sure we made at least one call\n\t\tassert.NotEqual(t, counts, 0)\n\t\tshutDownServer(t, listener)\n\t}()\n\ttime.Sleep(5 * time.Second)\n}\n\nfunc TestRunDummyServersWithHandlers(t *testing.T) {\n\t// Given:\n\t//   several dummy servers, each with the same path\n\t// When:\n\t//   all of them are started at the same time\n\t// Then:\n\t//   every one of them can be started and serves their unique content\n\tt.Parallel()\n\n\tnumServers := 2\n\n\ttype testData struct {\n\t\ttext string\n\t\tport int\n\t}\n\tdata := make([]testData, numServers)\n\n\tfor idx := 0; idx < numServers; idx++ {\n\t\tuniqueID := random.UniqueId()\n\t\ttext := fmt.Sprintf(\"dummy-server-%s\", uniqueID)\n\n\t\thandler := func(w http.ResponseWriter, r *http.Request) {\n\t\t\tfmt.Fprintf(w, \"%s\", text)\n\t\t}\n\n\t\thandlerMap := map[string]func(http.ResponseWriter, *http.Request){\n\t\t\t// The same endpoint is provided for each dummy server.\n\t\t\t\"/v1/endpoint\": handler,\n\t\t}\n\n\t\tlistener, port := RunDummyServerWithHandlers(t, handlerMap)\n\t\tdefer shutDownServer(t, listener)\n\n\t\tdata[idx] = testData{text: text, port: port}\n\t}\n\n\tfor _, testInstance := range data {\n\t\turl := fmt.Sprintf(\"http://localhost:%d/v1/endpoint\", testInstance.port)\n\t\tHttpGetWithValidation(t, url, &tls.Config{}, 200, testInstance.text)\n\t}\n}\n\nfunc shutDownServer(t *testing.T, listener io.Closer) {\n\terr := listener.Close()\n\tassert.NoError(t, err)\n}\n"
  },
  {
    "path": "modules/http-helper/errors.go",
    "content": "package http_helper\n\nimport \"fmt\"\n\n// ValidationFunctionFailed is an error that occurs if a validation function fails.\ntype ValidationFunctionFailed struct {\n\tUrl    string\n\tStatus int\n\tBody   string\n}\n\nfunc (err ValidationFunctionFailed) Error() string {\n\treturn fmt.Sprintf(\"Validation failed for URL %s. Response status: %d. Response body:\\n%s\", err.Url, err.Status, err.Body)\n}\n"
  },
  {
    "path": "modules/http-helper/http_helper.go",
    "content": "// Package http_helper contains helpers to interact with deployed resources through HTTP.\npackage http_helper\n\nimport (\n\t\"bytes\"\n\t\"crypto/tls\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/gruntwork-io/terratest/modules/logger\"\n\t\"github.com/gruntwork-io/terratest/modules/retry\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n)\n\ntype HttpGetOptions struct {\n\tUrl       string\n\tTlsConfig *tls.Config\n\tTimeout   int\n}\n\ntype HttpDoOptions struct {\n\tMethod    string\n\tUrl       string\n\tBody      io.Reader\n\tHeaders   map[string]string\n\tTlsConfig *tls.Config\n\tTimeout   int\n}\n\n// HttpGet performs an HTTP GET, with an optional pointer to a custom TLS configuration, on the given URL and\n// return the HTTP status code and body. If there's any error, fail the test.\nfunc HttpGet(t testing.TestingT, url string, tlsConfig *tls.Config) (int, string) {\n\treturn HttpGetWithOptions(t, HttpGetOptions{Url: url, TlsConfig: tlsConfig, Timeout: 10})\n}\n\n// HttpGetWithOptions performs an HTTP GET, with an optional pointer to a custom TLS configuration, on the given URL and\n// return the HTTP status code and body. If there's any error, fail the test.\nfunc HttpGetWithOptions(t testing.TestingT, options HttpGetOptions) (int, string) {\n\tstatusCode, body, err := HttpGetWithOptionsE(t, options)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn statusCode, body\n}\n\n// HttpGetE performs an HTTP GET, with an optional pointer to a custom TLS configuration, on the given URL and\n// return the HTTP status code, body, and any error.\nfunc HttpGetE(t testing.TestingT, url string, tlsConfig *tls.Config) (int, string, error) {\n\treturn HttpGetWithOptionsE(t, HttpGetOptions{Url: url, TlsConfig: tlsConfig, Timeout: 10})\n}\n\n// HttpGetWithOptionsE performs an HTTP GET, with an optional pointer to a custom TLS configuration, on the given URL and\n// return the HTTP status code, body, and any error.\nfunc HttpGetWithOptionsE(t testing.TestingT, options HttpGetOptions) (int, string, error) {\n\tlogger.Default.Logf(t, \"Making an HTTP GET call to URL %s\", options.Url)\n\n\t// Set HTTP client transport config\n\ttr := http.DefaultTransport.(*http.Transport).Clone()\n\ttr.TLSClientConfig = options.TlsConfig\n\n\tclient := http.Client{\n\t\t// By default, Go does not impose a timeout, so an HTTP connection attempt can hang for a LONG time.\n\t\tTimeout: time.Duration(options.Timeout) * time.Second,\n\t\t// Include the previously created transport config\n\t\tTransport: tr,\n\t}\n\n\tresp, err := client.Get(options.Url)\n\tif err != nil {\n\t\treturn -1, \"\", err\n\t}\n\n\tdefer resp.Body.Close()\n\tbody, err := io.ReadAll(resp.Body)\n\n\tif err != nil {\n\t\treturn -1, \"\", err\n\t}\n\n\treturn resp.StatusCode, strings.TrimSpace(string(body)), nil\n}\n\n// HttpGetWithValidation performs an HTTP GET on the given URL and verify that you get back the expected status code and body. If either\n// doesn't match, fail the test.\nfunc HttpGetWithValidation(t testing.TestingT, url string, tlsConfig *tls.Config, expectedStatusCode int, expectedBody string) {\n\toptions := HttpGetOptions{Url: url, TlsConfig: tlsConfig, Timeout: 10}\n\tHttpGetWithValidationWithOptions(t, options, expectedStatusCode, expectedBody)\n}\n\n// HttpGetWithValidationWithOptions performs an HTTP GET on the given URL and verify that you get back the expected status code and body. If either\n// doesn't match, fail the test.\nfunc HttpGetWithValidationWithOptions(t testing.TestingT, options HttpGetOptions, expectedStatusCode int, expectedBody string) {\n\terr := HttpGetWithValidationWithOptionsE(t, options, expectedStatusCode, expectedBody)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n\n// HttpGetWithValidationE performs an HTTP GET on the given URL and verify that you get back the expected status code and body. If either\n// doesn't match, return an error.\nfunc HttpGetWithValidationE(t testing.TestingT, url string, tlsConfig *tls.Config, expectedStatusCode int, expectedBody string) error {\n\toptions := HttpGetOptions{Url: url, TlsConfig: tlsConfig, Timeout: 10}\n\treturn HttpGetWithValidationWithOptionsE(t, options, expectedStatusCode, expectedBody)\n}\n\n// HttpGetWithValidationWithOptionsE performs an HTTP GET on the given URL and verify that you get back the expected status code and body. If either\n// doesn't match, return an error.\nfunc HttpGetWithValidationWithOptionsE(t testing.TestingT, options HttpGetOptions, expectedStatusCode int, expectedBody string) error {\n\treturn HttpGetWithCustomValidationWithOptionsE(t, options, func(statusCode int, body string) bool {\n\t\treturn statusCode == expectedStatusCode && body == expectedBody\n\t})\n}\n\n// HttpGetWithCustomValidation performs an HTTP GET on the given URL and validate the returned status code and body using the given function.\nfunc HttpGetWithCustomValidation(t testing.TestingT, url string, tlsConfig *tls.Config, validateResponse func(int, string) bool) {\n\tHttpGetWithCustomValidationWithOptions(t, HttpGetOptions{Url: url, TlsConfig: tlsConfig, Timeout: 10}, validateResponse)\n}\n\n// HttpGetWithCustomValidationWithOptions performs an HTTP GET on the given URL and validate the returned status code and body using the given function.\nfunc HttpGetWithCustomValidationWithOptions(t testing.TestingT, options HttpGetOptions, validateResponse func(int, string) bool) {\n\terr := HttpGetWithCustomValidationWithOptionsE(t, options, validateResponse)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n\n// HttpGetWithCustomValidationE performs an HTTP GET on the given URL and validate the returned status code and body using the given function.\nfunc HttpGetWithCustomValidationE(t testing.TestingT, url string, tlsConfig *tls.Config, validateResponse func(int, string) bool) error {\n\treturn HttpGetWithCustomValidationWithOptionsE(t, HttpGetOptions{Url: url, TlsConfig: tlsConfig, Timeout: 10}, validateResponse)\n}\n\n// HttpGetWithCustomValidationWithOptionsE performs an HTTP GET on the given URL and validate the returned status code and body using the given function.\nfunc HttpGetWithCustomValidationWithOptionsE(t testing.TestingT, options HttpGetOptions, validateResponse func(int, string) bool) error {\n\tstatusCode, body, err := HttpGetWithOptionsE(t, options)\n\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif !validateResponse(statusCode, body) {\n\t\treturn ValidationFunctionFailed{Url: options.Url, Status: statusCode, Body: body}\n\t}\n\n\treturn nil\n}\n\n// HttpGetWithRetry repeatedly performs an HTTP GET on the given URL until the given status code and body are returned or until max\n// retries has been exceeded.\nfunc HttpGetWithRetry(t testing.TestingT, url string, tlsConfig *tls.Config, expectedStatus int, expectedBody string, retries int, sleepBetweenRetries time.Duration) {\n\toptions := HttpGetOptions{Url: url, TlsConfig: tlsConfig, Timeout: 10}\n\tHttpGetWithRetryWithOptions(t, options, expectedStatus, expectedBody, retries, sleepBetweenRetries)\n}\n\n// HttpGetWithRetryWithOptions repeatedly performs an HTTP GET on the given URL until the given status code and body are returned or until max\n// retries has been exceeded.\nfunc HttpGetWithRetryWithOptions(t testing.TestingT, options HttpGetOptions, expectedStatus int, expectedBody string, retries int, sleepBetweenRetries time.Duration) {\n\terr := HttpGetWithRetryWithOptionsE(t, options, expectedStatus, expectedBody, retries, sleepBetweenRetries)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n\n// HttpGetWithRetryE repeatedly performs an HTTP GET on the given URL until the given status code and body are returned or until max\n// retries has been exceeded.\nfunc HttpGetWithRetryE(t testing.TestingT, url string, tlsConfig *tls.Config, expectedStatus int, expectedBody string, retries int, sleepBetweenRetries time.Duration) error {\n\toptions := HttpGetOptions{Url: url, TlsConfig: tlsConfig, Timeout: 10}\n\treturn HttpGetWithRetryWithOptionsE(t, options, expectedStatus, expectedBody, retries, sleepBetweenRetries)\n}\n\n// HttpGetWithRetryWithOptionsE repeatedly performs an HTTP GET on the given URL until the given status code and body are returned or until max\n// retries has been exceeded.\nfunc HttpGetWithRetryWithOptionsE(t testing.TestingT, options HttpGetOptions, expectedStatus int, expectedBody string, retries int, sleepBetweenRetries time.Duration) error {\n\t_, err := retry.DoWithRetryE(t, fmt.Sprintf(\"HTTP GET to URL %s\", options.Url), retries, sleepBetweenRetries, func() (string, error) {\n\t\treturn \"\", HttpGetWithValidationWithOptionsE(t, options, expectedStatus, expectedBody)\n\t})\n\n\treturn err\n}\n\n// HttpGetWithRetryWithCustomValidation repeatedly performs an HTTP GET on the given URL until the given validation function returns true or max retries\n// has been exceeded.\nfunc HttpGetWithRetryWithCustomValidation(t testing.TestingT, url string, tlsConfig *tls.Config, retries int, sleepBetweenRetries time.Duration, validateResponse func(int, string) bool) {\n\toptions := HttpGetOptions{Url: url, TlsConfig: tlsConfig, Timeout: 10}\n\tHttpGetWithRetryWithCustomValidationWithOptions(t, options, retries, sleepBetweenRetries, validateResponse)\n}\n\n// HttpGetWithRetryWithCustomValidationWithOptions repeatedly performs an HTTP GET on the given URL until the given validation function returns true or max retries\n// has been exceeded.\nfunc HttpGetWithRetryWithCustomValidationWithOptions(t testing.TestingT, options HttpGetOptions, retries int, sleepBetweenRetries time.Duration, validateResponse func(int, string) bool) {\n\terr := HttpGetWithRetryWithCustomValidationWithOptionsE(t, options, retries, sleepBetweenRetries, validateResponse)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n\n// HttpGetWithRetryWithCustomValidationE repeatedly performs an HTTP GET on the given URL until the given validation function returns true or max retries\n// has been exceeded.\nfunc HttpGetWithRetryWithCustomValidationE(t testing.TestingT, url string, tlsConfig *tls.Config, retries int, sleepBetweenRetries time.Duration, validateResponse func(int, string) bool) error {\n\toptions := HttpGetOptions{Url: url, TlsConfig: tlsConfig, Timeout: 10}\n\treturn HttpGetWithRetryWithCustomValidationWithOptionsE(t, options, retries, sleepBetweenRetries, validateResponse)\n}\n\n// HttpGetWithRetryWithCustomValidationWithOptionsE repeatedly performs an HTTP GET on the given URL until the given validation function returns true or max retries\n// has been exceeded.\nfunc HttpGetWithRetryWithCustomValidationWithOptionsE(t testing.TestingT, options HttpGetOptions, retries int, sleepBetweenRetries time.Duration, validateResponse func(int, string) bool) error {\n\t_, err := retry.DoWithRetryE(t, fmt.Sprintf(\"HTTP GET to URL %s\", options.Url), retries, sleepBetweenRetries, func() (string, error) {\n\t\treturn \"\", HttpGetWithCustomValidationWithOptionsE(t, options, validateResponse)\n\t})\n\n\treturn err\n}\n\n// HTTPDo performs the given HTTP method on the given URL and return the HTTP status code and body.\n// If there's any error, fail the test.\nfunc HTTPDo(\n\tt testing.TestingT, method string, url string, body io.Reader,\n\theaders map[string]string, tlsConfig *tls.Config,\n) (int, string) {\n\toptions := HttpDoOptions{\n\t\tMethod:    method,\n\t\tUrl:       url,\n\t\tBody:      body,\n\t\tHeaders:   headers,\n\t\tTlsConfig: tlsConfig,\n\t\tTimeout:   10}\n\treturn HTTPDoWithOptions(t, options)\n}\n\n// HTTPDoWithOptions performs the given HTTP method on the given URL and return the HTTP status code and body.\n// If there's any error, fail the test.\nfunc HTTPDoWithOptions(\n\tt testing.TestingT, options HttpDoOptions,\n) (int, string) {\n\tstatusCode, respBody, err := HTTPDoWithOptionsE(t, options)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn statusCode, respBody\n}\n\n// HTTPDoE performs the given HTTP method on the given URL and return the HTTP status code, body, and any error.\nfunc HTTPDoE(\n\tt testing.TestingT, method string, url string, body io.Reader,\n\theaders map[string]string, tlsConfig *tls.Config,\n) (int, string, error) {\n\toptions := HttpDoOptions{\n\t\tMethod:    method,\n\t\tUrl:       url,\n\t\tBody:      body,\n\t\tHeaders:   headers,\n\t\tTimeout:   10,\n\t\tTlsConfig: tlsConfig}\n\treturn HTTPDoWithOptionsE(t, options)\n}\n\n// HTTPDoWithOptionsE performs the given HTTP method on the given URL and return the HTTP status code, body, and any error.\nfunc HTTPDoWithOptionsE(\n\tt testing.TestingT, options HttpDoOptions,\n) (int, string, error) {\n\tlogger.Default.Logf(t, \"Making an HTTP %s call to URL %s\", options.Method, options.Url)\n\n\ttr := http.DefaultTransport.(*http.Transport).Clone()\n\ttr.TLSClientConfig = options.TlsConfig\n\n\tclient := http.Client{\n\t\t// By default, Go does not impose a timeout, so an HTTP connection attempt can hang for a LONG time.\n\t\tTimeout:   time.Duration(options.Timeout) * time.Second,\n\t\tTransport: tr,\n\t}\n\n\treq := newRequest(options.Method, options.Url, options.Body, options.Headers)\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn -1, \"\", err\n\t}\n\n\tdefer resp.Body.Close()\n\trespBody, err := io.ReadAll(resp.Body)\n\n\tif err != nil {\n\t\treturn -1, \"\", err\n\t}\n\n\treturn resp.StatusCode, strings.TrimSpace(string(respBody)), nil\n}\n\n// HTTPDoWithRetry repeatedly performs the given HTTP method on the given URL until the given status code and body are\n// returned or until max retries has been exceeded.\n// The function compares the expected status code against the received one and fails if they don't match.\nfunc HTTPDoWithRetry(\n\tt testing.TestingT, method string, url string,\n\tbody []byte, headers map[string]string, expectedStatus int,\n\tretries int, sleepBetweenRetries time.Duration, tlsConfig *tls.Config,\n) string {\n\toptions := HttpDoOptions{\n\t\tMethod:    method,\n\t\tUrl:       url,\n\t\tBody:      bytes.NewReader(body),\n\t\tHeaders:   headers,\n\t\tTlsConfig: tlsConfig,\n\t\tTimeout:   10}\n\treturn HTTPDoWithRetryWithOptions(t, options, expectedStatus, retries, sleepBetweenRetries)\n}\n\n// HTTPDoWithRetryWithOptions repeatedly performs the given HTTP method on the given URL until the given status code and body are\n// returned or until max retries has been exceeded.\n// The function compares the expected status code against the received one and fails if they don't match.\nfunc HTTPDoWithRetryWithOptions(\n\tt testing.TestingT, options HttpDoOptions, expectedStatus int,\n\tretries int, sleepBetweenRetries time.Duration,\n) string {\n\tout, err := HTTPDoWithRetryWithOptionsE(t, options, expectedStatus, retries, sleepBetweenRetries)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn out\n}\n\n// HTTPDoWithRetryE repeatedly performs the given HTTP method on the given URL until the given status code and body are\n// returned or until max retries has been exceeded.\n// The function compares the expected status code against the received one and fails if they don't match.\nfunc HTTPDoWithRetryE(\n\tt testing.TestingT, method string, url string,\n\tbody []byte, headers map[string]string, expectedStatus int,\n\tretries int, sleepBetweenRetries time.Duration, tlsConfig *tls.Config,\n) (string, error) {\n\toptions := HttpDoOptions{\n\t\tMethod:    method,\n\t\tUrl:       url,\n\t\tBody:      bytes.NewReader(body),\n\t\tHeaders:   headers,\n\t\tTlsConfig: tlsConfig,\n\t\tTimeout:   10}\n\n\treturn HTTPDoWithRetryWithOptionsE(t, options, expectedStatus, retries, sleepBetweenRetries)\n}\n\n// HTTPDoWithRetryWithOptionsE repeatedly performs the given HTTP method on the given URL until the given status code and body are\n// returned or until max retries has been exceeded.\n// The function compares the expected status code against the received one and fails if they don't match.\nfunc HTTPDoWithRetryWithOptionsE(\n\tt testing.TestingT, options HttpDoOptions, expectedStatus int,\n\tretries int, sleepBetweenRetries time.Duration,\n) (string, error) {\n\tvar data []byte\n\tif options.Body != nil {\n\t\t// The request body is closed after a request is complete.\n\t\t// Read the underlying data and cache it, so we can reuse for retried requests.\n\t\tb, err := io.ReadAll(options.Body)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\tdata = b\n\t}\n\n\toptions.Body = nil\n\n\tout, err := retry.DoWithRetryE(\n\t\tt, fmt.Sprintf(\"HTTP %s to URL %s\", options.Method, options.Url), retries,\n\t\tsleepBetweenRetries, func() (string, error) {\n\t\t\toptions.Body = bytes.NewReader(data)\n\t\t\tstatusCode, out, err := HTTPDoWithOptionsE(t, options)\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", err\n\t\t\t}\n\t\t\tlogger.Default.Logf(t, \"output: %v\", out)\n\t\t\tif statusCode != expectedStatus {\n\t\t\t\treturn \"\", ValidationFunctionFailed{Url: options.Url, Status: statusCode}\n\t\t\t}\n\t\t\treturn out, nil\n\t\t})\n\n\treturn out, err\n}\n\n// HTTPDoWithValidationRetry repeatedly performs the given HTTP method on the given URL until the given status code and\n// body are returned or until max retries has been exceeded.\nfunc HTTPDoWithValidationRetry(\n\tt testing.TestingT, method string, url string,\n\tbody []byte, headers map[string]string, expectedStatus int,\n\texpectedBody string, retries int, sleepBetweenRetries time.Duration, tlsConfig *tls.Config,\n) {\n\toptions := HttpDoOptions{\n\t\tMethod:    method,\n\t\tUrl:       url,\n\t\tBody:      bytes.NewReader(body),\n\t\tHeaders:   headers,\n\t\tTlsConfig: tlsConfig,\n\t\tTimeout:   10}\n\n\tHTTPDoWithValidationRetryWithOptions(t, options, expectedStatus, expectedBody, retries, sleepBetweenRetries)\n}\n\n// HTTPDoWithValidationRetryWithOptions repeatedly performs the given HTTP method on the given URL until the given status code and\n// body are returned or until max retries has been exceeded.\nfunc HTTPDoWithValidationRetryWithOptions(\n\tt testing.TestingT, options HttpDoOptions, expectedStatus int,\n\texpectedBody string, retries int, sleepBetweenRetries time.Duration,\n) {\n\terr := HTTPDoWithValidationRetryWithOptionsE(t, options, expectedStatus, expectedBody, retries, sleepBetweenRetries)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n\n// HTTPDoWithValidationRetryE repeatedly performs the given HTTP method on the given URL until the given status code and\n// body are returned or until max retries has been exceeded.\nfunc HTTPDoWithValidationRetryE(\n\tt testing.TestingT, method string, url string,\n\tbody []byte, headers map[string]string, expectedStatus int,\n\texpectedBody string, retries int, sleepBetweenRetries time.Duration, tlsConfig *tls.Config,\n) error {\n\toptions := HttpDoOptions{\n\t\tMethod:    method,\n\t\tUrl:       url,\n\t\tBody:      bytes.NewReader(body),\n\t\tHeaders:   headers,\n\t\tTlsConfig: tlsConfig,\n\t\tTimeout:   10}\n\n\treturn HTTPDoWithValidationRetryWithOptionsE(t, options, expectedStatus, expectedBody, retries, sleepBetweenRetries)\n}\n\n// HTTPDoWithValidationRetryWithOptionsE repeatedly performs the given HTTP method on the given URL until the given status code and\n// body are returned or until max retries has been exceeded.\nfunc HTTPDoWithValidationRetryWithOptionsE(\n\tt testing.TestingT, options HttpDoOptions, expectedStatus int,\n\texpectedBody string, retries int, sleepBetweenRetries time.Duration,\n) error {\n\t_, err := retry.DoWithRetryE(t, fmt.Sprintf(\"HTTP %s to URL %s\", options.Method, options.Url), retries,\n\t\tsleepBetweenRetries, func() (string, error) {\n\t\t\treturn \"\", HTTPDoWithValidationWithOptionsE(t, options, expectedStatus, expectedBody)\n\t\t})\n\n\treturn err\n}\n\n// HTTPDoWithValidation performs the given HTTP method on the given URL and verify that you get back the expected status\n// code and body. If either doesn't match, fail the test.\nfunc HTTPDoWithValidation(t testing.TestingT, method string, url string, body io.Reader, headers map[string]string, expectedStatusCode int, expectedBody string, tlsConfig *tls.Config) {\n\toptions := HttpDoOptions{\n\t\tMethod:    method,\n\t\tUrl:       url,\n\t\tBody:      body,\n\t\tHeaders:   headers,\n\t\tTlsConfig: tlsConfig,\n\t\tTimeout:   10}\n\n\tHTTPDoWithValidationWithOptions(t, options, expectedStatusCode, expectedBody)\n}\n\n// HTTPDoWithValidationWithOptions performs the given HTTP method on the given URL and verify that you get back the expected status\n// code and body. If either doesn't match, fail the test.\nfunc HTTPDoWithValidationWithOptions(t testing.TestingT, options HttpDoOptions, expectedStatusCode int, expectedBody string) {\n\terr := HTTPDoWithValidationWithOptionsE(t, options, expectedStatusCode, expectedBody)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n\n// HTTPDoWithValidationE performs the given HTTP method on the given URL and verify that you get back the expected status\n// code and body. If either doesn't match, return an error.\nfunc HTTPDoWithValidationE(t testing.TestingT, method string, url string, body io.Reader, headers map[string]string, expectedStatusCode int, expectedBody string, tlsConfig *tls.Config) error {\n\toptions := HttpDoOptions{\n\t\tMethod:    method,\n\t\tUrl:       url,\n\t\tBody:      body,\n\t\tHeaders:   headers,\n\t\tTlsConfig: tlsConfig,\n\t\tTimeout:   10}\n\n\treturn HTTPDoWithValidationWithOptionsE(t, options, expectedStatusCode, expectedBody)\n}\n\n// HTTPDoWithValidationWithOptionsE performs the given HTTP method on the given URL and verify that you get back the expected status\n// code and body. If either doesn't match, return an error.\nfunc HTTPDoWithValidationWithOptionsE(t testing.TestingT, options HttpDoOptions, expectedStatusCode int, expectedBody string) error {\n\treturn HTTPDoWithCustomValidationWithOptionsE(t, options, func(statusCode int, body string) bool {\n\t\treturn statusCode == expectedStatusCode && body == expectedBody\n\t})\n}\n\n// HTTPDoWithCustomValidation performs the given HTTP method on the given URL and validate the returned status code and\n// body using the given function.\nfunc HTTPDoWithCustomValidation(t testing.TestingT, method string, url string, body io.Reader, headers map[string]string, validateResponse func(int, string) bool, tlsConfig *tls.Config) {\n\toptions := HttpDoOptions{\n\t\tMethod:    method,\n\t\tUrl:       url,\n\t\tBody:      body,\n\t\tHeaders:   headers,\n\t\tTlsConfig: tlsConfig,\n\t\tTimeout:   10}\n\n\tHTTPDoWithCustomValidationWithOptions(t, options, validateResponse)\n}\n\n// HTTPDoWithCustomValidationWithOptions performs the given HTTP method on the given URL and validate the returned status code and\n// body using the given function.\nfunc HTTPDoWithCustomValidationWithOptions(t testing.TestingT, options HttpDoOptions, validateResponse func(int, string) bool) {\n\terr := HTTPDoWithCustomValidationWithOptionsE(t, options, validateResponse)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n\n// HTTPDoWithCustomValidationE performs the given HTTP method on the given URL and validate the returned status code and\n// body using the given function.\nfunc HTTPDoWithCustomValidationE(t testing.TestingT, method string, url string, body io.Reader, headers map[string]string, validateResponse func(int, string) bool, tlsConfig *tls.Config) error {\n\toptions := HttpDoOptions{\n\t\tMethod:    method,\n\t\tUrl:       url,\n\t\tBody:      body,\n\t\tHeaders:   headers,\n\t\tTlsConfig: tlsConfig,\n\t\tTimeout:   10}\n\n\treturn HTTPDoWithCustomValidationWithOptionsE(t, options, validateResponse)\n}\n\n// HTTPDoWithCustomValidationWithOptionsE performs the given HTTP method on the given URL and validate the returned status code and\n// body using the given function.\nfunc HTTPDoWithCustomValidationWithOptionsE(t testing.TestingT, options HttpDoOptions, validateResponse func(int, string) bool) error {\n\tstatusCode, respBody, err := HTTPDoWithOptionsE(t, options)\n\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif !validateResponse(statusCode, respBody) {\n\t\treturn ValidationFunctionFailed{Url: options.Url, Status: statusCode, Body: respBody}\n\t}\n\n\treturn nil\n}\n\nfunc newRequest(method string, url string, body io.Reader, headers map[string]string) *http.Request {\n\treq, err := http.NewRequest(method, url, body)\n\tif err != nil {\n\t\treturn nil\n\t}\n\tfor k, v := range headers {\n\t\tswitch k {\n\t\tcase \"Host\":\n\t\t\treq.Host = v\n\t\tdefault:\n\t\t\treq.Header.Add(k, v)\n\t\t}\n\t}\n\treturn req\n}\n"
  },
  {
    "path": "modules/http-helper/http_helper_test.go",
    "content": "package http_helper\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"regexp\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc getTestServerForFunction(handler func(w http.ResponseWriter,\n\tr *http.Request)) *httptest.Server {\n\treturn httptest.NewServer(http.HandlerFunc(handler))\n}\n\nfunc TestOkBody(t *testing.T) {\n\tt.Parallel()\n\tts := getTestServerForFunction(bodyCopyHandler)\n\tdefer ts.Close()\n\turl := ts.URL\n\texpectedBody := \"Hello, Terratest!\"\n\tbody := bytes.NewReader([]byte(expectedBody))\n\tstatusCode, respBody := HTTPDo(t, \"POST\", url, body, nil, nil)\n\n\texpectedCode := 200\n\tif statusCode != expectedCode {\n\t\tt.Errorf(\"handler returned wrong status code: got %v want %v\", statusCode, expectedCode)\n\t}\n\tif respBody != expectedBody {\n\t\tt.Errorf(\"handler returned wrong body: got %v want %v\", respBody, expectedBody)\n\t}\n}\n\nfunc TestHTTPDoWithValidation(t *testing.T) {\n\tt.Parallel()\n\tts := getTestServerForFunction(bodyCopyHandler)\n\tdefer ts.Close()\n\turl := ts.URL\n\texpectedBody := \"Hello, Terratest!\"\n\tbody := bytes.NewReader([]byte(expectedBody))\n\tHTTPDoWithValidation(t, \"POST\", url, body, nil, 200, expectedBody, nil)\n}\n\nfunc TestHTTPDoWithCustomValidation(t *testing.T) {\n\tt.Parallel()\n\tts := getTestServerForFunction(bodyCopyHandler)\n\tdefer ts.Close()\n\turl := ts.URL\n\texpectedBody := \"Hello, Terratest!\"\n\tbody := bytes.NewReader([]byte(expectedBody))\n\n\tcustomValidation := func(statusCode int, response string) bool {\n\t\treturn statusCode == 200 && response == expectedBody\n\t}\n\n\tHTTPDoWithCustomValidation(t, \"POST\", url, body, nil, customValidation, nil)\n}\n\nfunc TestOkHeaders(t *testing.T) {\n\tt.Parallel()\n\tts := getTestServerForFunction(headersCopyHandler)\n\tdefer ts.Close()\n\turl := ts.URL\n\theaders := map[string]string{\"Authorization\": \"Bearer 1a2b3c99ff\"}\n\tstatusCode, respBody := HTTPDo(t, \"POST\", url, nil, headers, nil)\n\n\texpectedCode := 200\n\tif statusCode != expectedCode {\n\t\tt.Errorf(\"handler returned wrong status code: got %v want %v\", statusCode, expectedCode)\n\t}\n\texpectedLine := \"Authorization: Bearer 1a2b3c99ff\"\n\tif !strings.Contains(respBody, expectedLine) {\n\t\tt.Errorf(\"handler returned wrong body: got %v want %v\", respBody, expectedLine)\n\t}\n}\n\nfunc TestWrongStatus(t *testing.T) {\n\tt.Parallel()\n\tts := getTestServerForFunction(wrongStatusHandler)\n\tdefer ts.Close()\n\turl := ts.URL\n\tstatusCode, _ := HTTPDo(t, \"POST\", url, nil, nil, nil)\n\n\texpectedCode := 500\n\tif statusCode != expectedCode {\n\t\tt.Errorf(\"handler returned wrong status code: got %v want %v\", statusCode, expectedCode)\n\t}\n}\n\nfunc TestRequestTimeout(t *testing.T) {\n\tt.Parallel()\n\tts := getTestServerForFunction(sleepingHandler)\n\tdefer ts.Close()\n\turl := ts.URL\n\t_, _, err := HTTPDoE(t, \"DELETE\", url, nil, nil, nil)\n\n\tif err == nil {\n\t\tt.Error(\"handler didn't return a timeout error\")\n\t}\n\tif !strings.Contains(err.Error(), \"Client.Timeout\") {\n\t\tt.Errorf(\"handler didn't return an expected error, got %q\", err)\n\t}\n}\n\nfunc TestOkWithRetry(t *testing.T) {\n\tt.Parallel()\n\tts := getTestServerForFunction(retryHandler)\n\tdefer ts.Close()\n\tbody := \"TEST_CONTENT\"\n\tbodyBytes := []byte(body)\n\turl := ts.URL\n\tcounter = 3\n\tresponse := HTTPDoWithRetry(t, \"POST\", url, bodyBytes, nil, 200, 10, time.Second, nil)\n\trequire.Equal(t, body, response)\n}\n\nfunc TestErrorWithRetry(t *testing.T) {\n\tt.Parallel()\n\tts := getTestServerForFunction(failRetryHandler)\n\tdefer ts.Close()\n\tfailCounter = 3\n\turl := ts.URL\n\t_, err := HTTPDoWithRetryE(t, \"POST\", url, nil, nil, 200, 2, time.Second, nil)\n\n\tif err == nil {\n\t\tt.Error(\"handler didn't return a retry error\")\n\t}\n\n\tpattern := `unsuccessful after \\d+ retries`\n\tmatch, _ := regexp.MatchString(pattern, err.Error())\n\tif !match {\n\t\tt.Errorf(\"handler didn't return an expected error, got %q\", err)\n\t}\n}\n\nfunc TestEmptyRequestBodyWithRetryWithOptions(t *testing.T) {\n\tt.Parallel()\n\tts := getTestServerForFunction(bodyCopyHandler)\n\tdefer ts.Close()\n\n\toptions := HttpDoOptions{\n\t\tMethod: \"GET\",\n\t\tUrl:    ts.URL,\n\t\tBody:   nil,\n\t}\n\n\tresponse := HTTPDoWithRetryWithOptions(t, options, 200, 0, time.Second)\n\trequire.Equal(t, \"\", response)\n}\n\nfunc bodyCopyHandler(w http.ResponseWriter, r *http.Request) {\n\tw.WriteHeader(http.StatusOK)\n\tbody, _ := io.ReadAll(r.Body)\n\tw.Write(body)\n}\n\nfunc headersCopyHandler(w http.ResponseWriter, r *http.Request) {\n\tw.WriteHeader(http.StatusOK)\n\tvar buffer bytes.Buffer\n\tfor key, values := range r.Header {\n\t\tbuffer.WriteString(fmt.Sprintf(\"%s: %s\\n\", key, strings.Join(values, \",\")))\n\t}\n\tw.Write(buffer.Bytes())\n}\n\nfunc wrongStatusHandler(w http.ResponseWriter, r *http.Request) {\n\tw.WriteHeader(http.StatusInternalServerError)\n}\n\nfunc sleepingHandler(w http.ResponseWriter, r *http.Request) {\n\ttime.Sleep(time.Second * 15)\n}\n\nvar counter int\n\nfunc retryHandler(w http.ResponseWriter, r *http.Request) {\n\tif counter > 0 {\n\t\tcounter--\n\t\tw.WriteHeader(http.StatusServiceUnavailable)\n\t\tio.ReadAll(r.Body)\n\t} else {\n\t\tw.WriteHeader(http.StatusOK)\n\t\tbytes, _ := io.ReadAll(r.Body)\n\t\tw.Write(bytes)\n\t}\n}\n\nvar failCounter int\n\nfunc failRetryHandler(w http.ResponseWriter, r *http.Request) {\n\tif failCounter > 0 {\n\t\tfailCounter--\n\t\tw.WriteHeader(http.StatusServiceUnavailable)\n\t\tio.ReadAll(r.Body)\n\t} else {\n\t\tw.WriteHeader(http.StatusOK)\n\t\tbytes, _ := io.ReadAll(r.Body)\n\t\tw.Write(bytes)\n\t}\n}\n\nfunc TestGlobalProxy(t *testing.T) {\n\tproxiedURL := \"\"\n\thttpProxy := getTestServerForFunction(func(w http.ResponseWriter, r *http.Request) {\n\t\tproxiedURL = r.RequestURI\n\t\tbodyCopyHandler(w, r)\n\t})\n\tt.Cleanup(httpProxy.Close)\n\n\tt.Setenv(\"HTTP_PROXY\", httpProxy.URL)\n\ttargetURL := \"http://www.notexist.com/\"\n\tbody := \"should be copied\"\n\n\tst, b, err := HTTPDoWithOptionsE(t, HttpDoOptions{\n\t\tUrl:    targetURL,\n\t\tMethod: http.MethodPost,\n\t\tBody:   strings.NewReader(body),\n\t})\n\trequire.NoError(t, err)\n\tassert.Equal(t, http.StatusOK, st)\n\tassert.Equal(t, targetURL, proxiedURL)\n\tassert.Equal(t, body, b)\n}\n"
  },
  {
    "path": "modules/k8s/client.go",
    "content": "package k8s\n\nimport (\n\t\"k8s.io/client-go/kubernetes\"\n\t\"k8s.io/client-go/rest\"\n\n\t// The following line loads the gcp plugin which is required to authenticate against GKE clusters.\n\t// See: https://github.com/kubernetes/client-go/issues/242\n\t_ \"k8s.io/client-go/plugin/pkg/client/auth/gcp\"\n\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n)\n\n// GetKubernetesClientE returns a Kubernetes API client that can be used to make requests.\nfunc GetKubernetesClientE(t testing.TestingT) (*kubernetes.Clientset, error) {\n\tkubeConfigPath, err := GetKubeConfigPathE(t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\toptions := NewKubectlOptions(\"\", kubeConfigPath, \"default\")\n\treturn GetKubernetesClientFromOptionsE(t, options)\n}\n\n// GetKubernetesClientFromOptionsE returns a Kubernetes API client given a configured KubectlOptions object.\nfunc GetKubernetesClientFromOptionsE(t testing.TestingT, options *KubectlOptions) (*kubernetes.Clientset, error) {\n\tvar err error\n\tvar config *rest.Config\n\n\tif options.InClusterAuth {\n\t\tconfig, err = rest.InClusterConfig()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\toptions.Logger.Logf(t, \"Configuring Kubernetes client to use the in-cluster serviceaccount token\")\n\t} else if options.RestConfig != nil {\n\t\tconfig = options.RestConfig\n\t\toptions.Logger.Logf(t, \"Configuring Kubernetes client to use provided rest config object set with API server address: %s\", config.Host)\n\t} else {\n\t\tkubeConfigPath, err := options.GetConfigPath(t)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\toptions.Logger.Logf(t, \"Configuring Kubernetes client using config file %s with context %s\", kubeConfigPath, options.ContextName)\n\t\t// Load API config (instead of more low level ClientConfig)\n\t\tconfig, err = LoadApiClientConfigE(kubeConfigPath, options.ContextName)\n\t\tif err != nil {\n\t\t\toptions.Logger.Logf(t, \"Error loading api client config, falling back to in-cluster authentication via serviceaccount token: %s\", err)\n\t\t\tconfig, err = rest.InClusterConfig()\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\toptions.Logger.Logf(t, \"Configuring Kubernetes client to use the in-cluster serviceaccount token\")\n\t\t}\n\t}\n\n\tclientset, err := kubernetes.NewForConfig(config)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn clientset, nil\n}\n"
  },
  {
    "path": "modules/k8s/cluster_role.go",
    "content": "package k8s\n\nimport (\n\t\"context\"\n\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/stretchr/testify/require\"\n\trbacv1 \"k8s.io/api/rbac/v1\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n)\n\n// GetClusterRole returns a Kubernetes ClusterRole resource with the given name. This will fail the test if there is an error.\nfunc GetClusterRole(t testing.TestingT, options *KubectlOptions, roleName string) *rbacv1.ClusterRole {\n\trole, err := GetClusterRoleE(t, options, roleName)\n\trequire.NoError(t, err)\n\treturn role\n}\n\n// GetClusterRoleE returns a Kubernetes ClusterRole resource with the given name.\nfunc GetClusterRoleE(t testing.TestingT, options *KubectlOptions, roleName string) (*rbacv1.ClusterRole, error) {\n\tclientset, err := GetKubernetesClientFromOptionsE(t, options)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn clientset.RbacV1().ClusterRoles().Get(context.Background(), roleName, metav1.GetOptions{})\n}\n"
  },
  {
    "path": "modules/k8s/cluster_role_test.go",
    "content": "//go:build kubeall || kubernetes\n// +build kubeall kubernetes\n\n// NOTE: we have build tags to differentiate kubernetes tests from non-kubernetes tests. This is done because minikube\n// is heavy and can interfere with docker related tests in terratest. Specifically, many of the tests start to fail with\n// `connection refused` errors from `minikube`. To avoid overloading the system, we run the kubernetes tests and helm\n// tests separately from the others. This may not be necessary if you have a sufficiently powerful machine.  We\n// recommend at least 4 cores and 16GB of RAM if you want to run all the tests together.\n\npackage k8s\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestGetClusterRoleEReturnsErrorForNonExistantClusterRole(t *testing.T) {\n\tt.Parallel()\n\n\toptions := NewKubectlOptions(\"\", \"\", \"default\")\n\t_, err := GetClusterRoleE(t, options, \"non-existing-role\")\n\trequire.Error(t, err)\n}\n\nfunc TestGetClusterRoleEReturnsCorrectClusterRoleInCorrectNamespace(t *testing.T) {\n\tt.Parallel()\n\n\toptions := NewKubectlOptions(\"\", \"\", \"default\")\n\tdefer KubectlDeleteFromString(t, options, EXAMPLE_CLUSTER_ROLE_YAML_TEMPLATE)\n\tKubectlApplyFromString(t, options, EXAMPLE_CLUSTER_ROLE_YAML_TEMPLATE)\n\n\trole := GetClusterRole(t, options, \"terratest-cluster-role\")\n\trequire.Equal(t, role.Name, \"terratest-cluster-role\")\n}\n\nconst EXAMPLE_CLUSTER_ROLE_YAML_TEMPLATE = `---\napiVersion: rbac.authorization.k8s.io/v1\nkind: ClusterRole\nmetadata:\n  name: 'terratest-cluster-role'\nrules:\n- apiGroups:\n  - '*'\n  resources:\n  - '*'\n  verbs:\n  - '*'\n`\n"
  },
  {
    "path": "modules/k8s/config.go",
    "content": "package k8s\n\nimport (\n\t\"errors\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"sort\"\n\n\tgwErrors \"github.com/gruntwork-io/go-commons/errors\"\n\thomedir \"github.com/mitchellh/go-homedir\"\n\trestclient \"k8s.io/client-go/rest\"\n\t\"k8s.io/client-go/tools/clientcmd\"\n\t\"k8s.io/client-go/tools/clientcmd/api\"\n\n\t\"github.com/gruntwork-io/terratest/modules/environment\"\n\t\"github.com/gruntwork-io/terratest/modules/files\"\n\t\"github.com/gruntwork-io/terratest/modules/logger\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n)\n\n// LoadConfigFromPath will load a ClientConfig object from a file path that points to a location on disk containing a\n// kubectl config.\nfunc LoadConfigFromPath(path string) clientcmd.ClientConfig {\n\tconfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(\n\t\t&clientcmd.ClientConfigLoadingRules{ExplicitPath: path},\n\t\t&clientcmd.ConfigOverrides{})\n\treturn config\n}\n\n// LoadApiClientConfigE will load a ClientConfig object from a file path that points to a location on disk containing a\n// kubectl config, with the requested context loaded.\nfunc LoadApiClientConfigE(configPath string, contextName string) (*restclient.Config, error) {\n\toverrides := clientcmd.ConfigOverrides{}\n\tif contextName != \"\" {\n\t\toverrides.CurrentContext = contextName\n\t}\n\tconfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(\n\t\t&clientcmd.ClientConfigLoadingRules{ExplicitPath: configPath},\n\t\t&overrides)\n\treturn config.ClientConfig()\n}\n\n// DeleteConfigContextE will remove the context specified at the provided name, and remove any clusters and authinfos\n// that are orphaned as a result of it. The config path is either specified in the environment variable KUBECONFIG or at\n// the user's home directory under `.kube/config`.\nfunc DeleteConfigContextE(t testing.TestingT, contextName string) error {\n\tkubeConfigPath, err := GetKubeConfigPathE(t)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn DeleteConfigContextWithPathE(t, kubeConfigPath, contextName)\n}\n\n// DeleteConfigContextWithPathE will remove the context specified at the provided name, and remove any clusters and\n// authinfos that are orphaned as a result of it.\nfunc DeleteConfigContextWithPathE(t testing.TestingT, kubeConfigPath string, contextName string) error {\n\tlogger.Default.Logf(t, \"Removing kubectl config context %s from config at path %s\", contextName, kubeConfigPath)\n\n\t// Load config and get data structure representing config info\n\tconfig := LoadConfigFromPath(kubeConfigPath)\n\trawConfig, err := config.RawConfig()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Check if the context we want to delete actually exists, and if so, delete it.\n\t_, ok := rawConfig.Contexts[contextName]\n\tif !ok {\n\t\tlogger.Default.Logf(t, \"WARNING: Could not find context %s from config at path %s\", contextName, kubeConfigPath)\n\t\treturn nil\n\t}\n\tdelete(rawConfig.Contexts, contextName)\n\n\t// If the removing context is the current context, be sure to set a new one\n\tif contextName == rawConfig.CurrentContext {\n\t\tif err := setNewContext(&rawConfig); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\t// Finally, clean up orphaned clusters and authinfos and then save config\n\tRemoveOrphanedClusterAndAuthInfoConfig(&rawConfig)\n\tif err := clientcmd.ModifyConfig(config.ConfigAccess(), rawConfig, false); err != nil {\n\t\treturn err\n\t}\n\n\tlogger.Default.Logf(\n\t\tt,\n\t\t\"Removed context %s from config at path %s and any orphaned clusters and authinfos\",\n\t\tcontextName,\n\t\tkubeConfigPath)\n\treturn nil\n}\n\n// setNewContext will pick the alphebetically first available context from the list of contexts in the config to use as\n// the new current context\nfunc setNewContext(config *api.Config) error {\n\t// Sort contextNames and pick the first one\n\tvar contextNames []string\n\tfor name := range config.Contexts {\n\t\tcontextNames = append(contextNames, name)\n\t}\n\tsort.Strings(contextNames)\n\tif len(contextNames) > 0 {\n\t\tconfig.CurrentContext = contextNames[0]\n\t} else {\n\t\treturn errors.New(\"There are no available contexts remaining\")\n\t}\n\treturn nil\n}\n\n// RemoveOrphanedClusterAndAuthInfoConfig will remove all configurations related to clusters and users that have no\n// contexts associated with it\nfunc RemoveOrphanedClusterAndAuthInfoConfig(config *api.Config) {\n\tnewAuthInfos := map[string]*api.AuthInfo{}\n\tnewClusters := map[string]*api.Cluster{}\n\tfor _, context := range config.Contexts {\n\t\tnewClusters[context.Cluster] = config.Clusters[context.Cluster]\n\t\tnewAuthInfos[context.AuthInfo] = config.AuthInfos[context.AuthInfo]\n\t}\n\tconfig.AuthInfos = newAuthInfos\n\tconfig.Clusters = newClusters\n}\n\n// GetKubeConfigPathE determines which file path to use as the kubectl config path\nfunc GetKubeConfigPathE(t testing.TestingT) (string, error) {\n\tkubeConfigPath := environment.GetFirstNonEmptyEnvVarOrEmptyString(t, []string{\"KUBECONFIG\"})\n\tif kubeConfigPath == \"\" {\n\t\tconfigPath, err := KubeConfigPathFromHomeDirE()\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\tkubeConfigPath = configPath\n\t}\n\treturn kubeConfigPath, nil\n}\n\n// KubeConfigPathFromHomeDirE returns a string to the default Kubernetes config path in the home directory. This will\n// error if the home directory can not be determined.\nfunc KubeConfigPathFromHomeDirE() (string, error) {\n\thome, err := homedir.Dir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tconfigPath := filepath.Join(home, \".kube\", \"config\")\n\treturn configPath, err\n}\n\n// CopyHomeKubeConfigToTemp will copy the kubeconfig in the home directory to a temp file. This will fail the test if\n// there are any errors.\nfunc CopyHomeKubeConfigToTemp(t testing.TestingT) string {\n\tpath, err := CopyHomeKubeConfigToTempE(t)\n\tif err != nil {\n\t\tif path != \"\" {\n\t\t\tos.Remove(path)\n\t\t}\n\t\tt.Fatal(err)\n\t}\n\treturn path\n}\n\n// CopyHomeKubeConfigToTempE will copy the kubeconfig in the home directory to a temp file.\nfunc CopyHomeKubeConfigToTempE(t testing.TestingT) (string, error) {\n\tconfigPath, err := KubeConfigPathFromHomeDirE()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\ttmpConfig, err := os.CreateTemp(\"\", \"\")\n\tif err != nil {\n\t\treturn \"\", gwErrors.WithStackTrace(err)\n\t}\n\tdefer tmpConfig.Close()\n\terr = files.CopyFile(configPath, tmpConfig.Name())\n\treturn tmpConfig.Name(), err\n}\n\n// UpsertConfigContext will update or insert a new context to the provided config, binding the provided cluster to the\n// provided user.\nfunc UpsertConfigContext(config *api.Config, contextName string, clusterName string, userName string) {\n\tconfig.Contexts[contextName] = &api.Context{Cluster: clusterName, AuthInfo: userName}\n}\n"
  },
  {
    "path": "modules/k8s/config_test.go",
    "content": "//go:build kubeall || kubernetes\n// +build kubeall kubernetes\n\n// NOTE: we have build tags to differentiate kubernetes tests from non-kubernetes tests. This is done because minikube\n// is heavy and can interfere with docker related tests in terratest. Specifically, many of the tests start to fail with\n// `connection refused` errors from `minikube`. To avoid overloading the system, we run the kubernetes tests and helm\n// tests separately from the others. This may not be necessary if you have a sufficiently powerful machine.  We\n// recommend at least 4 cores and 16GB of RAM if you want to run all the tests together.\n\npackage k8s\n\nimport (\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"k8s.io/client-go/tools/clientcmd\"\n)\n\nfunc TestDeleteConfigContext(t *testing.T) {\n\tt.Parallel()\n\n\tpath := StoreConfigToTempFile(t, BASIC_CONFIG_WITH_EXTRA_CONTEXT)\n\tdefer os.Remove(path)\n\n\terr := DeleteConfigContextWithPathE(t, path, \"extra_minikube\")\n\trequire.NoError(t, err)\n\n\tdata, err := os.ReadFile(path)\n\trequire.NoError(t, err)\n\tstoredConfig := string(data)\n\tassert.Equal(t, BASIC_CONFIG, storedConfig)\n}\n\nfunc TestDeleteConfigContextWithAnotherContextRemaining(t *testing.T) {\n\tt.Parallel()\n\n\tpath := StoreConfigToTempFile(t, BASIC_CONFIG_WITH_EXTRA_CONTEXT_NO_GARBAGE)\n\tdefer os.Remove(path)\n\n\terr := DeleteConfigContextWithPathE(t, path, \"extra_minikube\")\n\trequire.NoError(t, err)\n\n\tdata, err := os.ReadFile(path)\n\trequire.NoError(t, err)\n\tstoredConfig := string(data)\n\tassert.Equal(t, EXPECTED_CONFIG_AFTER_EXTRA_MINIKUBE_DELETED_NO_GARBAGE, storedConfig)\n}\n\nfunc TestRemoveOrphanedClusterAndAuthInfoConfig(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tname string\n\t\tin   string\n\t\tout  string\n\t}{\n\t\t{\n\t\t\t\"TestExtraClusterRemoveOrphanedClusterAndAuthInfoed\",\n\t\t\tBASIC_CONFIG_WITH_EXTRA_CLUSTER,\n\t\t\tBASIC_CONFIG,\n\t\t},\n\t\t{\n\t\t\t\"TestExtraAuthInfoRemoveOrphanedClusterAndAuthInfoed\",\n\t\t\tBASIC_CONFIG_WITH_EXTRA_AUTH_INFO,\n\t\t\tBASIC_CONFIG,\n\t\t},\n\t}\n\tfor _, testCase := range testCases {\n\t\t// Capture range variable to scope within range\n\t\ttestCase := testCase\n\n\t\tt.Run(testCase.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tremoveOrphanedClusterAndAuthInfoConfigTestFunc(t, testCase.in, testCase.out)\n\t\t})\n\t}\n}\n\nfunc removeOrphanedClusterAndAuthInfoConfigTestFunc(t *testing.T, inputConfig string, expectedOutputConfig string) {\n\tpath := StoreConfigToTempFile(t, inputConfig)\n\tdefer os.Remove(path)\n\n\tconfig := LoadConfigFromPath(path)\n\trawConfig, err := config.RawConfig()\n\trequire.NoError(t, err)\n\tRemoveOrphanedClusterAndAuthInfoConfig(&rawConfig)\n\terr = clientcmd.ModifyConfig(config.ConfigAccess(), rawConfig, false)\n\trequire.NoError(t, err)\n\tdata, err := os.ReadFile(path)\n\trequire.NoError(t, err)\n\tstoredConfig := string(data)\n\tassert.Equal(t, expectedOutputConfig, storedConfig)\n}\n\n// Various example configs used in testing the config manipulation functions\n\nconst BASIC_CONFIG = `apiVersion: v1\nclusters:\n- cluster:\n    certificate-authority: /home/terratest/.minikube/ca.crt\n    server: https://172.17.0.48:8443\n  name: minikube\ncontexts:\n- context:\n    cluster: minikube\n    user: minikube\n  name: minikube\ncurrent-context: minikube\nkind: Config\nusers:\n- name: minikube\n  user:\n    client-certificate: /home/terratest/.minikube/client.crt\n    client-key: /home/terratest/.minikube/client.key\n`\n\nconst BASIC_CONFIG_WITH_EXTRA_CLUSTER = `apiVersion: v1\nclusters:\n- cluster:\n    certificate-authority: /home/terratest/.minikube/ca.crt\n    server: https://172.17.0.48:8443\n  name: minikube\n- cluster:\n    certificate-authority: /home/terratest/.minikube/extra_ca.crt\n    server: https://172.17.0.48:8443\n  name: extra_minikube\ncontexts:\n- context:\n    cluster: minikube\n    user: minikube\n  name: minikube\ncurrent-context: minikube\nkind: Config\npreferences: {}\nusers:\n- name: minikube\n  user:\n    client-certificate: /home/terratest/.minikube/client.crt\n    client-key: /home/terratest/.minikube/client.key\n`\n\nconst BASIC_CONFIG_WITH_EXTRA_AUTH_INFO = `apiVersion: v1\nclusters:\n- cluster:\n    certificate-authority: /home/terratest/.minikube/ca.crt\n    server: https://172.17.0.48:8443\n  name: minikube\ncontexts:\n- context:\n    cluster: minikube\n    user: minikube\n  name: minikube\ncurrent-context: minikube\nkind: Config\npreferences: {}\nusers:\n- name: minikube\n  user:\n    client-certificate: /home/terratest/.minikube/client.crt\n    client-key: /home/terratest/.minikube/client.key\n- name: extra_minikube\n  user:\n    client-certificate: /home/terratest/.minikube/extra_client.crt\n    client-key: /home/terratest/.minikube/extra_client.key\n`\n\nconst BASIC_CONFIG_WITH_EXTRA_CONTEXT = `apiVersion: v1\nclusters:\n- cluster:\n    certificate-authority: /home/terratest/.minikube/ca.crt\n    server: https://172.17.0.48:8443\n  name: minikube\n- cluster:\n    certificate-authority: /home/terratest/.minikube/extra_ca.crt\n    server: https://172.17.0.48:8443\n  name: extra_minikube\ncontexts:\n- context:\n    cluster: minikube\n    user: minikube\n  name: minikube\n- context:\n    cluster: extra_minikube\n    user: extra_minikube\n  name: extra_minikube\ncurrent-context: extra_minikube\nkind: Config\npreferences: {}\nusers:\n- name: minikube\n  user:\n    client-certificate: /home/terratest/.minikube/client.crt\n    client-key: /home/terratest/.minikube/client.key\n- name: extra_minikube\n  user:\n    client-certificate: /home/terratest/.minikube/extra_client.crt\n    client-key: /home/terratest/.minikube/extra_client.key\n`\n\nconst BASIC_CONFIG_WITH_EXTRA_CONTEXT_NO_GARBAGE = `apiVersion: v1\nclusters:\n- cluster:\n    certificate-authority: /home/terratest/.minikube/ca.crt\n    server: https://172.17.0.48:8443\n  name: minikube\n- cluster:\n    certificate-authority: /home/terratest/.minikube/extra_ca.crt\n    server: https://172.17.0.48:8443\n  name: extra_minikube\ncontexts:\n- context:\n    cluster: minikube\n    user: minikube\n  name: minikube\n- context:\n    cluster: extra_minikube\n    user: extra_minikube\n  name: extra_minikube\n- context:\n    cluster: extra_minikube\n    user: minikube\n  name: other_minikube\n\ncurrent-context: extra_minikube\nkind: Config\npreferences: {}\nusers:\n- name: minikube\n  user:\n    client-certificate: /home/terratest/.minikube/client.crt\n    client-key: /home/terratest/.minikube/client.key\n- name: extra_minikube\n  user:\n    client-certificate: /home/terratest/.minikube/extra_client.crt\n    client-key: /home/terratest/.minikube/extra_client.key\n`\n\nconst EXPECTED_CONFIG_AFTER_EXTRA_MINIKUBE_DELETED_NO_GARBAGE = `apiVersion: v1\nclusters:\n- cluster:\n    certificate-authority: /home/terratest/.minikube/extra_ca.crt\n    server: https://172.17.0.48:8443\n  name: extra_minikube\n- cluster:\n    certificate-authority: /home/terratest/.minikube/ca.crt\n    server: https://172.17.0.48:8443\n  name: minikube\ncontexts:\n- context:\n    cluster: minikube\n    user: minikube\n  name: minikube\n- context:\n    cluster: extra_minikube\n    user: minikube\n  name: other_minikube\ncurrent-context: minikube\nkind: Config\nusers:\n- name: minikube\n  user:\n    client-certificate: /home/terratest/.minikube/client.crt\n    client-key: /home/terratest/.minikube/client.key\n`\n"
  },
  {
    "path": "modules/k8s/configmap.go",
    "content": "package k8s\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"time\"\n\n\tcorev1 \"k8s.io/api/core/v1\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\n\t\"github.com/gruntwork-io/terratest/modules/retry\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// GetConfigMap returns a Kubernetes configmap resource in the provided namespace with the given name. The namespace used\n// is the one provided in the KubectlOptions. This will fail the test if there is an error.\nfunc GetConfigMap(t testing.TestingT, options *KubectlOptions, configMapName string) *corev1.ConfigMap {\n\tconfigMap, err := GetConfigMapE(t, options, configMapName)\n\trequire.NoError(t, err)\n\treturn configMap\n}\n\n// GetConfigMapE returns a Kubernetes configmap resource in the provided namespace with the given name. The namespace used\n// is the one provided in the KubectlOptions.\nfunc GetConfigMapE(t testing.TestingT, options *KubectlOptions, configMapName string) (*corev1.ConfigMap, error) {\n\tclientset, err := GetKubernetesClientFromOptionsE(t, options)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn clientset.CoreV1().ConfigMaps(options.Namespace).Get(context.Background(), configMapName, metav1.GetOptions{})\n}\n\n// WaitUntilConfigMapAvailable waits until the configmap is present on the cluster in cases where it is not immediately\n// available (for example, when using ClusterIssuer to request a certificate).\nfunc WaitUntilConfigMapAvailable(t testing.TestingT, options *KubectlOptions, configMapName string, retries int, sleepBetweenRetries time.Duration) {\n\tstatusMsg := fmt.Sprintf(\"Wait for configmap %s to be provisioned.\", configMapName)\n\tmessage := retry.DoWithRetry(\n\t\tt,\n\t\tstatusMsg,\n\t\tretries,\n\t\tsleepBetweenRetries,\n\t\tfunc() (string, error) {\n\t\t\t_, err := GetConfigMapE(t, options, configMapName)\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", err\n\t\t\t}\n\n\t\t\treturn \"configmap is now available\", nil\n\t\t},\n\t)\n\toptions.Logger.Logf(t, \"%s\", message)\n}\n"
  },
  {
    "path": "modules/k8s/configmap_test.go",
    "content": "//go:build kubeall || kubernetes\n// +build kubeall kubernetes\n\n// NOTE: we have build tags to differentiate kubernetes tests from non-kubernetes tests. This is done because minikube\n// is heavy and can interfere with docker related tests in terratest. Specifically, many of the tests start to fail with\n// `connection refused` errors from `minikube`. To avoid overloading the system, we run the kubernetes tests and helm\n// tests separately from the others. This may not be necessary if you have a sufficiently powerful machine.  We\n// recommend at least 4 cores and 16GB of RAM if you want to run all the tests together.\n\npackage k8s\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n)\n\nfunc TestGetConfigMapEReturnsErrorForNonExistantConfigMap(t *testing.T) {\n\tt.Parallel()\n\n\toptions := NewKubectlOptions(\"\", \"\", \"default\")\n\t_, err := GetConfigMapE(t, options, \"test-config-map\")\n\trequire.Error(t, err)\n}\n\nfunc TestGetConfigMapEReturnsCorrectConfigMapInCorrectNamespace(t *testing.T) {\n\tt.Parallel()\n\n\tuniqueID := strings.ToLower(random.UniqueId())\n\toptions := NewKubectlOptions(\"\", \"\", uniqueID)\n\tconfigData := fmt.Sprintf(EXAMPLE_CONFIGMAP_YAML_TEMPLATE, uniqueID, uniqueID)\n\tdefer KubectlDeleteFromString(t, options, configData)\n\tKubectlApplyFromString(t, options, configData)\n\n\tconfigMap := GetConfigMap(t, options, \"test-config-map\")\n\trequire.Equal(t, configMap.Name, \"test-config-map\")\n\trequire.Equal(t, configMap.Namespace, uniqueID)\n}\n\nfunc TestWaitUntilConfigMapAvailableReturnsSuccessfully(t *testing.T) {\n\tt.Parallel()\n\n\tuniqueID := strings.ToLower(random.UniqueId())\n\toptions := NewKubectlOptions(\"\", \"\", uniqueID)\n\tconfigData := fmt.Sprintf(EXAMPLE_CONFIGMAP_YAML_TEMPLATE, uniqueID, uniqueID)\n\tdefer KubectlDeleteFromString(t, options, configData)\n\n\tKubectlApplyFromString(t, options, configData)\n\tWaitUntilConfigMapAvailable(t, options, \"test-config-map\", 10, 1*time.Second)\n}\n\nconst EXAMPLE_CONFIGMAP_YAML_TEMPLATE = `---\napiVersion: v1\nkind: Namespace\nmetadata:\n  name: %s\n---\napiVersion: v1\nkind: ConfigMap\nmetadata:\n  name: test-config-map\n  namespace: %s\n`\n"
  },
  {
    "path": "modules/k8s/cronjob.go",
    "content": "package k8s\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/gruntwork-io/terratest/modules/retry\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/stretchr/testify/require\"\n\n\tbatchv1 \"k8s.io/api/batch/v1\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n)\n\n// ListCronJobs list cron jobs in namespace that match provided filters. This will fail the test if there is an error.\nfunc ListCronJobs(t testing.TestingT, options *KubectlOptions, filters metav1.ListOptions) []batchv1.CronJob {\n\tcronJobs, err := ListCronJobsE(t, options, filters)\n\trequire.NoError(t, err)\n\treturn cronJobs\n}\n\n// ListCronJobsE list cron jobs in namespace that match provided filters. This will return list or error.\nfunc ListCronJobsE(t testing.TestingT, options *KubectlOptions, filters metav1.ListOptions) ([]batchv1.CronJob, error) {\n\tclientset, err := GetKubernetesClientFromOptionsE(t, options)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tresp, err := clientset.BatchV1().CronJobs(options.Namespace).List(context.Background(), filters)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn resp.Items, nil\n}\n\n// GetCronJob return cron job resource from namespace by name. This will fail the test if there is an error.\nfunc GetCronJob(t testing.TestingT, options *KubectlOptions, cronJobName string) *batchv1.CronJob {\n\tjob, err := GetCronJobE(t, options, cronJobName)\n\trequire.NoError(t, err)\n\treturn job\n}\n\n// GetCronJobE return cron job resource from namespace by name. This will return cron job or error.\nfunc GetCronJobE(t testing.TestingT, options *KubectlOptions, cronJobName string) (*batchv1.CronJob, error) {\n\tclientset, err := GetKubernetesClientFromOptionsE(t, options)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn clientset.BatchV1().CronJobs(options.Namespace).Get(context.Background(), cronJobName, metav1.GetOptions{})\n}\n\n// WaitUntilCronJobSucceed waits until cron job will successfully complete a job. This will fail the test if there is an\n// error or if the check times out.\nfunc WaitUntilCronJobSucceed(t testing.TestingT, options *KubectlOptions, cronJobName string, retries int, sleepBetweenRetries time.Duration) {\n\trequire.NoError(t, WaitUntilCronJobSucceedE(t, options, cronJobName, retries, sleepBetweenRetries))\n}\n\n// WaitUntilCronJobSucceedE waits until cron job will successfully complete a job, retrying the check for the specified\n// amount of times, sleeping for the provided duration between each try.\nfunc WaitUntilCronJobSucceedE(t testing.TestingT, options *KubectlOptions, cronJobName string, retries int, sleepBetweenRetries time.Duration) error {\n\tstatusMsg := fmt.Sprintf(\"Wait for CronJob %s to successfully schedule container\", cronJobName)\n\tmessage, err := retry.DoWithRetryE(\n\t\tt,\n\t\tstatusMsg,\n\t\tretries,\n\t\tsleepBetweenRetries,\n\t\tfunc() (string, error) {\n\t\t\tjob, err := GetCronJobE(t, options, cronJobName)\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", err\n\t\t\t}\n\t\t\tif !IsCronJobSucceeded(job) {\n\t\t\t\treturn \"\", NewCronJobNotSucceeded(job)\n\t\t\t}\n\t\t\treturn \"CronJob scheduled container\", nil\n\t\t},\n\t)\n\tif err != nil {\n\t\toptions.Logger.Logf(t, \"Timed out waiting for CronJob to schedule job: %s\", err)\n\t\treturn err\n\t}\n\toptions.Logger.Logf(t, \"%s\", message)\n\treturn nil\n}\n\n// IsCronJobSucceeded returns true if cron job successfully scheduled and completed job.\n// https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/cron-job-v1/#CronJobStatus\nfunc IsCronJobSucceeded(cronJob *batchv1.CronJob) bool {\n\treturn cronJob.Status.LastScheduleTime != nil\n}\n"
  },
  {
    "path": "modules/k8s/cronjob_test.go",
    "content": "//go:build kubeall || kubernetes\n// +build kubeall kubernetes\n\npackage k8s\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\tbatchv1 \"k8s.io/api/batch/v1\"\n\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/stretchr/testify/require\"\n\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n)\n\nfunc TestListCronJobsReturnsCronJobsInNamespace(t *testing.T) {\n\tt.Parallel()\n\n\tuniqueID := strings.ToLower(random.UniqueId())\n\toptions := NewKubectlOptions(\"\", \"\", uniqueID)\n\tconfigData := fmt.Sprintf(ExampleCronjobYamlTemplate, uniqueID, uniqueID)\n\tdefer KubectlDeleteFromString(t, options, configData)\n\tKubectlApplyFromString(t, options, configData)\n\n\tjobs := ListCronJobs(t, options, metav1.ListOptions{})\n\trequire.Equal(t, len(jobs), 1)\n\tjob := jobs[0]\n\trequire.Equal(t, job.Name, \"cron-job\")\n\trequire.Equal(t, job.Namespace, uniqueID)\n}\n\nfunc TestGetCronJobEReturnErrorForNotExistingCronJob(t *testing.T) {\n\tt.Parallel()\n\n\toptions := NewKubectlOptions(\"\", \"\", \"default\")\n\t_, err := GetJobE(t, options, random.UniqueId())\n\trequire.Error(t, err)\n}\n\nfunc TestGetCronJobEReturnsCorrectJobInNamespace(t *testing.T) {\n\tt.Parallel()\n\tuniqueID := strings.ToLower(random.UniqueId())\n\toptions := NewKubectlOptions(\"\", \"\", uniqueID)\n\tconfigData := fmt.Sprintf(ExampleCronjobYamlTemplate, uniqueID, uniqueID)\n\tdefer KubectlDeleteFromString(t, options, configData)\n\tKubectlApplyFromString(t, options, configData)\n\n\tjob := GetCronJob(t, options, \"cron-job\")\n\trequire.Equal(t, job.Name, \"cron-job\")\n\trequire.Equal(t, job.Namespace, uniqueID)\n}\n\nfunc TestWaitUntilCronJobScheduleSuccessfullyContainer(t *testing.T) {\n\tt.Parallel()\n\n\tuniqueID := strings.ToLower(random.UniqueId())\n\toptions := NewKubectlOptions(\"\", \"\", uniqueID)\n\tconfigData := fmt.Sprintf(ExampleCronjobYamlTemplate, uniqueID, uniqueID)\n\tdefer KubectlDeleteFromString(t, options, configData)\n\tKubectlApplyFromString(t, options, configData)\n\n\tWaitUntilCronJobSucceed(t, options, \"cron-job\", 60, 5*time.Second)\n}\n\nfunc TestIsCronJobSucceeded(t *testing.T) {\n\n\tcases := []struct {\n\t\ttitle          string\n\t\tcronJob        *batchv1.CronJob\n\t\texpectedResult bool\n\t}{\n\t\t{\n\t\t\ttitle: \"CronJobScheduledContainer\",\n\t\t\tcronJob: &batchv1.CronJob{\n\t\t\t\tStatus: batchv1.CronJobStatus{\n\t\t\t\t\tLastScheduleTime: &metav1.Time{},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedResult: true,\n\t\t},\n\t\t{\n\t\t\ttitle: \"CronJobNotScheduledContainer\",\n\t\t\tcronJob: &batchv1.CronJob{\n\t\t\t\tStatus: batchv1.CronJobStatus{\n\t\t\t\t\tLastScheduleTime: nil,\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedResult: false,\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\ttc := tc\n\t\tt.Run(tc.title, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tactualResult := IsCronJobSucceeded(tc.cronJob)\n\t\t\trequire.Equal(t, tc.expectedResult, actualResult)\n\t\t})\n\t}\n}\n\nconst ExampleCronjobYamlTemplate = `---\napiVersion: v1\nkind: Namespace\nmetadata:\n  name: %s\n---\napiVersion: batch/v1\nkind: CronJob\nmetadata:\n  name: cron-job\n  namespace: %s\nspec:\n  schedule: \"* * * * *\"\n  jobTemplate:\n    spec:\n      template:\n        spec:\n          containers:\n          - name: ubuntu\n            image: ubuntu:20.04\n            command: [\"sh\", \"-c\", \"ls\"]\n          restartPolicy: OnFailure\n`\n"
  },
  {
    "path": "modules/k8s/daemonset.go",
    "content": "package k8s\n\nimport (\n\t\"context\"\n\n\t\"github.com/stretchr/testify/require\"\n\tappsv1 \"k8s.io/api/apps/v1\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n)\n\n// ListDaemonSets will look for daemonsets in the given namespace that match the given filters and return them. This will\n// fail the test if there is an error.\nfunc ListDaemonSets(t testing.TestingT, options *KubectlOptions, filters metav1.ListOptions) []appsv1.DaemonSet {\n\tdaemonset, err := ListDaemonSetsE(t, options, filters)\n\trequire.NoError(t, err)\n\treturn daemonset\n}\n\n// ListDaemonSetsE will look for daemonsets in the given namespace that match the given filters and return them.\nfunc ListDaemonSetsE(t testing.TestingT, options *KubectlOptions, filters metav1.ListOptions) ([]appsv1.DaemonSet, error) {\n\tclientset, err := GetKubernetesClientFromOptionsE(t, options)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tresp, err := clientset.AppsV1().DaemonSets(options.Namespace).List(context.Background(), filters)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn resp.Items, nil\n}\n\n// GetDaemonSet returns a Kubernetes daemonset resource in the provided namespace with the given name. This will\n// fail the test if there is an error.\nfunc GetDaemonSet(t testing.TestingT, options *KubectlOptions, daemonSetName string) *appsv1.DaemonSet {\n\tdaemonset, err := GetDaemonSetE(t, options, daemonSetName)\n\trequire.NoError(t, err)\n\treturn daemonset\n}\n\n// GetDaemonSetE returns a Kubernetes daemonset resource in the provided namespace with the given name.\nfunc GetDaemonSetE(t testing.TestingT, options *KubectlOptions, daemonSetName string) (*appsv1.DaemonSet, error) {\n\tclientset, err := GetKubernetesClientFromOptionsE(t, options)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn clientset.AppsV1().DaemonSets(options.Namespace).Get(context.Background(), daemonSetName, metav1.GetOptions{})\n}\n"
  },
  {
    "path": "modules/k8s/daemonset_test.go",
    "content": "//go:build kubernetes\n// +build kubernetes\n\n// NOTE: we have build tags to differentiate kubernetes tests from non-kubernetes tests. This is done because minikube\n// is heavy and can interfere with docker related tests in terratest. Specifically, many of the tests start to fail with\n// `connection refused` errors from `minikube`. To avoid overloading the system, we run the kubernetes tests and helm\n// tests separately from the others. This may not be necessary if you have a sufficiently powerful machine.  We\n// recommend at least 4 cores and 16GB of RAM if you want to run all the tests together.\npackage k8s\n\nimport (\n\t\"fmt\"\n\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestGetDaemonSetEReturnsErrorForNonExistantDaemonSet(t *testing.T) {\n\tt.Parallel()\n\n\toptions := NewKubectlOptions(\"\", \"\", \"\")\n\t_, err := GetDaemonSetE(t, options, \"sample-ds\")\n\trequire.Error(t, err)\n}\n\nfunc TestGetDaemonSetEReturnsCorrectServiceInCorrectNamespace(t *testing.T) {\n\tt.Parallel()\n\n\tuniqueID := strings.ToLower(random.UniqueId())\n\toptions := NewKubectlOptions(\"\", \"\", uniqueID)\n\tconfigData := fmt.Sprintf(EXAMPLE_DAEMONSET_YAML_TEMPLATE, uniqueID, uniqueID)\n\tKubectlApplyFromString(t, options, configData)\n\tdefer KubectlDeleteFromString(t, options, configData)\n\n\tdaemonSet := GetDaemonSet(t, options, \"sample-ds\")\n\trequire.Equal(t, daemonSet.Name, \"sample-ds\")\n\trequire.Equal(t, daemonSet.Namespace, uniqueID)\n}\n\nfunc TestListDaemonSetsReturnsCorrectServiceInCorrectNamespace(t *testing.T) {\n\tt.Parallel()\n\n\tuniqueID := strings.ToLower(random.UniqueId())\n\toptions := NewKubectlOptions(\"\", \"\", uniqueID)\n\tconfigData := fmt.Sprintf(EXAMPLE_DAEMONSET_YAML_TEMPLATE, uniqueID, uniqueID)\n\tKubectlApplyFromString(t, options, configData)\n\tdefer KubectlDeleteFromString(t, options, configData)\n\n\tdaemonSets := ListDaemonSets(t, options, metav1.ListOptions{})\n\trequire.Equal(t, len(daemonSets), 1)\n\n\tdaemonSet := daemonSets[0]\n\trequire.Equal(t, daemonSet.Name, \"sample-ds\")\n\trequire.Equal(t, daemonSet.Namespace, uniqueID)\n}\n\nconst EXAMPLE_DAEMONSET_YAML_TEMPLATE = `---\napiVersion: v1\nkind: Namespace\nmetadata:\n  name: %s\n---\napiVersion: apps/v1\nkind: DaemonSet\nmetadata:\n  name: sample-ds\n  namespace: %s\n  labels:\n    k8s-app: sample-ds\nspec:\n  selector:\n    matchLabels:\n      name: sample-ds\n  template:\n    metadata:\n      labels:\n        name: sample-ds\n    spec:\n      tolerations:\n      - key: node-role.kubernetes.io/master\n        effect: NoSchedule\n      containers:\n      - name: alpine\n        image: alpine:3.8\n        command: ['sh', '-c', 'echo Hello Terratest! && sleep 99999']\n`\n"
  },
  {
    "path": "modules/k8s/deployment.go",
    "content": "package k8s\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/require\"\n\tappsv1 \"k8s.io/api/apps/v1\"\n\tv1 \"k8s.io/api/core/v1\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\n\t\"github.com/gruntwork-io/terratest/modules/retry\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n)\n\n// ListDeployments will look for deployments in the given namespace that match the given filters and return them. This will\n// fail the test if there is an error.\nfunc ListDeployments(t testing.TestingT, options *KubectlOptions, filters metav1.ListOptions) []appsv1.Deployment {\n\tdeployment, err := ListDeploymentsE(t, options, filters)\n\trequire.NoError(t, err)\n\treturn deployment\n}\n\n// ListDeploymentsE will look for deployments in the given namespace that match the given filters and return them.\nfunc ListDeploymentsE(t testing.TestingT, options *KubectlOptions, filters metav1.ListOptions) ([]appsv1.Deployment, error) {\n\tclientset, err := GetKubernetesClientFromOptionsE(t, options)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdeployments, err := clientset.AppsV1().Deployments(options.Namespace).List(context.Background(), filters)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn deployments.Items, nil\n}\n\n// GetDeployment returns a Kubernetes deployment resource in the provided namespace with the given name. This will\n// fail the test if there is an error.\nfunc GetDeployment(t testing.TestingT, options *KubectlOptions, deploymentName string) *appsv1.Deployment {\n\tdeployment, err := GetDeploymentE(t, options, deploymentName)\n\trequire.NoError(t, err)\n\treturn deployment\n}\n\n// GetDeploymentE returns a Kubernetes deployment resource in the provided namespace with the given name.\nfunc GetDeploymentE(t testing.TestingT, options *KubectlOptions, deploymentName string) (*appsv1.Deployment, error) {\n\tclientset, err := GetKubernetesClientFromOptionsE(t, options)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn clientset.AppsV1().Deployments(options.Namespace).Get(context.Background(), deploymentName, metav1.GetOptions{})\n}\n\n// WaitUntilDeploymentAvailableE waits until all pods within the deployment are ready and started,\n// retrying the check for the specified amount of times, sleeping\n// for the provided duration between each try.\n// This will fail the test if there is an error.\nfunc WaitUntilDeploymentAvailable(t testing.TestingT, options *KubectlOptions, deploymentName string, retries int, sleepBetweenRetries time.Duration) {\n\trequire.NoError(t, WaitUntilDeploymentAvailableE(t, options, deploymentName, retries, sleepBetweenRetries))\n}\n\n// WaitUntilDeploymentAvailableE waits until all pods within the deployment are ready and started,\n// retrying the check for the specified amount of times, sleeping\n// for the provided duration between each try.\nfunc WaitUntilDeploymentAvailableE(\n\tt testing.TestingT,\n\toptions *KubectlOptions,\n\tdeploymentName string,\n\tretries int,\n\tsleepBetweenRetries time.Duration,\n) error {\n\tstatusMsg := fmt.Sprintf(\"Wait for deployment %s to be provisioned.\", deploymentName)\n\tmessage, err := retry.DoWithRetryE(\n\t\tt,\n\t\tstatusMsg,\n\t\tretries,\n\t\tsleepBetweenRetries,\n\t\tfunc() (string, error) {\n\t\t\tdeployment, err := GetDeploymentE(t, options, deploymentName)\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", err\n\t\t\t}\n\t\t\tif !IsDeploymentAvailable(deployment) {\n\t\t\t\treturn \"\", NewDeploymentNotAvailableError(deployment)\n\t\t\t}\n\t\t\treturn \"Deployment is now available\", nil\n\t\t},\n\t)\n\tif err != nil {\n\t\toptions.Logger.Logf(t, \"Timedout waiting for Deployment to be provisioned: %s\", err)\n\t\treturn err\n\t}\n\toptions.Logger.Logf(t, \"%s\", message)\n\treturn nil\n}\n\n// IsDeploymentAvailable returns true if all pods within the deployment are ready and started\nfunc IsDeploymentAvailable(deploy *appsv1.Deployment) bool {\n\tdc := getDeploymentCondition(deploy, appsv1.DeploymentProgressing)\n\treturn dc != nil && dc.Status == v1.ConditionTrue && dc.Reason == \"NewReplicaSetAvailable\"\n}\n\nfunc getDeploymentCondition(deploy *appsv1.Deployment, cType appsv1.DeploymentConditionType) *appsv1.DeploymentCondition {\n\tfor idx := range deploy.Status.Conditions {\n\t\tdc := &deploy.Status.Conditions[idx]\n\t\tif dc.Type == cType {\n\t\t\treturn dc\n\t\t}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "modules/k8s/deployment_test.go",
    "content": "//go:build kubeall || kubernetes\n// +build kubeall kubernetes\n\n// NOTE: we have build tags to differentiate kubernetes tests from non-kubernetes tests. This is done because minikube\n// is heavy and can interfere with docker related tests in terratest. Specifically, many of the tests start to fail with\n// `connection refused` errors from `minikube`. To avoid overloading the system, we run the kubernetes tests and helm\n// tests separately from the others. This may not be necessary if you have a sufficiently powerful machine.  We\n// recommend at least 4 cores and 16GB of RAM if you want to run all the tests together.\n\npackage k8s\n\nimport (\n\t\"fmt\"\n\t\"time\"\n\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/stretchr/testify/require\"\n\tappsv1 \"k8s.io/api/apps/v1\"\n\tv1 \"k8s.io/api/core/v1\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n)\n\nfunc TestGetDeploymentEReturnsError(t *testing.T) {\n\tt.Parallel()\n\n\toptions := NewKubectlOptions(\"\", \"\", \"\")\n\t_, err := GetDeploymentE(t, options, \"nginx-deployment\")\n\trequire.Error(t, err)\n}\n\nfunc TestGetDeployments(t *testing.T) {\n\tt.Parallel()\n\n\tuniqueID := strings.ToLower(random.UniqueId())\n\toptions := NewKubectlOptions(\"\", \"\", uniqueID)\n\tconfigData := fmt.Sprintf(ExampleDeploymentYAMLTemplate, uniqueID)\n\tKubectlApplyFromString(t, options, configData)\n\tdefer KubectlDeleteFromString(t, options, configData)\n\n\tdeployment := GetDeployment(t, options, \"nginx-deployment\")\n\trequire.Equal(t, deployment.Name, \"nginx-deployment\")\n\trequire.Equal(t, deployment.Namespace, uniqueID)\n}\n\nfunc TestListDeployments(t *testing.T) {\n\tt.Parallel()\n\n\tuniqueID := strings.ToLower(random.UniqueId())\n\toptions := NewKubectlOptions(\"\", \"\", uniqueID)\n\tconfigData := fmt.Sprintf(ExampleDeploymentYAMLTemplate, uniqueID)\n\tKubectlApplyFromString(t, options, configData)\n\tdefer KubectlDeleteFromString(t, options, configData)\n\n\tdeployments := ListDeployments(t, options, metav1.ListOptions{})\n\trequire.Equal(t, len(deployments), 1)\n\n\tdeployment := deployments[0]\n\trequire.Equal(t, deployment.Name, \"nginx-deployment\")\n\trequire.Equal(t, deployment.Namespace, uniqueID)\n}\n\nfunc TestWaitUntilDeploymentAvailable(t *testing.T) {\n\tt.Parallel()\n\n\tuniqueID := strings.ToLower(random.UniqueId())\n\toptions := NewKubectlOptions(\"\", \"\", uniqueID)\n\tconfigData := fmt.Sprintf(ExampleDeploymentYAMLTemplate, uniqueID)\n\tKubectlApplyFromString(t, options, configData)\n\tdefer KubectlDeleteFromString(t, options, configData)\n\n\tWaitUntilDeploymentAvailable(t, options, \"nginx-deployment\", 60, 1*time.Second)\n}\n\nfunc TestTestIsDeploymentAvailable(t *testing.T) {\n\ttestCases := []struct {\n\t\ttitle          string\n\t\tdeploy         *appsv1.Deployment\n\t\texpectedResult bool\n\t}{\n\t\t{\n\t\t\ttitle: \"TestIsDeploymentAvailableWithProgressingNewReplicaSetAvailable\",\n\t\t\tdeploy: &appsv1.Deployment{\n\t\t\t\tStatus: appsv1.DeploymentStatus{\n\t\t\t\t\tConditions: []appsv1.DeploymentCondition{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tType:   appsv1.DeploymentProgressing,\n\t\t\t\t\t\t\tStatus: v1.ConditionTrue,\n\t\t\t\t\t\t\tReason: \"NewReplicaSetAvailable\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedResult: true,\n\t\t},\n\t\t{\n\t\t\ttitle: \"TestIsDeploymentAvailableWithoutProgressingNewReplicaSetAvailable\",\n\t\t\tdeploy: &appsv1.Deployment{\n\t\t\t\tStatus: appsv1.DeploymentStatus{\n\t\t\t\t\tConditions: []appsv1.DeploymentCondition{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tType:   appsv1.DeploymentProgressing,\n\t\t\t\t\t\t\tStatus: v1.ConditionTrue,\n\t\t\t\t\t\t\tReason: \"ReplicaSetUpdated\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedResult: false,\n\t\t},\n\t\t{\n\t\t\ttitle: \"TestIsDeploymentAvailableWithoutProgressingCondition\",\n\t\t\tdeploy: &appsv1.Deployment{\n\t\t\t\tStatus: appsv1.DeploymentStatus{\n\t\t\t\t\tConditions: []appsv1.DeploymentCondition{},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedResult: false,\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\ttc := tc\n\t\tt.Run(tc.title, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tactualResult := IsDeploymentAvailable(tc.deploy)\n\t\t\trequire.Equal(t, tc.expectedResult, actualResult)\n\t\t})\n\t}\n}\n\nconst ExampleDeploymentYAMLTemplate = `---\napiVersion: v1\nkind: Namespace\nmetadata:\n  name: %s\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: nginx-deployment\n  labels:\n    app: nginx\nspec:\n  strategy:\n    rollingUpdate:\n      maxSurge: 10%%\n      maxUnavailable: 0\n  replicas: 2\n  selector:\n    matchLabels:\n      app: nginx\n  template:\n    metadata:\n      labels:\n        app: nginx\n    spec:\n      containers:\n      - name: nginx\n        image: nginx:1.15.7\n        ports:\n        - containerPort: 80\n        readinessProbe:\n          httpGet:\n            path: /\n            port: 80\n`\n"
  },
  {
    "path": "modules/k8s/errors.go",
    "content": "package k8s\n\nimport (\n\t\"fmt\"\n\n\tappsv1 \"k8s.io/api/apps/v1\"\n\tbatchv1 \"k8s.io/api/batch/v1\"\n\tcorev1 \"k8s.io/api/core/v1\"\n\tnetworkingv1 \"k8s.io/api/networking/v1\"\n\tnetworkingv1beta1 \"k8s.io/api/networking/v1beta1\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n)\n\n// IngressNotAvailable is returned when a Kubernetes service is not yet available to accept traffic.\ntype IngressNotAvailable struct {\n\tingress *networkingv1.Ingress\n}\n\n// Error is a simple function to return a formatted error message as a string\nfunc (err IngressNotAvailable) Error() string {\n\treturn fmt.Sprintf(\"Ingress %s is not available\", err.ingress.Name)\n}\n\n// IngressNotAvailableV1Beta1 is returned when a Kubernetes service is not yet available to accept traffic.\ntype IngressNotAvailableV1Beta1 struct {\n\tingress *networkingv1beta1.Ingress\n}\n\n// Error is a simple function to return a formatted error message as a string\nfunc (err IngressNotAvailableV1Beta1) Error() string {\n\treturn fmt.Sprintf(\"Ingress %s is not available\", err.ingress.Name)\n}\n\n// UnknownKubeResourceType is returned if the given resource type does not match the list of known resource types.\ntype UnknownKubeResourceType struct {\n\tResourceType KubeResourceType\n}\n\nfunc (err UnknownKubeResourceType) Error() string {\n\treturn fmt.Sprintf(\"ResourceType ID %d is unknown\", err.ResourceType)\n}\n\n// DesiredNumberOfPodsNotCreated is returned when the number of pods matching a filter condition does not match the\n// desired number of Pods.\ntype DesiredNumberOfPodsNotCreated struct {\n\tFilter       metav1.ListOptions\n\tDesiredCount int\n}\n\n// Error is a simple function to return a formatted error message as a string\nfunc (err DesiredNumberOfPodsNotCreated) Error() string {\n\treturn fmt.Sprintf(\"Desired number of pods (%d) matching filter %v not yet created\", err.DesiredCount, err.Filter)\n}\n\n// ServiceAccountTokenNotAvailable is returned when a Kubernetes ServiceAccount does not have a token provisioned yet.\ntype ServiceAccountTokenNotAvailable struct {\n\tName string\n}\n\n// Error is a simple function to return a formatted error message as a string\nfunc (err ServiceAccountTokenNotAvailable) Error() string {\n\treturn fmt.Sprintf(\"ServiceAccount %s does not have a token yet.\", err.Name)\n}\n\n// DeploymentNotAvailable is returned when a Kubernetes deployment is not yet available to accept traffic.\ntype DeploymentNotAvailable struct {\n\tdeploy *appsv1.Deployment\n}\n\n// Error is a simple function to return a formatted error message as a string\nfunc (err DeploymentNotAvailable) Error() string {\n\tdc := getDeploymentCondition(err.deploy, appsv1.DeploymentProgressing)\n\tif dc == nil {\n\t\treturn fmt.Sprintf(\n\t\t\t\"Deployment %s is not available, missing '%s' condition\",\n\t\t\terr.deploy.Name,\n\t\t\tappsv1.DeploymentProgressing,\n\t\t)\n\t}\n\treturn fmt.Sprintf(\n\t\t\"Deployment %s is not available as '%s' condition indicates that the Deployment is not complete, status: %v, reason: %s, message: %s\",\n\t\terr.deploy.Name,\n\t\tappsv1.DeploymentProgressing,\n\t\tdc.Status,\n\t\tdc.Reason,\n\t\tdc.Message,\n\t)\n}\n\n// NewDeploymentNotAvailableError returns a DeploymentNotAvailable struct when Kubernetes deems a deployment is not available\nfunc NewDeploymentNotAvailableError(deploy *appsv1.Deployment) DeploymentNotAvailable {\n\treturn DeploymentNotAvailable{deploy}\n}\n\n// PodNotAvailable is returned when a Kubernetes service is not yet available to accept traffic.\ntype PodNotAvailable struct {\n\tpod *corev1.Pod\n}\n\n// Error is a simple function to return a formatted error message as a string\nfunc (err PodNotAvailable) Error() string {\n\treturn fmt.Sprintf(\"Pod %s is not available, reason: %s, message: %s\", err.pod.Name, err.pod.Status.Reason, err.pod.Status.Message)\n}\n\n// NewPodNotAvailableError returns a PodNotAvailable struct when Kubernetes deems a pod is not available\nfunc NewPodNotAvailableError(pod *corev1.Pod) PodNotAvailable {\n\treturn PodNotAvailable{pod}\n}\n\n// JobNotSucceeded is returned when a Kubernetes job is not Succeeded\ntype JobNotSucceeded struct {\n\tjob *batchv1.Job\n}\n\n// Error is a simple function to return a formatted error message as a string\nfunc (err JobNotSucceeded) Error() string {\n\treturn fmt.Sprintf(\"Job %s is not Succeeded\", err.job.Name)\n}\n\n// NewJobNotSucceeded returns a JobNotSucceeded when the status of the job is not Succeeded\nfunc NewJobNotSucceeded(job *batchv1.Job) JobNotSucceeded {\n\treturn JobNotSucceeded{job}\n}\n\n// ServiceNotAvailable is returned when a Kubernetes service is not yet available to accept traffic.\ntype ServiceNotAvailable struct {\n\tservice *corev1.Service\n}\n\n// Error is a simple function to return a formatted error message as a string\nfunc (err ServiceNotAvailable) Error() string {\n\treturn fmt.Sprintf(\"Service %s is not available\", err.service.Name)\n}\n\n// NewServiceNotAvailableError returns a ServiceNotAvailable struct when Kubernetes deems a service is not available\nfunc NewServiceNotAvailableError(service *corev1.Service) ServiceNotAvailable {\n\treturn ServiceNotAvailable{service}\n}\n\n// UnknownServiceType is returned when a Kubernetes service has a type that is not yet handled by the test functions.\ntype UnknownServiceType struct {\n\tservice *corev1.Service\n}\n\n// Error is a simple function to return a formatted error message as a string\nfunc (err UnknownServiceType) Error() string {\n\treturn fmt.Sprintf(\"Service %s has an unknown service type\", err.service.Name)\n}\n\n// NewUnknownServiceTypeError returns an UnknownServiceType struct when is it deemed that Kubernetes does not know the service type provided\nfunc NewUnknownServiceTypeError(service *corev1.Service) UnknownServiceType {\n\treturn UnknownServiceType{service}\n}\n\n// UnknownServicePort is returned when the given service port is not an exported port of the service.\ntype UnknownServicePort struct {\n\tservice *corev1.Service\n\tport    int32\n}\n\n// Error is a simple function to return a formatted error message as a string\nfunc (err UnknownServicePort) Error() string {\n\treturn fmt.Sprintf(\"Port %d is not a part of the service %s\", err.port, err.service.Name)\n}\n\n// NewUnknownServicePortError returns an UnknownServicePort struct when it is deemed that Kubernetes does not know of the provided Service Port\nfunc NewUnknownServicePortError(service *corev1.Service, port int32) UnknownServicePort {\n\treturn UnknownServicePort{service, port}\n}\n\n// PersistentVolumeNotInStatus is returned when a Kubernetes PersistentVolume is not in the expected status phase\ntype PersistentVolumeNotInStatus struct {\n\tpv            *corev1.PersistentVolume\n\tpvStatusPhase *corev1.PersistentVolumePhase\n}\n\n// Error is a simple function to return a formatted error message as a string\nfunc (err PersistentVolumeNotInStatus) Error() string {\n\treturn fmt.Sprintf(\"Pv %s is not '%s'\", err.pv.Name, *err.pvStatusPhase)\n}\n\n// NewPersistentVolumeNotInStatusError returns a PersistentVolumeNotInStatus struct when the given Persistent Volume is not in the expected status phase\nfunc NewPersistentVolumeNotInStatusError(pv *corev1.PersistentVolume, pvStatusPhase *corev1.PersistentVolumePhase) PersistentVolumeNotInStatus {\n\treturn PersistentVolumeNotInStatus{pv, pvStatusPhase}\n}\n\n// PersistentVolumeClaimNotInStatus is returned when a Kubernetes PersistentVolumeClaim is not in the expected status phase\ntype PersistentVolumeClaimNotInStatus struct {\n\tpvc            *corev1.PersistentVolumeClaim\n\tpvcStatusPhase *corev1.PersistentVolumeClaimPhase\n}\n\n// Error is a simple function to return a formatted error message as a string\nfunc (err PersistentVolumeClaimNotInStatus) Error() string {\n\treturn fmt.Sprintf(\"PVC %s is not '%s'\", err.pvc.Name, *err.pvcStatusPhase)\n}\n\n// NewPersistentVolumeClaimNotInStatusError returns a PersistentVolumeClaimNotInStatus struct when the given PersistentVolumeClaim is not in the expected status phase\nfunc NewPersistentVolumeClaimNotInStatusError(pvc *corev1.PersistentVolumeClaim, pvcStatusPhase *corev1.PersistentVolumeClaimPhase) PersistentVolumeClaimNotInStatus {\n\treturn PersistentVolumeClaimNotInStatus{pvc, pvcStatusPhase}\n}\n\n// NoNodesInKubernetes is returned when the Kubernetes cluster has no nodes registered.\ntype NoNodesInKubernetes struct{}\n\n// Error is a simple function to return a formatted error message as a string\nfunc (err NoNodesInKubernetes) Error() string {\n\treturn \"There are no nodes in the Kubernetes cluster\"\n}\n\n// NewNoNodesInKubernetesError returns a NoNodesInKubernetes struct when it is deemed that there are no Kubernetes nodes registered\nfunc NewNoNodesInKubernetesError() NoNodesInKubernetes {\n\treturn NoNodesInKubernetes{}\n}\n\n// NodeHasNoHostname is returned when a Kubernetes node has no discernible hostname\ntype NodeHasNoHostname struct {\n\tnode *corev1.Node\n}\n\n// Error is a simple function to return a formatted error message as a string\nfunc (err NodeHasNoHostname) Error() string {\n\treturn fmt.Sprintf(\"Node %s has no hostname\", err.node.Name)\n}\n\n// NewNodeHasNoHostnameError returns a NodeHasNoHostname struct when it is deemed that the provided node has no hostname\nfunc NewNodeHasNoHostnameError(node *corev1.Node) NodeHasNoHostname {\n\treturn NodeHasNoHostname{node}\n}\n\n// MalformedNodeID is returned when a Kubernetes node has a malformed node id scheme\ntype MalformedNodeID struct {\n\tnode *corev1.Node\n}\n\n// Error is a simple function to return a formatted error message as a string\nfunc (err MalformedNodeID) Error() string {\n\treturn fmt.Sprintf(\"Node %s has malformed ID %s\", err.node.Name, err.node.Spec.ProviderID)\n}\n\n// NewMalformedNodeIDError returns a MalformedNodeID struct when Kubernetes deems that a NodeID is malformed\nfunc NewMalformedNodeIDError(node *corev1.Node) MalformedNodeID {\n\treturn MalformedNodeID{node}\n}\n\n// JSONPathMalformedJSONErr is returned when the jsonpath unmarshal routine fails to parse the given JSON blob.\ntype JSONPathMalformedJSONErr struct {\n\tunderlyingErr error\n}\n\nfunc (err JSONPathMalformedJSONErr) Error() string {\n\treturn fmt.Sprintf(\"Error unmarshaling original json blob: %s\", err.underlyingErr)\n}\n\n// JSONPathMalformedJSONPathErr is returned when the jsonpath unmarshal routine fails to parse the given JSON path\n// string.\ntype JSONPathMalformedJSONPathErr struct {\n\tunderlyingErr error\n}\n\nfunc (err JSONPathMalformedJSONPathErr) Error() string {\n\treturn fmt.Sprintf(\"Error parsing json path: %s\", err.underlyingErr)\n}\n\n// JSONPathExtractJSONPathErr is returned when the jsonpath unmarshal routine fails to extract the given JSON path from\n// the JSON blob.\ntype JSONPathExtractJSONPathErr struct {\n\tunderlyingErr error\n}\n\nfunc (err JSONPathExtractJSONPathErr) Error() string {\n\treturn fmt.Sprintf(\"Error extracting json path from blob: %s\", err.underlyingErr)\n}\n\n// JSONPathMalformedJSONPathResultErr is returned when the jsonpath unmarshal routine fails to unmarshal the resulting\n// data from extraction.\ntype JSONPathMalformedJSONPathResultErr struct {\n\tunderlyingErr error\n}\n\nfunc (err JSONPathMalformedJSONPathResultErr) Error() string {\n\treturn fmt.Sprintf(\"Error unmarshaling json path output: %s\", err.underlyingErr)\n}\n\n// CronJobNotSucceeded is returned when a Kubernetes cron job didn't successfully schedule a job.\ntype CronJobNotSucceeded struct {\n\tcronJob *batchv1.CronJob\n}\n\n// Error format message for cron job error.\nfunc (err CronJobNotSucceeded) Error() string {\n\treturn fmt.Sprintf(\"CronJob %s failed to be scheduled.\", err.cronJob.Name)\n}\n\n// NewCronJobNotSucceeded create error for case when CronJob didn't schedule a job.\nfunc NewCronJobNotSucceeded(cronJob *batchv1.CronJob) CronJobNotSucceeded {\n\treturn CronJobNotSucceeded{cronJob}\n}\n"
  },
  {
    "path": "modules/k8s/errors_test.go",
    "content": "package k8s\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\n\tappsv1 \"k8s.io/api/apps/v1\"\n\tv1 \"k8s.io/api/core/v1\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n)\n\nfunc TestErrorDeploymentNotAvailable(t *testing.T) {\n\ttestCases := []struct {\n\t\ttitle       string\n\t\tdeploy      *appsv1.Deployment\n\t\texpectedErr string\n\t}{\n\t\t{\n\t\t\ttitle: \"NoProgressingCondition\",\n\t\t\tdeploy: &appsv1.Deployment{\n\t\t\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\t\t\tName: \"foo\",\n\t\t\t\t},\n\t\t\t\tStatus: appsv1.DeploymentStatus{\n\t\t\t\t\tConditions: []appsv1.DeploymentCondition{},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedErr: \"Deployment foo is not available, missing 'Progressing' condition\",\n\t\t},\n\t\t{\n\t\t\ttitle: \"DeploymentNotComplete\",\n\t\t\tdeploy: &appsv1.Deployment{\n\t\t\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\t\t\tName: \"foo\",\n\t\t\t\t},\n\t\t\t\tStatus: appsv1.DeploymentStatus{\n\t\t\t\t\tConditions: []appsv1.DeploymentCondition{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tType:    appsv1.DeploymentProgressing,\n\t\t\t\t\t\t\tStatus:  v1.ConditionTrue,\n\t\t\t\t\t\t\tReason:  \"ReplicaSetUpdated\",\n\t\t\t\t\t\t\tMessage: \"bar\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedErr: \"Deployment foo is not available as 'Progressing' condition indicates that the Deployment is not complete, status: True, reason: ReplicaSetUpdated, message: bar\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\ttc := tc\n\t\tt.Run(tc.title, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\terr := NewDeploymentNotAvailableError(tc.deploy)\n\t\t\tassert.EqualError(t, err, tc.expectedErr)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "modules/k8s/event.go",
    "content": "package k8s\n\nimport (\n\t\"context\"\n\n\t\"github.com/stretchr/testify/require\"\n\n\tcorev1 \"k8s.io/api/core/v1\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n)\n\n// ListEvents will retrieve the Events in the given namespace that match the given filters and return them. This will fail the\n// test if there is an error.\nfunc ListEvents(t testing.TestingT, options *KubectlOptions, filters metav1.ListOptions) []corev1.Event {\n\tevents, err := ListEventsE(t, options, filters)\n\trequire.NoError(t, err)\n\treturn events\n}\n\n// ListEventsE will retrieve the Events that match the given filters and return them.\nfunc ListEventsE(t testing.TestingT, options *KubectlOptions, filters metav1.ListOptions) ([]corev1.Event, error) {\n\tclientset, err := GetKubernetesClientFromOptionsE(t, options)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresp, err := clientset.CoreV1().Events(options.Namespace).List(context.Background(), filters)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn resp.Items, nil\n}\n"
  },
  {
    "path": "modules/k8s/event_test.go",
    "content": "//go:build kubernetes\n// +build kubernetes\n\n// NOTE: we have build tags to differentiate kubernetes tests from non-kubernetes tests. This is done because minikube\n// is heavy and can interfere with docker related tests in terratest. Specifically, many of the tests start to fail with\n// `connection refused` errors from `minikube`. To avoid overloading the system, we run the kubernetes tests and helm\n// tests separately from the others. This may not be necessary if you have a sufficiently powerful machine.  We\n// recommend at least 4 cores and 16GB of RAM if you want to run all the tests together.\n\npackage k8s\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n\n\tv1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\t_ \"k8s.io/client-go/plugin/pkg/client/auth\"\n)\n\nfunc TestListEventsEReturnsNilErrorWhenListingEvents(t *testing.T) {\n\tt.Parallel()\n\n\toptions := NewKubectlOptions(\"\", \"\", \"kube-system\")\n\tevents, err := ListEventsE(t, options, v1.ListOptions{})\n\trequire.Nil(t, err)\n\trequire.Greater(t, len(events), 0)\n}\n\nfunc TestListEventsInNamespace(t *testing.T) {\n\tt.Parallel()\n\n\toptions := NewKubectlOptions(\"\", \"\", \"kube-system\")\n\tevents := ListEvents(t, options, v1.ListOptions{})\n\trequire.Greater(t, len(events), 0)\n}\n\nfunc TestListEventsReturnsZeroEventsIfNoneCreated(t *testing.T) {\n\tt.Parallel()\n\tns := \"test-ns\"\n\n\toptions := NewKubectlOptions(\"\", \"\", \"\")\n\n\tdefer DeleteNamespace(t, options, ns)\n\tCreateNamespace(t, options, ns)\n\n\toptions.Namespace = ns\n\tevents := ListEvents(t, options, v1.ListOptions{})\n\trequire.Equal(t, 0, len(events))\n}\n"
  },
  {
    "path": "modules/k8s/ingress.go",
    "content": "package k8s\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/require\"\n\tnetworkingv1 \"k8s.io/api/networking/v1\"\n\tnetworkingv1beta1 \"k8s.io/api/networking/v1beta1\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\n\t\"github.com/gruntwork-io/terratest/modules/retry\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n)\n\n// ListIngresses will look for Ingress resources in the given namespace that match the given filters and return them.\n// This will fail the test if there is an error.\nfunc ListIngresses(t testing.TestingT, options *KubectlOptions, filters metav1.ListOptions) []networkingv1.Ingress {\n\tingresses, err := ListIngressesE(t, options, filters)\n\trequire.NoError(t, err)\n\treturn ingresses\n}\n\n// ListIngressesE will look for Ingress resources in the given namespace that match the given filters and return them.\nfunc ListIngressesE(t testing.TestingT, options *KubectlOptions, filters metav1.ListOptions) ([]networkingv1.Ingress, error) {\n\tclientset, err := GetKubernetesClientFromOptionsE(t, options)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tresp, err := clientset.NetworkingV1().Ingresses(options.Namespace).List(context.Background(), filters)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn resp.Items, nil\n\n}\n\n// GetIngress returns a Kubernetes Ingress resource in the provided namespace with the given name. This will fail the\n// test if there is an error.\nfunc GetIngress(t testing.TestingT, options *KubectlOptions, ingressName string) *networkingv1.Ingress {\n\tingress, err := GetIngressE(t, options, ingressName)\n\trequire.NoError(t, err)\n\treturn ingress\n}\n\n// GetIngressE returns a Kubernetes Ingress resource in the provided namespace with the given name.\nfunc GetIngressE(t testing.TestingT, options *KubectlOptions, ingressName string) (*networkingv1.Ingress, error) {\n\tclientset, err := GetKubernetesClientFromOptionsE(t, options)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn clientset.NetworkingV1().Ingresses(options.Namespace).Get(context.Background(), ingressName, metav1.GetOptions{})\n}\n\n// IsIngressAvailable returns true if the Ingress endpoint is provisioned and available.\nfunc IsIngressAvailable(ingress *networkingv1.Ingress) bool {\n\t// Ingress is ready if it has at least one endpoint\n\tendpoints := ingress.Status.LoadBalancer.Ingress\n\treturn len(endpoints) > 0\n}\n\n// WaitUntilIngressAvailable waits until the Ingress resource has an endpoint provisioned for it.\nfunc WaitUntilIngressAvailable(t testing.TestingT, options *KubectlOptions, ingressName string, retries int, sleepBetweenRetries time.Duration) {\n\tstatusMsg := fmt.Sprintf(\"Wait for ingress %s to be provisioned.\", ingressName)\n\tmessage := retry.DoWithRetry(\n\t\tt,\n\t\tstatusMsg,\n\t\tretries,\n\t\tsleepBetweenRetries,\n\t\tfunc() (string, error) {\n\t\t\tingress, err := GetIngressE(t, options, ingressName)\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", err\n\t\t\t}\n\t\t\tif !IsIngressAvailable(ingress) {\n\t\t\t\treturn \"\", IngressNotAvailable{ingress: ingress}\n\t\t\t}\n\t\t\treturn \"Ingress is now available\", nil\n\t\t},\n\t)\n\toptions.Logger.Logf(t, \"%s\", message)\n}\n\n// ListIngressesV1Beta1 will look for Ingress resources in the given namespace that match the given filters and return\n// them, using networking.k8s.io/v1beta1 API. This will fail the test if there is an error.\nfunc ListIngressesV1Beta1(t testing.TestingT, options *KubectlOptions, filters metav1.ListOptions) []networkingv1beta1.Ingress {\n\tingresses, err := ListIngressesV1Beta1E(t, options, filters)\n\trequire.NoError(t, err)\n\treturn ingresses\n}\n\n// ListIngressesV1Beta1E will look for Ingress resources in the given namespace that match the given filters and return\n// them, using networking.k8s.io/v1beta1 API.\nfunc ListIngressesV1Beta1E(t testing.TestingT, options *KubectlOptions, filters metav1.ListOptions) ([]networkingv1beta1.Ingress, error) {\n\tclientset, err := GetKubernetesClientFromOptionsE(t, options)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tresp, err := clientset.NetworkingV1beta1().Ingresses(options.Namespace).List(context.Background(), filters)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn resp.Items, nil\n\n}\n\n// GetIngressV1Beta1 returns a Kubernetes Ingress resource in the provided namespace with the given name, using\n// networking.k8s.io/v1beta1 API. This will fail the test if there is an error.\nfunc GetIngressV1Beta1(t testing.TestingT, options *KubectlOptions, ingressName string) *networkingv1beta1.Ingress {\n\tingress, err := GetIngressV1Beta1E(t, options, ingressName)\n\trequire.NoError(t, err)\n\treturn ingress\n}\n\n// GetIngressV1Beta1E returns a Kubernetes Ingress resource in the provided namespace with the given name, using\n// networking.k8s.io/v1beta1.\nfunc GetIngressV1Beta1E(t testing.TestingT, options *KubectlOptions, ingressName string) (*networkingv1beta1.Ingress, error) {\n\tclientset, err := GetKubernetesClientFromOptionsE(t, options)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn clientset.NetworkingV1beta1().Ingresses(options.Namespace).Get(context.Background(), ingressName, metav1.GetOptions{})\n}\n\n// IsIngressAvailableV1Beta1 returns true if the Ingress endpoint is provisioned and available, using\n// networking.k8s.io/v1beta1 API.\nfunc IsIngressAvailableV1Beta1(ingress *networkingv1beta1.Ingress) bool {\n\t// Ingress is ready if it has at least one endpoint\n\tendpoints := ingress.Status.LoadBalancer.Ingress\n\treturn len(endpoints) > 0\n}\n\n// WaitUntilIngressAvailableV1Beta1 waits until the Ingress resource has an endpoint provisioned for it, using\n// networking.k8s.io/v1beta1 API.\nfunc WaitUntilIngressAvailableV1Beta1(t testing.TestingT, options *KubectlOptions, ingressName string, retries int, sleepBetweenRetries time.Duration) {\n\tstatusMsg := fmt.Sprintf(\"Wait for ingress %s to be provisioned.\", ingressName)\n\tmessage := retry.DoWithRetry(\n\t\tt,\n\t\tstatusMsg,\n\t\tretries,\n\t\tsleepBetweenRetries,\n\t\tfunc() (string, error) {\n\t\t\tingress, err := GetIngressV1Beta1E(t, options, ingressName)\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", err\n\t\t\t}\n\t\t\tif !IsIngressAvailableV1Beta1(ingress) {\n\t\t\t\treturn \"\", IngressNotAvailableV1Beta1{ingress: ingress}\n\t\t\t}\n\t\t\treturn \"Ingress is now available\", nil\n\t\t},\n\t)\n\toptions.Logger.Logf(t, \"%s\", message)\n}\n"
  },
  {
    "path": "modules/k8s/ingress_test.go",
    "content": "//go:build kubernetes\n// +build kubernetes\n\n// NOTE: we have build tags to differentiate kubernetes tests from non-kubernetes tests. This is done because minikube\n// is heavy and can interfere with docker related tests in terratest. Specifically, many of the tests start to fail with\n// `connection refused` errors from `minikube`. To avoid overloading the system, we run the kubernetes tests and helm\n// tests separately from the others. This may not be necessary if you have a sufficiently powerful machine.  We\n// recommend at least 4 cores and 16GB of RAM if you want to run all the tests together.\n\npackage k8s\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/require\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n)\n\nconst ExampleIngressName = \"nginx-service-ingress\"\n\nfunc TestGetIngressEReturnsErrorForNonExistantIngress(t *testing.T) {\n\tt.Parallel()\n\n\toptions := NewKubectlOptions(\"\", \"\", \"default\")\n\t_, err := GetIngressE(t, options, \"i-dont-exist\")\n\trequire.Error(t, err)\n}\n\nfunc TestGetIngressEReturnsCorrectIngressInCorrectNamespace(t *testing.T) {\n\tt.Parallel()\n\n\tuniqueID := strings.ToLower(random.UniqueId())\n\toptions := NewKubectlOptions(\"\", \"\", uniqueID)\n\tconfigData := fmt.Sprintf(exampleIngressDeploymentYamlTemplate, uniqueID, uniqueID, uniqueID, uniqueID, uniqueID)\n\tKubectlApplyFromString(t, options, configData)\n\tdefer KubectlDeleteFromString(t, options, configData)\n\n\tservice := GetIngress(t, options, \"nginx-service-ingress\")\n\trequire.Equal(t, service.Name, \"nginx-service-ingress\")\n\trequire.Equal(t, service.Namespace, uniqueID)\n}\n\nfunc TestListIngressesReturnsCorrectIngressInCorrectNamespace(t *testing.T) {\n\tt.Parallel()\n\n\tuniqueID := strings.ToLower(random.UniqueId())\n\toptions := NewKubectlOptions(\"\", \"\", uniqueID)\n\tconfigData := fmt.Sprintf(exampleIngressDeploymentYamlTemplate, uniqueID, uniqueID, uniqueID, uniqueID, uniqueID)\n\tKubectlApplyFromString(t, options, configData)\n\tdefer KubectlDeleteFromString(t, options, configData)\n\n\tingresses := ListIngresses(t, options, metav1.ListOptions{})\n\trequire.Equal(t, len(ingresses), 1)\n\n\tingress := ingresses[0]\n\trequire.Equal(t, ingress.Name, ExampleIngressName)\n\trequire.Equal(t, ingress.Namespace, uniqueID)\n}\n\nfunc TestWaitUntilIngressAvailableReturnsSuccessfully(t *testing.T) {\n\tt.Parallel()\n\n\tuniqueID := strings.ToLower(random.UniqueId())\n\toptions := NewKubectlOptions(\"\", \"\", uniqueID)\n\tconfigData := fmt.Sprintf(exampleIngressDeploymentYamlTemplate, uniqueID, uniqueID, uniqueID, uniqueID, uniqueID)\n\tKubectlApplyFromString(t, options, configData)\n\tdefer KubectlDeleteFromString(t, options, configData)\n\n\tWaitUntilIngressAvailable(t, options, ExampleIngressName, 60, 5*time.Second)\n}\n\nconst exampleIngressDeploymentYamlTemplate = `---\napiVersion: v1\nkind: Namespace\nmetadata:\n  name: %s\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: nginx-deployment\n  namespace: %s\nspec:\n  selector:\n    matchLabels:\n      app: nginx\n  replicas: 1\n  template:\n    metadata:\n      labels:\n        app: nginx\n    spec:\n      containers:\n      - name: nginx\n        image: nginx:1.15.7\n        ports:\n        - containerPort: 80\n---\nkind: Service\napiVersion: v1\nmetadata:\n  name: nginx-service\n  namespace: %s\nspec:\n  selector:\n    app: nginx\n  ports:\n  - protocol: TCP\n    targetPort: 80\n    port: 80\n  type: NodePort\n---\nkind: Ingress\napiVersion: networking.k8s.io/v1\nmetadata:\n  name: nginx-service-ingress\n  namespace: %s\nspec:\n  rules:\n  - http:\n      paths:\n      - path: /app-%s\n        pathType: Prefix\n        backend:\n          service:\n            name: nginx-service\n            port:\n              number: 80\n`\n"
  },
  {
    "path": "modules/k8s/job.go",
    "content": "package k8s\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/require\"\n\tbatchv1 \"k8s.io/api/batch/v1\"\n\tcorev1 \"k8s.io/api/core/v1\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\n\t\"github.com/gruntwork-io/terratest/modules/retry\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n)\n\n// ListJobs will look for Jobs in the given namespace that match the given filters and return them. This will fail the\n// test if there is an error.\nfunc ListJobs(t testing.TestingT, options *KubectlOptions, filters metav1.ListOptions) []batchv1.Job {\n\tjobs, err := ListJobsE(t, options, filters)\n\trequire.NoError(t, err)\n\treturn jobs\n}\n\n// ListJobsE will look for jobs in the given namespace that match the given filters and return them.\nfunc ListJobsE(t testing.TestingT, options *KubectlOptions, filters metav1.ListOptions) ([]batchv1.Job, error) {\n\tclientset, err := GetKubernetesClientFromOptionsE(t, options)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresp, err := clientset.BatchV1().Jobs(options.Namespace).List(context.Background(), filters)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn resp.Items, nil\n}\n\n// GetJob returns a Kubernetes job resource in the provided namespace with the given name. This will\n// fail the test if there is an error.\nfunc GetJob(t testing.TestingT, options *KubectlOptions, jobName string) *batchv1.Job {\n\tjob, err := GetJobE(t, options, jobName)\n\trequire.NoError(t, err)\n\treturn job\n}\n\n// GetJobE returns a Kubernetes job resource in the provided namespace with the given name.\nfunc GetJobE(t testing.TestingT, options *KubectlOptions, jobName string) (*batchv1.Job, error) {\n\tclientset, err := GetKubernetesClientFromOptionsE(t, options)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn clientset.BatchV1().Jobs(options.Namespace).Get(context.Background(), jobName, metav1.GetOptions{})\n}\n\n// WaitUntilJobSucceed waits until requested job is suceeded, retrying the check for the specified amount of times, sleeping\n// for the provided duration between each try. This will fail the test if there is an error or if the check times out.\nfunc WaitUntilJobSucceed(t testing.TestingT, options *KubectlOptions, jobName string, retries int, sleepBetweenRetries time.Duration) {\n\trequire.NoError(t, WaitUntilJobSucceedE(t, options, jobName, retries, sleepBetweenRetries))\n}\n\n// WaitUntilJobSucceedE waits until requested job is succeeded, retrying the check for the specified amount of times, sleeping\n// for the provided duration between each try.\nfunc WaitUntilJobSucceedE(t testing.TestingT, options *KubectlOptions, jobName string, retries int, sleepBetweenRetries time.Duration) error {\n\tstatusMsg := fmt.Sprintf(\"Wait for job %s to be provisioned.\", jobName)\n\tmessage, err := retry.DoWithRetryE(\n\t\tt,\n\t\tstatusMsg,\n\t\tretries,\n\t\tsleepBetweenRetries,\n\t\tfunc() (string, error) {\n\t\t\tjob, err := GetJobE(t, options, jobName)\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", err\n\t\t\t}\n\t\t\tif !IsJobSucceeded(job) {\n\t\t\t\treturn \"\", NewJobNotSucceeded(job)\n\t\t\t}\n\t\t\treturn \"Job is now Succeeded\", nil\n\t\t},\n\t)\n\tif err != nil {\n\t\toptions.Logger.Logf(t, \"Timed out waiting for Job to be provisioned: %s\", err)\n\t\treturn err\n\t}\n\toptions.Logger.Logf(t, \"%s\", message)\n\treturn nil\n}\n\n// IsJobSucceeded returns true when the job status condition \"Complete\" is true. This behavior is documented in the kubernetes API reference:\n// https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/job-v1/#JobStatus\nfunc IsJobSucceeded(job *batchv1.Job) bool {\n\tfor _, condition := range job.Status.Conditions {\n\t\tif condition.Type == batchv1.JobComplete && condition.Status == corev1.ConditionTrue {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// CreateJobFromCronJob creates a Job from the specified CronJob in the given namespace and returns the created Job.\nfunc CreateJobFromCronJob(t testing.TestingT, options *KubectlOptions, cronJobName, newJobName string) *batchv1.Job {\n\tjob, err := CreateJobFromCronJobE(t, options, cronJobName, newJobName)\n\trequire.NoError(t, err)\n\treturn job\n}\n\n// CreateJobFromCronJobE creates a Job from the specified CronJob in the given namespace and returns the created Job.\n// This function is similar to running `kubectl create job --from=cronjob/<cron-job-name> <new-job-name>`.\nfunc CreateJobFromCronJobE(t testing.TestingT, options *KubectlOptions, cronJobName, newJobName string) (*batchv1.Job, error) {\n\tclientset, err := GetKubernetesClientFromOptionsE(t, options)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tcronJob, err := GetCronJobE(t, options, cronJobName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tannotations := make(map[string]string)\n\tfor k, v := range cronJob.Spec.JobTemplate.Annotations {\n\t\tannotations[k] = v\n\t}\n\n\tjob := &batchv1.Job{\n\t\tTypeMeta: metav1.TypeMeta{APIVersion: batchv1.SchemeGroupVersion.String(), Kind: \"Job\"},\n\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\tName:        newJobName,\n\t\t\tNamespace:   options.Namespace,\n\t\t\tLabels:      cronJob.Spec.JobTemplate.Labels,\n\t\t\tAnnotations: annotations,\n\t\t},\n\t\tSpec: cronJob.Spec.JobTemplate.Spec,\n\t}\n\n\tcreatedJob, err := clientset.BatchV1().Jobs(options.Namespace).Create(context.Background(), job, metav1.CreateOptions{})\n\treturn createdJob, err\n}\n"
  },
  {
    "path": "modules/k8s/job_test.go",
    "content": "//go:build kubeall || kubernetes\n// +build kubeall kubernetes\n\n// NOTE: we have build tags to differentiate kubernetes tests from non-kubernetes tests. This is done because minikube\n// is heavy and can interfere with docker related tests in terratest. Specifically, many of the tests start to fail with\n// `connection refused` errors from `minikube`. To avoid overloading the system, we run the kubernetes tests and helm\n// tests separately from the others. This may not be necessary if you have a sufficiently powerful machine.  We\n// recommend at least 4 cores and 16GB of RAM if you want to run all the tests together.\n\npackage k8s\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\tbatchv1 \"k8s.io/api/batch/v1\"\n\tcorev1 \"k8s.io/api/core/v1\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n)\n\nfunc TestListJobsReturnsJobsInNamespace(t *testing.T) {\n\tt.Parallel()\n\n\tuniqueID := strings.ToLower(random.UniqueId())\n\toptions := NewKubectlOptions(\"\", \"\", uniqueID)\n\tconfigData := fmt.Sprintf(EXAMPLE_JOB_YAML_TEMPLATE, uniqueID, uniqueID)\n\tdefer KubectlDeleteFromString(t, options, configData)\n\tKubectlApplyFromString(t, options, configData)\n\n\tjobs := ListJobs(t, options, metav1.ListOptions{})\n\trequire.Equal(t, len(jobs), 1)\n\tjob := jobs[0]\n\trequire.Equal(t, job.Name, \"pi-job\")\n\trequire.Equal(t, job.Namespace, uniqueID)\n}\n\nfunc TestGetJobEReturnsErrorForNonExistantJob(t *testing.T) {\n\tt.Parallel()\n\n\toptions := NewKubectlOptions(\"\", \"\", \"default\")\n\t_, err := GetJobE(t, options, \"pi-job\")\n\trequire.Error(t, err)\n}\n\nfunc TestGetJobEReturnsCorrectJobInCorrectNamespace(t *testing.T) {\n\tt.Parallel()\n\n\tuniqueID := strings.ToLower(random.UniqueId())\n\toptions := NewKubectlOptions(\"\", \"\", uniqueID)\n\tconfigData := fmt.Sprintf(EXAMPLE_JOB_YAML_TEMPLATE, uniqueID, uniqueID)\n\tdefer KubectlDeleteFromString(t, options, configData)\n\tKubectlApplyFromString(t, options, configData)\n\n\tjob := GetJob(t, options, \"pi-job\")\n\trequire.Equal(t, job.Name, \"pi-job\")\n\trequire.Equal(t, job.Namespace, uniqueID)\n}\n\nfunc TestWaitUntilJobSucceedReturnsSuccessfully(t *testing.T) {\n\tt.Parallel()\n\n\tuniqueID := strings.ToLower(random.UniqueId())\n\toptions := NewKubectlOptions(\"\", \"\", uniqueID)\n\tconfigData := fmt.Sprintf(EXAMPLE_JOB_YAML_TEMPLATE, uniqueID, uniqueID)\n\tdefer KubectlDeleteFromString(t, options, configData)\n\tKubectlApplyFromString(t, options, configData)\n\n\tWaitUntilJobSucceed(t, options, \"pi-job\", 60, 1*time.Second)\n}\n\nfunc TestIsJobSucceeded(t *testing.T) {\n\tt.Parallel()\n\n\tcases := []struct {\n\t\ttitle          string\n\t\tjob            *batchv1.Job\n\t\texpectedResult bool\n\t}{\n\t\t{\n\t\t\ttitle: \"TestIsJobSucceeded\",\n\t\t\tjob: &batchv1.Job{\n\t\t\t\tStatus: batchv1.JobStatus{\n\t\t\t\t\tConditions: []batchv1.JobCondition{\n\t\t\t\t\t\tbatchv1.JobCondition{\n\t\t\t\t\t\t\tType:   batchv1.JobComplete,\n\t\t\t\t\t\t\tStatus: corev1.ConditionTrue,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedResult: true,\n\t\t},\n\t\t{\n\t\t\ttitle: \"TestIsJobFailed\",\n\t\t\tjob: &batchv1.Job{\n\t\t\t\tStatus: batchv1.JobStatus{\n\t\t\t\t\tConditions: []batchv1.JobCondition{\n\t\t\t\t\t\tbatchv1.JobCondition{\n\t\t\t\t\t\t\tType:   batchv1.JobFailed,\n\t\t\t\t\t\t\tStatus: corev1.ConditionTrue,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedResult: false,\n\t\t},\n\t\t{\n\t\t\ttitle: \"TestIsJobStarting\",\n\t\t\tjob: &batchv1.Job{\n\t\t\t\tStatus: batchv1.JobStatus{\n\t\t\t\t\tConditions: []batchv1.JobCondition{},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedResult: false,\n\t\t},\n\t}\n\tfor _, tc := range cases {\n\t\ttc := tc\n\t\tt.Run(tc.title, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tactualResult := IsJobSucceeded(tc.job)\n\t\t\trequire.Equal(t, tc.expectedResult, actualResult)\n\t\t})\n\t}\n}\n\nfunc TestCreateJobFromCronJobReturnsCreatedJob(t *testing.T) {\n\tt.Parallel()\n\n\tuniqueID := strings.ToLower(random.UniqueId())\n\toptions := NewKubectlOptions(\"\", \"\", uniqueID)\n\tconfigData := fmt.Sprintf(EXAMPLE_CRON_JOB_YAML_TEMPLATE, uniqueID, uniqueID)\n\tdefer KubectlDeleteFromString(t, options, configData)\n\tKubectlApplyFromString(t, options, configData)\n\n\tnewJobName := \"pi-copied-job\"\n\tjob := CreateJobFromCronJob(t, options, \"pi-cronjob\", newJobName)\n\trequire.NotNil(t, job)\n\tassert.Equal(t, job.Namespace, uniqueID)\n\tassert.Equal(t, job.Name, newJobName)\n}\n\nfunc TestCreateJobFromCronJobEReturnsErrorForNonExistentCronJob(t *testing.T) {\n\tt.Parallel()\n\toptions := NewKubectlOptions(\"\", \"\", \"default\")\n\t_, err := CreateJobFromCronJobE(t, options, \"non-existent-cronjob\", \"new-job-name\")\n\trequire.Error(t, err)\n}\n\nconst EXAMPLE_JOB_YAML_TEMPLATE = `---\napiVersion: v1\nkind: Namespace\nmetadata:\n  name: %s\n---\napiVersion: batch/v1\nkind: Job\nmetadata:\n  name: pi-job\n  namespace: %s\nspec:\n  template:\n    spec:\n      containers:\n      - name: pi\n        image: \"perl:5.34.1\"\n        command: [\"perl\",  \"-Mbignum=bpi\", \"-wle\", \"print bpi(2000)\"]\n      restartPolicy: Never\n  backoffLimit: 4\n`\n\nconst EXAMPLE_CRON_JOB_YAML_TEMPLATE = `---\napiVersion: v1\nkind: Namespace\nmetadata:\n  name: %s\n---\napiVersion: batch/v1\nkind: CronJob\nmetadata:\n  name: pi-cronjob\n  namespace: %s\nspec:\n  schedule: \"* 1 * * *\"\n  successfulJobsHistoryLimit: 3\n  failedJobsHistoryLimit: 1\n  jobTemplate:\n    spec:\n      backoffLimit: 4\n      template:\n        spec:\n          containers:\n          - name: pi\n            image: \"perl:5.34.1\"\n            command: [\"perl\", \"-Mbignum=bpi\", \"-wle\", \"print bpi(2000)\"]\n          restartPolicy: Never\n`\n"
  },
  {
    "path": "modules/k8s/jsonpath.go",
    "content": "package k8s\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\n\t\"k8s.io/client-go/util/jsonpath\"\n\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// UnmarshalJSONPath allows you to use an arbitrary JSONPath string to query a json blob and unmarshal the resulting\n// output into a go object. Note that the output will always be a list. That means that if you query a single object,\n// the output will be a list of single element, not the element itself. However, if the json path maps to a list, then\n// the output will be that list.\n// Example:\n//\n// jsonBlob := []byte(`{\"key\": {\"data\": [1,2,3]}}`)\n// jsonPath := \"{.key.data[*]}\"\n// var output []int\n// UnmarshalJSONPath(t, jsonBlob, jsonPath, &output)\n// // output is []int{1,2,3}\n//\n// This will fail the test if there is an error.\nfunc UnmarshalJSONPath(t testing.TestingT, jsonData []byte, jsonpathStr string, output interface{}) {\n\terr := UnmarshalJSONPathE(t, jsonData, jsonpathStr, output)\n\trequire.NoError(t, err)\n}\n\n// UnmarshalJSONPathE allows you to use an arbitrary JSONPath string to query a json blob and unmarshal the resulting\n// output into a go object. Note that the output will always be a list. That means that if you query a single object,\n// the output will be a list of single element, not the element itself. However, if the json path maps to a list, then\n// the output will be that list.\n// Example:\n//\n// jsonBlob := []byte(`{\"key\": {\"data\": [1,2,3]}}`)\n// jsonPath := \"{.key.data[*]}\"\n// var output []int\n// UnmarshalJSONPathE(t, jsonBlob, jsonPath, &output)\n// => output = []int{1,2,3}\nfunc UnmarshalJSONPathE(t testing.TestingT, jsonData []byte, jsonpathStr string, output interface{}) error {\n\t// First, unmarshal the full json object. We use interface{} to avoid the type conversions, as jsonpath will handle\n\t// it for us.\n\tvar blob interface{}\n\tif err := json.Unmarshal(jsonData, &blob); err != nil {\n\t\treturn JSONPathMalformedJSONErr{err}\n\t}\n\n\t// Then, query the json object with the given jsonpath to get the output string.\n\tjsonpathParser := jsonpath.New(t.Name())\n\tjsonpathParser.EnableJSONOutput(true)\n\tif err := jsonpathParser.Parse(jsonpathStr); err != nil {\n\t\treturn JSONPathMalformedJSONPathErr{err}\n\t}\n\toutputJSONBuffer := new(bytes.Buffer)\n\tif err := jsonpathParser.Execute(outputJSONBuffer, blob); err != nil {\n\t\treturn JSONPathExtractJSONPathErr{err}\n\t}\n\toutputJSON := outputJSONBuffer.Bytes()\n\n\t// Finally, we need to unmarshal the output object into the given output var.\n\tif err := json.Unmarshal(outputJSON, output); err != nil {\n\t\treturn JSONPathMalformedJSONPathResultErr{err}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "modules/k8s/jsonpath_test.go",
    "content": "package k8s\n\nimport (\n\t\"encoding/json\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestUnmarshalJSONPath(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tname        string\n\t\tjsonBlob    string\n\t\tjsonPath    string\n\t\texpectedOut interface{}\n\t}{\n\t\t{\n\t\t\t\"boolField\",\n\t\t\t`{\"key\": true}`,\n\t\t\t\"{ .key }\",\n\t\t\t[]bool{true},\n\t\t},\n\t\t{\n\t\t\t\"nestedObject\",\n\t\t\t`{\"key\": {\"data\": [1,2,3]}}`,\n\t\t\t\"{ .key }\",\n\t\t\t[]map[string][]int{\n\t\t\t\tmap[string][]int{\n\t\t\t\t\t\"data\": []int{1, 2, 3},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"nestedArray\",\n\t\t\t`{\"key\": {\"data\": [1,2,3]}}`,\n\t\t\t\"{ .key.data[*] }\",\n\t\t\t[]int{1, 2, 3},\n\t\t},\n\t}\n\n\tfor _, testCase := range testCases {\n\t\t// capture range variable so that it doesn't update when the subtest goroutine swaps.\n\t\ttestCase := testCase\n\t\tt.Run(testCase.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tvar output interface{}\n\t\t\tUnmarshalJSONPath(t, []byte(testCase.jsonBlob), testCase.jsonPath, &output)\n\t\t\t// NOTE: we have to do equality check on the marshalled json data to allow equality checks over dynamic\n\t\t\t// types in this table driven test.\n\t\t\texpectedOutJSON, err := json.Marshal(testCase.expectedOut)\n\t\t\trequire.NoError(t, err)\n\t\t\tactualOutJSON, err := json.Marshal(output)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, string(expectedOutJSON), string(actualOutJSON))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "modules/k8s/k8s.go",
    "content": "// Package k8s provides common functionalities for interacting with a Kubernetes cluster in the context of\n// infrastructure testing.\npackage k8s\n"
  },
  {
    "path": "modules/k8s/kubectl.go",
    "content": "package k8s\n\nimport (\n\t\"net/url\"\n\t\"os\"\n\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/gruntwork-io/terratest/modules/shell\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n)\n\n// RunKubectl will call kubectl using the provided options and args, failing the test on error.\nfunc RunKubectl(t testing.TestingT, options *KubectlOptions, args ...string) {\n\trequire.NoError(t, RunKubectlE(t, options, args...))\n}\n\n// RunKubectlE will call kubectl using the provided options and args.\nfunc RunKubectlE(t testing.TestingT, options *KubectlOptions, args ...string) error {\n\t_, err := RunKubectlAndGetOutputE(t, options, args...)\n\treturn err\n}\n\n// RunKubectlAndGetOutputE will call kubectl using the provided options and args, returning the output of stdout and\n// stderr.\nfunc RunKubectlAndGetOutputE(t testing.TestingT, options *KubectlOptions, args ...string) (string, error) {\n\tcmdArgs := []string{}\n\tif options.ContextName != \"\" {\n\t\tcmdArgs = append(cmdArgs, \"--context\", options.ContextName)\n\t}\n\tif options.ConfigPath != \"\" {\n\t\tcmdArgs = append(cmdArgs, \"--kubeconfig\", options.ConfigPath)\n\t}\n\tif options.Namespace != \"\" {\n\t\tcmdArgs = append(cmdArgs, \"--namespace\", options.Namespace)\n\t}\n\tif options.RequestTimeout > 0 {\n\t\tcmdArgs = append(cmdArgs, \"--request-timeout\", options.RequestTimeout.String())\n\t}\n\tcmdArgs = append(cmdArgs, args...)\n\tcommand := shell.Command{\n\t\tCommand: \"kubectl\",\n\t\tArgs:    cmdArgs,\n\t\tEnv:     options.Env,\n\t\tLogger:  options.Logger,\n\t}\n\treturn shell.RunCommandAndGetOutputE(t, command)\n}\n\n// KubectlDelete will take in a file path and delete it from the cluster targeted by KubectlOptions. If there are any\n// errors, fail the test immediately.\nfunc KubectlDelete(t testing.TestingT, options *KubectlOptions, configPath string) {\n\trequire.NoError(t, KubectlDeleteE(t, options, configPath))\n}\n\n// KubectlDeleteE will take in a file path and delete it from the cluster targeted by KubectlOptions.\nfunc KubectlDeleteE(t testing.TestingT, options *KubectlOptions, configPath string) error {\n\treturn RunKubectlE(t, options, \"delete\", \"-f\", configPath)\n}\n\n// KubectlDeleteFromKustomize will take in a kustomization directory path and delete it from the cluster targeted by KubectlOptions. If there are any\n// errors, fail the test immediately.\nfunc KubectlDeleteFromKustomize(t testing.TestingT, options *KubectlOptions, configPath string) {\n\trequire.NoError(t, KubectlDeleteFromKustomizeE(t, options, configPath))\n}\n\n// KubectlDeleteFromKustomizeE will take in a kustomization directory path and delete it from the cluster targeted by KubectlOptions.\nfunc KubectlDeleteFromKustomizeE(t testing.TestingT, options *KubectlOptions, configPath string) error {\n\treturn RunKubectlE(t, options, \"delete\", \"-k\", configPath)\n}\n\n// KubectlDeleteFromString will take in a kubernetes resource config as a string and delete it on the cluster specified\n// by the provided kubectl options.\nfunc KubectlDeleteFromString(t testing.TestingT, options *KubectlOptions, configData string) {\n\trequire.NoError(t, KubectlDeleteFromStringE(t, options, configData))\n}\n\n// KubectlDeleteFromStringE will take in a kubernetes resource config as a string and delete it on the cluster specified\n// by the provided kubectl options. If it fails, this will return the error.\nfunc KubectlDeleteFromStringE(t testing.TestingT, options *KubectlOptions, configData string) error {\n\ttmpfile, err := StoreConfigToTempFileE(t, configData)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer os.Remove(tmpfile)\n\treturn KubectlDeleteE(t, options, tmpfile)\n}\n\n// KubectlApply will take in a file path and apply it to the cluster targeted by KubectlOptions. If there are any\n// errors, fail the test immediately.\nfunc KubectlApply(t testing.TestingT, options *KubectlOptions, configPath string) {\n\trequire.NoError(t, KubectlApplyE(t, options, configPath))\n}\n\n// KubectlApplyE will take in a file path and apply it to the cluster targeted by KubectlOptions.\nfunc KubectlApplyE(t testing.TestingT, options *KubectlOptions, configPath string) error {\n\treturn RunKubectlE(t, options, \"apply\", \"-f\", configPath)\n}\n\n// KubectlApplyFromKustomize will take in a kustomization directory path and apply it to the cluster targeted by KubectlOptions. If there are any\n// errors, fail the test immediately.\nfunc KubectlApplyFromKustomize(t testing.TestingT, options *KubectlOptions, configPath string) {\n\trequire.NoError(t, KubectlApplyFromKustomizeE(t, options, configPath))\n}\n\n// KubectlApplyFromKustomizeE will take in a kustomization directory path and apply it to the cluster targeted by KubectlOptions.\nfunc KubectlApplyFromKustomizeE(t testing.TestingT, options *KubectlOptions, configPath string) error {\n\treturn RunKubectlE(t, options, \"apply\", \"-k\", configPath)\n}\n\n// KubectlApplyFromString will take in a kubernetes resource config as a string and apply it on the cluster specified\n// by the provided kubectl options.\nfunc KubectlApplyFromString(t testing.TestingT, options *KubectlOptions, configData string) {\n\trequire.NoError(t, KubectlApplyFromStringE(t, options, configData))\n}\n\n// KubectlApplyFromStringE will take in a kubernetes resource config as a string and apply it on the cluster specified\n// by the provided kubectl options. If it fails, this will return the error.\nfunc KubectlApplyFromStringE(t testing.TestingT, options *KubectlOptions, configData string) error {\n\ttmpfile, err := StoreConfigToTempFileE(t, configData)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer os.Remove(tmpfile)\n\treturn KubectlApplyE(t, options, tmpfile)\n}\n\n// StoreConfigToTempFile will store the provided config data to a temporary file created on the os and return the\n// filename.\nfunc StoreConfigToTempFile(t testing.TestingT, configData string) string {\n\tout, err := StoreConfigToTempFileE(t, configData)\n\trequire.NoError(t, err)\n\treturn out\n}\n\n// StoreConfigToTempFileE will store the provided config data to a temporary file created on the os and return the\n// filename, or error.\nfunc StoreConfigToTempFileE(t testing.TestingT, configData string) (string, error) {\n\tescapedTestName := url.PathEscape(t.Name())\n\ttmpfile, err := os.CreateTemp(\"\", escapedTestName)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tdefer tmpfile.Close()\n\n\t_, err = tmpfile.WriteString(configData)\n\treturn tmpfile.Name(), err\n}\n"
  },
  {
    "path": "modules/k8s/kubectl_options.go",
    "content": "package k8s\n\nimport (\n\t\"time\"\n\n\t\"github.com/gruntwork-io/terratest/modules/logger\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"k8s.io/client-go/rest\"\n)\n\n// KubectlOptions represents common options necessary to specify for all Kubectl calls\ntype KubectlOptions struct {\n\tContextName    string\n\tConfigPath     string\n\tNamespace      string\n\tEnv            map[string]string\n\tInClusterAuth  bool\n\tRestConfig     *rest.Config\n\tLogger         *logger.Logger\n\tRequestTimeout time.Duration\n}\n\n// NewKubectlOptions will return a pointer to new instance of KubectlOptions with the configured options\nfunc NewKubectlOptions(contextName string, configPath string, namespace string) *KubectlOptions {\n\treturn &KubectlOptions{\n\t\tContextName: contextName,\n\t\tConfigPath:  configPath,\n\t\tNamespace:   namespace,\n\t\tEnv:         map[string]string{},\n\t}\n}\n\n// NewKubectlOptionsWithInClusterAuth will return a pointer to a new instance of KubectlOptions with the InClusterAuth field set to true\nfunc NewKubectlOptionsWithInClusterAuth() *KubectlOptions {\n\treturn &KubectlOptions{\n\t\tInClusterAuth: true,\n\t}\n}\n\n// NewKubectlOptionsWithRestConfig will return a pointer to a new instance of KubectlOptions with pre-built config object\nfunc NewKubectlOptionsWithRestConfig(config *rest.Config, namespace string) *KubectlOptions {\n\treturn &KubectlOptions{\n\t\tNamespace:  namespace,\n\t\tRestConfig: config,\n\t}\n}\n\n// GetConfigPath will return a sensible default if the config path is not set on the options.\nfunc (kubectlOptions *KubectlOptions) GetConfigPath(t testing.TestingT) (string, error) {\n\t// We predeclare `err` here so that we can update `kubeConfigPath` in the if block below. Otherwise, go complains\n\t// saying `err` is undefined.\n\tvar err error\n\n\tkubeConfigPath := kubectlOptions.ConfigPath\n\tif kubeConfigPath == \"\" {\n\t\tkubeConfigPath, err = GetKubeConfigPathE(t)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\treturn kubeConfigPath, nil\n}\n"
  },
  {
    "path": "modules/k8s/kubectl_test.go",
    "content": "//go:build kubeall || kubernetes\n// +build kubeall kubernetes\n\n// NOTE: we have build tags to differentiate kubernetes tests from non-kubernetes tests. This is done because minikube\n// is heavy and can interfere with docker related tests in terratest. Specifically, many of the tests start to fail with\n// `connection refused` errors from `minikube`. To avoid overloading the system, we run the kubernetes tests and helm\n// tests separately from the others. This may not be necessary if you have a sufficiently powerful machine.  We\n// recommend at least 4 cores and 16GB of RAM if you want to run all the tests together.\n\npackage k8s\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// Test that RunKubectlAndGetOutputE will run kubectl and return the output by running a can-i command call.\nfunc TestRunKubectlAndGetOutputReturnsOutput(t *testing.T) {\n\tnamespaceName := fmt.Sprintf(\"kubectl-test-%s\", strings.ToLower(random.UniqueId()))\n\toptions := NewKubectlOptions(\"\", \"\", namespaceName)\n\toutput, err := RunKubectlAndGetOutputE(t, options, \"auth\", \"can-i\", \"get\", \"pods\")\n\trequire.NoError(t, err)\n\trequire.Equal(t, output, \"yes\")\n}\n\nfunc TestKubectlRequestTimeout(t *testing.T) {\n\tt.Parallel()\n\n\tvar parsedTimeout time.Duration\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tparsedTimeout, _ = time.ParseDuration(r.URL.Query().Get(\"timeout\"))\n\t\tselect {\n\t\tcase <-time.After(3 * time.Second):\n\t\tcase <-r.Context().Done():\n\t\t}\n\t\tw.WriteHeader(http.StatusInternalServerError)\n\t\tw.Write([]byte(\"dummy-error\"))\n\t}))\n\n\tconfig := fmt.Sprintf(`\napiVersion: v1\nkind: Config\nclusters:\n- name: dummy-cluster\n  cluster:\n    server: %s\nusers:\n- name: dummy-user\n  user:\n    token: dummy-token\ncontexts:\n- name: dummy-context\n  context:\n    cluster: dummy-cluster\n    user: dummy-user\ncurrent-context: dummy-context\n`, server.URL)\n\n\tt.Run(\"WithoutTimeout\", func(t *testing.T) {\n\t\toptions := &KubectlOptions{\n\t\t\tContextName: \"dummy-context\",\n\t\t\tConfigPath:  StoreConfigToTempFile(t, config),\n\t\t}\n\t\t_, err := RunKubectlAndGetOutputE(t, options, \"get\", \"pods\")\n\t\trequire.Error(t, err)\n\t\tassert.Contains(t, err.Error(), \"dummy-error\")\n\t\tassert.NotContains(t, err.Error(), \"Client.Timeout exceeded while awaiting headers\")\n\t})\n\n\tt.Run(\"WithTimeout\", func(t *testing.T) {\n\t\toptions := &KubectlOptions{\n\t\t\tContextName:    \"dummy-context\",\n\t\t\tConfigPath:     StoreConfigToTempFile(t, config),\n\t\t\tRequestTimeout: time.Second,\n\t\t}\n\t\t_, err := RunKubectlAndGetOutputE(t, options, \"get\", \"pods\")\n\t\trequire.Error(t, err)\n\t\tassert.Equal(t, options.RequestTimeout, parsedTimeout)\n\t\tassert.NotContains(t, err.Error(), \"dummy-error\")\n\t\tassert.Contains(t, err.Error(), \"Client.Timeout exceeded while awaiting headers\")\n\t})\n\n}\n"
  },
  {
    "path": "modules/k8s/minikube.go",
    "content": "package k8s\n\nimport (\n\t\"strings\"\n\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\tcorev1 \"k8s.io/api/core/v1\"\n)\n\n// IsMinikubeE returns true if the underlying kubernetes cluster is Minikube. This is determined by getting the\n// associated nodes and checking if all nodes has at least one label namespaced with \"minikube.k8s.io\".\nfunc IsMinikubeE(t testing.TestingT, options *KubectlOptions) (bool, error) {\n\tnodes, err := GetNodesE(t, options)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\t// ASSUMPTION: All minikube setups will have nodes with labels that are namespaced with minikube.k8s.io\n\tfor _, node := range nodes {\n\t\tif !nodeHasMinikubeLabel(node) {\n\t\t\treturn false, nil\n\t\t}\n\t}\n\n\t// At this point we know that all the nodes in the cluster has the minikube label, so we return true.\n\treturn true, nil\n}\n\n// nodeHasMinikubeLabel returns true if any of the labels on the node is namespaced with minikube.k8s.io\nfunc nodeHasMinikubeLabel(node corev1.Node) bool {\n\tlabels := node.GetLabels()\n\tfor key, _ := range labels {\n\t\tif strings.HasPrefix(key, \"minikube.k8s.io\") {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n"
  },
  {
    "path": "modules/k8s/minikube_test.go",
    "content": "//go:build kubeall || kubernetes\n// +build kubeall kubernetes\n\n// NOTE: we have build tags to differentiate kubernetes tests from non-kubernetes tests. This is done because minikube\n// is heavy and can interfere with docker related tests in terratest. Specifically, many of the tests start to fail with\n// `connection refused` errors from `minikube`. To avoid overloading the system, we run the kubernetes tests and helm\n// tests separately from the others. This may not be necessary if you have a sufficiently powerful machine.  We\n// recommend at least 4 cores and 16GB of RAM if you want to run all the tests together.\n\npackage k8s\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\n// Since we always run unit tests against minikube, we can only test if IsMinikubeE returns true.\nfunc TestIsMinikube(t *testing.T) {\n\tt.Parallel()\n\n\toptions := NewKubectlOptions(\"\", \"\", \"\")\n\tisMinikube, err := IsMinikubeE(t, options)\n\tassert.NoError(t, err)\n\tassert.True(t, isMinikube)\n}\n"
  },
  {
    "path": "modules/k8s/namespace.go",
    "content": "package k8s\n\nimport (\n\t\"context\"\n\t\"strings\"\n\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/stretchr/testify/require\"\n\tcorev1 \"k8s.io/api/core/v1\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n)\n\n// CreateNamespace will create a new Kubernetes namespace on the cluster targeted by the provided options. This will\n// fail the test if there is an error in creating the namespace.\nfunc CreateNamespace(t testing.TestingT, options *KubectlOptions, namespaceName string) {\n\trequire.NoError(t, CreateNamespaceE(t, options, namespaceName))\n}\n\n// CreateNamespaceE will create a new Kubernetes namespace on the cluster targeted by the provided options.\nfunc CreateNamespaceE(t testing.TestingT, options *KubectlOptions, namespaceName string) error {\n\tnamespaceObject := metav1.ObjectMeta{\n\t\tName: namespaceName,\n\t}\n\treturn CreateNamespaceWithMetadataE(t, options, namespaceObject)\n}\n\n// CreateNamespaceWithMetadataE will create a new Kubernetes namespace on the cluster targeted by the provided options and\n// with the provided metadata. This method expects the entire namespace ObjectMeta to be passed in, so you'll need to set the name within the ObjectMeta struct yourself.\nfunc CreateNamespaceWithMetadataE(t testing.TestingT, options *KubectlOptions, namespaceObjectMeta metav1.ObjectMeta) error {\n\tclientset, err := GetKubernetesClientFromOptionsE(t, options)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tnamespace := corev1.Namespace{\n\t\tObjectMeta: namespaceObjectMeta,\n\t}\n\t_, err = clientset.CoreV1().Namespaces().Create(context.Background(), &namespace, metav1.CreateOptions{})\n\treturn err\n}\n\n// CreateNamespaceWithMetadata will create a new Kubernetes namespace on the cluster targeted by the provided options and\n// with the provided metadata. This method expects the entire namespace ObjectMeta to be passed in, so you'll need to set the name within the ObjectMeta struct yourself.\n// This will fail the test if there is an error while creating the namespace.\nfunc CreateNamespaceWithMetadata(t testing.TestingT, options *KubectlOptions, namespaceObjectMeta metav1.ObjectMeta) {\n\trequire.NoError(t, CreateNamespaceWithMetadataE(t, options, namespaceObjectMeta))\n}\n\n// GetNamespace will query the Kubernetes cluster targeted by the provided options for the requested namespace. This will\n// fail the test if there is an error in getting the namespace or if the namespace doesn't exist.\nfunc GetNamespace(t testing.TestingT, options *KubectlOptions, namespaceName string) *corev1.Namespace {\n\tnamespace, err := GetNamespaceE(t, options, namespaceName)\n\trequire.NoError(t, err)\n\trequire.NotNil(t, namespace)\n\treturn namespace\n}\n\n// GetNamespaceE will query the Kubernetes cluster targeted by the provided options for the requested namespace.\nfunc GetNamespaceE(t testing.TestingT, options *KubectlOptions, namespaceName string) (*corev1.Namespace, error) {\n\tclientset, err := GetKubernetesClientFromOptionsE(t, options)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn clientset.CoreV1().Namespaces().Get(context.Background(), namespaceName, metav1.GetOptions{})\n}\n\n// DeleteNamespace will delete the requested namespace from the Kubernetes cluster targeted by the provided options. This will\n// fail the test if there is an error in creating the namespace.\nfunc DeleteNamespace(t testing.TestingT, options *KubectlOptions, namespaceName string) {\n\trequire.NoError(t, DeleteNamespaceE(t, options, namespaceName))\n}\n\n// DeleteNamespaceE will delete the requested namespace from the Kubernetes cluster targeted by the provided options.\nfunc DeleteNamespaceE(t testing.TestingT, options *KubectlOptions, namespaceName string) error {\n\tclientset, err := GetKubernetesClientFromOptionsE(t, options)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn clientset.CoreV1().Namespaces().Delete(context.Background(), namespaceName, metav1.DeleteOptions{})\n}\n\n// ListNamespaces will list all namespaces in the Kubernetes cluster targeted by the provided options.\n// This will fail the test if there is an error in listing the namespaces.\nfunc ListNamespaces(t testing.TestingT, options *KubectlOptions, filters metav1.ListOptions) []corev1.Namespace {\n\tnamespaces, err := ListNamespacesE(t, options, filters)\n\trequire.NoError(t, err)\n\n\tif len(namespaces) > 0 {\n\t\tvar namespaceNames []string\n\t\tfor _, ns := range namespaces {\n\t\t\tnamespaceNames = append(namespaceNames, ns.Name)\n\t\t}\n\t\toptions.Logger.Logf(t, \"Found namespaces: %s\", strings.Join(namespaceNames, \", \"))\n\t} else {\n\t\toptions.Logger.Logf(t, \"No namespaces found matching the provided filters.\")\n\t}\n\n\treturn namespaces\n}\n\n// ListNamespacesE lists all namespaces in the Kubernetes cluster and returns them or an error.\nfunc ListNamespacesE(t testing.TestingT, options *KubectlOptions, filters metav1.ListOptions) ([]corev1.Namespace, error) {\n\tclientset, err := GetKubernetesClientFromOptionsE(t, options)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tnamespaceList, err := clientset.CoreV1().Namespaces().List(context.Background(), filters)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn namespaceList.Items, nil\n}\n"
  },
  {
    "path": "modules/k8s/namespace_test.go",
    "content": "//go:build kubeall || kubernetes\n// +build kubeall kubernetes\n\n// NOTE: we have build tags to differentiate kubernetes tests from non-kubernetes tests. This is done because minikube\n// is heavy and can interfere with docker related tests in terratest. Specifically, many of the tests start to fail with\n// `connection refused` errors from `minikube`. To avoid overloading the system, we run the kubernetes tests and helm\n// tests separately from the others. This may not be necessary if you have a sufficiently powerful machine.  We\n// recommend at least 4 cores and 16GB of RAM if you want to run all the tests together.\n\npackage k8s\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n\tcorev1 \"k8s.io/api/core/v1\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n)\n\nfunc TestNamespaces(t *testing.T) {\n\tt.Parallel()\n\n\tuniqueId := random.UniqueId()\n\tnamespaceName := strings.ToLower(uniqueId)\n\toptions := NewKubectlOptions(\"\", \"\", namespaceName)\n\tCreateNamespace(t, options, namespaceName)\n\tdefer func() {\n\t\tDeleteNamespace(t, options, namespaceName)\n\t\tnamespace := GetNamespace(t, options, namespaceName)\n\t\trequire.Equal(t, namespace.Status.Phase, corev1.NamespaceTerminating)\n\t}()\n\n\tnamespace := GetNamespace(t, options, namespaceName)\n\trequire.Equal(t, namespace.Name, namespaceName)\n}\n\nfunc TestNamespaceWithMetadata(t *testing.T) {\n\tt.Parallel()\n\n\tuniqueId := random.UniqueId()\n\tnamespaceName := strings.ToLower(uniqueId)\n\toptions := NewKubectlOptions(\"\", \"\", namespaceName)\n\tnamespaceLabels := map[string]string{\"foo\": \"bar\"}\n\tnamespaceObjectMetaWithLabels := metav1.ObjectMeta{\n\t\tName:   namespaceName,\n\t\tLabels: namespaceLabels,\n\t}\n\tCreateNamespaceWithMetadata(t, options, namespaceObjectMetaWithLabels)\n\tdefer func() {\n\t\tDeleteNamespace(t, options, namespaceName)\n\t\tnamespace := GetNamespace(t, options, namespaceName)\n\t\trequire.Equal(t, namespace.Status.Phase, corev1.NamespaceTerminating)\n\t}()\n\n\tnamespace := GetNamespace(t, options, namespaceName)\n\trequire.Equal(t, namespace.Name, namespaceName)\n\tfor k, v := range namespaceLabels {\n\t\trequire.Equal(t, v, namespace.Labels[k], \"Expected label %s=%s\", k, v)\n\t}\n}\n\nfunc TestListNamespaces(t *testing.T) {\n\tt.Parallel()\n\n\tuniqueId := random.UniqueId()\n\tnamespaceName := strings.ToLower(uniqueId)\n\toptions := NewKubectlOptions(\"\", \"\", namespaceName)\n\n\tCreateNamespace(t, options, namespaceName)\n\tdefer DeleteNamespace(t, options, namespaceName)\n\n\tt.Run(\"List all namespaces and find the created one\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tnamespaces := ListNamespaces(t, options, metav1.ListOptions{})\n\t\trequire.NotEmpty(t, namespaces, \"Should find at least some namespaces\")\n\n\t\tfound := false\n\t\tfor _, ns := range namespaces {\n\t\t\tif ns.Name == namespaceName {\n\t\t\t\tfound = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\trequire.True(t, found, \"Should find the created namespace in the list\")\n\t})\n}\n"
  },
  {
    "path": "modules/k8s/networkpolicy.go",
    "content": "package k8s\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/gruntwork-io/terratest/modules/retry\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/stretchr/testify/require\"\n\tnetworkingv1 \"k8s.io/api/networking/v1\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n)\n\n// GetNetworkPolicy returns a Kubernetes networkpolicy resource in the provided namespace with the given name. The namespace used\n// is the one provided in the KubectlOptions. This will fail the test if there is an error.\nfunc GetNetworkPolicy(t testing.TestingT, options *KubectlOptions, networkPolicyName string) *networkingv1.NetworkPolicy {\n\tnetworkPolicy, err := GetNetworkPolicyE(t, options, networkPolicyName)\n\trequire.NoError(t, err)\n\treturn networkPolicy\n}\n\n// GetNetworkPolicyE returns a Kubernetes networkpolicy resource in the provided namespace with the given name. The namespace used\n// is the one provided in the KubectlOptions.\nfunc GetNetworkPolicyE(t testing.TestingT, options *KubectlOptions, networkPolicyName string) (*networkingv1.NetworkPolicy, error) {\n\tclientset, err := GetKubernetesClientFromOptionsE(t, options)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn clientset.NetworkingV1().NetworkPolicies(options.Namespace).Get(context.Background(), networkPolicyName, metav1.GetOptions{})\n}\n\n// WaitUntilNetworkPolicyAvailable waits until the networkpolicy is present on the cluster in cases where it is not immediately\n// available (for example, when using ClusterIssuer to request a certificate).\nfunc WaitUntilNetworkPolicyAvailable(t testing.TestingT, options *KubectlOptions, networkPolicyName string, retries int, sleepBetweenRetries time.Duration) {\n\tstatusMsg := fmt.Sprintf(\"Wait for networkpolicy %s to be provisioned.\", networkPolicyName)\n\tmessage := retry.DoWithRetry(\n\t\tt,\n\t\tstatusMsg,\n\t\tretries,\n\t\tsleepBetweenRetries,\n\t\tfunc() (string, error) {\n\t\t\t_, err := GetNetworkPolicyE(t, options, networkPolicyName)\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", err\n\t\t\t}\n\n\t\t\treturn \"networkpolicy is now available\", nil\n\t\t},\n\t)\n\toptions.Logger.Logf(t, \"%s\", message)\n}\n"
  },
  {
    "path": "modules/k8s/networkpolicy_test.go",
    "content": "//go:build kubeall || kubernetes\n// +build kubeall kubernetes\n\n// NOTE: we have build tags to differentiate kubernetes tests from non-kubernetes tests. This is done because minikube\n// is heavy and can interfere with docker related tests in terratest. Specifically, many of the tests start to fail with\n// `connection refused` errors from `minikube`. To avoid overloading the system, we run the kubernetes tests and helm\n// tests separately from the others. This may not be necessary if you have a sufficiently powerful machine.  We\n// recommend at least 4 cores and 16GB of RAM if you want to run all the tests together.\n\npackage k8s\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n)\n\nfunc TestGetNetworkPolicyEReturnsErrorForNonExistantNetworkPolicy(t *testing.T) {\n\tt.Parallel()\n\n\toptions := NewKubectlOptions(\"\", \"\", \"default\")\n\t_, err := GetNetworkPolicyE(t, options, \"test-network-policy\")\n\trequire.Error(t, err)\n}\n\nfunc TestGetNetworkPolicyEReturnsCorrectNetworkPolicyInCorrectNamespace(t *testing.T) {\n\tt.Parallel()\n\n\tuniqueID := strings.ToLower(random.UniqueId())\n\toptions := NewKubectlOptions(\"\", \"\", uniqueID)\n\tconfigData := fmt.Sprintf(EXAMPLE_NETWORK_POLICY_YAML_TEMPLATE, uniqueID, uniqueID)\n\tdefer KubectlDeleteFromString(t, options, configData)\n\tKubectlApplyFromString(t, options, configData)\n\n\tnetworkPolicy := GetNetworkPolicy(t, options, \"test-network-policy\")\n\trequire.Equal(t, networkPolicy.Name, \"test-network-policy\")\n\trequire.Equal(t, networkPolicy.Namespace, uniqueID)\n}\n\nfunc TestWaitUntilNetworkPolicyAvailableReturnsSuccessfully(t *testing.T) {\n\tt.Parallel()\n\n\tuniqueID := strings.ToLower(random.UniqueId())\n\toptions := NewKubectlOptions(\"\", \"\", uniqueID)\n\tconfigData := fmt.Sprintf(EXAMPLE_NETWORK_POLICY_YAML_TEMPLATE, uniqueID, uniqueID)\n\tdefer KubectlDeleteFromString(t, options, configData)\n\n\tKubectlApplyFromString(t, options, configData)\n\tWaitUntilNetworkPolicyAvailable(t, options, \"test-network-policy\", 10, 1*time.Second)\n}\n\nconst EXAMPLE_NETWORK_POLICY_YAML_TEMPLATE = `---\napiVersion: v1\nkind: Namespace\nmetadata:\n  name: %s\n---\napiVersion: networking.k8s.io/v1\nkind: NetworkPolicy\nmetadata:\n  name: test-network-policy\n  namespace: %s\nspec:\n  podSelector: {}\n  policyTypes:\n    - Ingress\n`\n"
  },
  {
    "path": "modules/k8s/node.go",
    "content": "package k8s\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/require\"\n\tcorev1 \"k8s.io/api/core/v1\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\n\t\"github.com/gruntwork-io/terratest/modules/retry\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n)\n\n// GetNodes queries Kubernetes for information about the worker nodes registered to the cluster. If anything goes wrong,\n// the function will automatically fail the test.\nfunc GetNodes(t testing.TestingT, options *KubectlOptions) []corev1.Node {\n\tnodes, err := GetNodesE(t, options)\n\trequire.NoError(t, err)\n\treturn nodes\n}\n\n// GetNodesE queries Kubernetes for information about the worker nodes registered to the cluster.\nfunc GetNodesE(t testing.TestingT, options *KubectlOptions) ([]corev1.Node, error) {\n\treturn GetNodesByFilterE(t, options, metav1.ListOptions{})\n}\n\n// GetNodesByFilterE queries Kubernetes for information about the worker nodes registered to the cluster, filtering the\n// list of nodes using the provided ListOptions.\nfunc GetNodesByFilterE(t testing.TestingT, options *KubectlOptions, filter metav1.ListOptions) ([]corev1.Node, error) {\n\toptions.Logger.Logf(t, \"Getting list of nodes from Kubernetes\")\n\n\tclientset, err := GetKubernetesClientFromOptionsE(t, options)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tnodes, err := clientset.CoreV1().Nodes().List(context.Background(), filter)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn nodes.Items, err\n}\n\n// GetReadyNodes queries Kubernetes for information about the worker nodes registered to the cluster and only returns\n// those that are in the ready state. If anything goes wrong, the function will automatically fail the test.\nfunc GetReadyNodes(t testing.TestingT, options *KubectlOptions) []corev1.Node {\n\tnodes, err := GetReadyNodesE(t, options)\n\trequire.NoError(t, err)\n\treturn nodes\n}\n\n// GetReadyNodesE queries Kubernetes for information about the worker nodes registered to the cluster and only returns\n// those that are in the ready state.\nfunc GetReadyNodesE(t testing.TestingT, options *KubectlOptions) ([]corev1.Node, error) {\n\tnodes, err := GetNodesE(t, options)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\toptions.Logger.Logf(t, \"Filtering list of nodes from Kubernetes for Ready nodes\")\n\tnodesFiltered := []corev1.Node{}\n\tfor _, node := range nodes {\n\t\tif IsNodeReady(node) {\n\t\t\tnodesFiltered = append(nodesFiltered, node)\n\t\t}\n\t}\n\treturn nodesFiltered, nil\n}\n\n// IsNodeReady takes a Kubernetes Node information object and checks if the Node is in the ready state.\nfunc IsNodeReady(node corev1.Node) bool {\n\tfor _, condition := range node.Status.Conditions {\n\t\tif condition.Type == corev1.NodeReady {\n\t\t\treturn condition.Status == corev1.ConditionTrue\n\t\t}\n\t}\n\treturn false\n}\n\n// WaitUntilAllNodesReady continuously polls the Kubernetes cluster until all nodes in the cluster reach the ready\n// state, or runs out of retries. Will fail the test immediately if it times out.\nfunc WaitUntilAllNodesReady(t testing.TestingT, options *KubectlOptions, retries int, sleepBetweenRetries time.Duration) {\n\terr := WaitUntilAllNodesReadyE(t, options, retries, sleepBetweenRetries)\n\trequire.NoError(t, err)\n}\n\n// WaitUntilAllNodesReadyE continuously polls the Kubernetes cluster until all nodes in the cluster reach the ready\n// state, or runs out of retries.\nfunc WaitUntilAllNodesReadyE(t testing.TestingT, options *KubectlOptions, retries int, sleepBetweenRetries time.Duration) error {\n\tmessage, err := retry.DoWithRetryE(\n\t\tt,\n\t\t\"Wait for all Kube Nodes to be ready\",\n\t\tretries,\n\t\tsleepBetweenRetries,\n\t\tfunc() (string, error) {\n\t\t\t_, err := AreAllNodesReadyE(t, options)\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", err\n\t\t\t}\n\t\t\treturn \"All nodes ready\", nil\n\t\t},\n\t)\n\toptions.Logger.Logf(t, \"%s\", message)\n\treturn err\n}\n\n// AreAllNodesReady checks if all nodes are ready in the Kubernetes cluster targeted by the current config context\nfunc AreAllNodesReady(t testing.TestingT, options *KubectlOptions) bool {\n\tnodesReady, _ := AreAllNodesReadyE(t, options)\n\treturn nodesReady\n}\n\n// AreAllNodesReadyE checks if all nodes are ready in the Kubernetes cluster targeted by the current config context. If\n// false, returns an error indicating the reason.\nfunc AreAllNodesReadyE(t testing.TestingT, options *KubectlOptions) (bool, error) {\n\tnodes, err := GetNodesE(t, options)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tif len(nodes) == 0 {\n\t\treturn false, errors.New(\"No nodes available\")\n\t}\n\tfor _, node := range nodes {\n\t\tif !IsNodeReady(node) {\n\t\t\treturn false, errors.New(\"Not all nodes ready\")\n\t\t}\n\t}\n\treturn true, nil\n}\n"
  },
  {
    "path": "modules/k8s/node_test.go",
    "content": "//go:build kubeall || kubernetes\n// +build kubeall kubernetes\n\n// NOTE: we have build tags to differentiate kubernetes tests from non-kubernetes tests. This is done because minikube\n// is heavy and can interfere with docker related tests in terratest. Specifically, many of the tests start to fail with\n// `connection refused` errors from `minikube`. To avoid overloading the system, we run the kubernetes tests and helm\n// tests separately from the others. This may not be necessary if you have a sufficiently powerful machine.  We\n// recommend at least 4 cores and 16GB of RAM if you want to run all the tests together.\n\npackage k8s\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// Tests that:\n// - kubectl is properly configured to talk to a kubernetes cluster\n// - GetNodes will return a list of nodes registered with kubernetes\nfunc TestGetNodes(t *testing.T) {\n\tt.Parallel()\n\n\t// Assumes local kubernetes (minikube or docker-for-desktop kube), where there is only one node\n\toptions := NewKubectlOptions(\"\", \"\", \"default\")\n\tnodes := GetNodes(t, options)\n\trequire.Equal(t, len(nodes), 1)\n\n\tnode := nodes[0]\n\t// Make sure node name is not blank, indicating an uninitialized Node object\n\tassert.NotEqual(t, node.Name, \"\")\n}\n\n// Tests that:\n// - kubectl is properly configured to talk to a kubernetes cluster\n// - GetReadyNodes will return a list of ready nodes registered with kubernetes\nfunc TestGetReadyNodes(t *testing.T) {\n\tt.Parallel()\n\n\t// Assumes local kubernetes (minikube or docker-for-desktop kube), where there is only one node\n\toptions := NewKubectlOptions(\"\", \"\", \"default\")\n\tnodes := GetReadyNodes(t, options)\n\trequire.Equal(t, len(nodes), 1)\n\n\tnode := nodes[0]\n\t// Make sure node name is not blank, indicating an uninitialized Node object\n\tassert.NotEqual(t, node.Name, \"\")\n}\n\n// Tests that:\n// - kubectl is properly configured to talk to a kubernetes cluster\n// - WaitUntilAllNodesReady checks if all nodes in the cluster are ready\nfunc TestWaitUntilAllNodesReady(t *testing.T) {\n\tt.Parallel()\n\n\toptions := NewKubectlOptions(\"\", \"\", \"default\")\n\n\tWaitUntilAllNodesReady(t, options, 12, 5*time.Second)\n\n\tnodes := GetNodes(t, options)\n\tnodeNames := map[string]bool{}\n\tfor _, node := range nodes {\n\t\tnodeNames[node.Name] = true\n\t}\n\n\treadyNodes := GetReadyNodes(t, options)\n\treadyNodeNames := map[string]bool{}\n\tfor _, node := range readyNodes {\n\t\treadyNodeNames[node.Name] = true\n\t}\n\n\tassert.Equal(t, nodeNames, readyNodeNames)\n}\n"
  },
  {
    "path": "modules/k8s/persistent_volume.go",
    "content": "package k8s\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/require\"\n\n\tcorev1 \"k8s.io/api/core/v1\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\n\t\"github.com/gruntwork-io/terratest/modules/retry\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n)\n\n// ListPersistentVolumes will look for PersistentVolumes in the given namespace that match the given filters and return them. This will fail the\n// test if there is an error.\nfunc ListPersistentVolumes(t testing.TestingT, options *KubectlOptions, filters metav1.ListOptions) []corev1.PersistentVolume {\n\tpvs, err := ListPersistentVolumesE(t, options, filters)\n\trequire.NoError(t, err)\n\treturn pvs\n}\n\n// ListPersistentVolumesE will look for PersistentVolumes that match the given filters and return them.\nfunc ListPersistentVolumesE(t testing.TestingT, options *KubectlOptions, filters metav1.ListOptions) ([]corev1.PersistentVolume, error) {\n\tclientset, err := GetKubernetesClientFromOptionsE(t, options)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresp, err := clientset.CoreV1().PersistentVolumes().List(context.Background(), filters)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn resp.Items, nil\n}\n\n// GetPersistentVolume returns a Kubernetes PersistentVolume resource with the given name. This will fail the test if there is an error.\nfunc GetPersistentVolume(t testing.TestingT, options *KubectlOptions, name string) *corev1.PersistentVolume {\n\tpv, err := GetPersistentVolumeE(t, options, name)\n\trequire.NoError(t, err)\n\treturn pv\n}\n\n// GetPersistentVolumeE returns a Kubernetes PersistentVolume resource with the given name.\nfunc GetPersistentVolumeE(t testing.TestingT, options *KubectlOptions, name string) (*corev1.PersistentVolume, error) {\n\tclientset, err := GetKubernetesClientFromOptionsE(t, options)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn clientset.CoreV1().PersistentVolumes().Get(context.Background(), name, metav1.GetOptions{})\n}\n\n// WaitUntilPersistentVolumeInStatus waits until the given Persistent Volume is the given status phase,\n// retrying the check for the specified amount of times, sleeping\n// for the provided duration between each try.\n// This will fail the test if there is an error.\nfunc WaitUntilPersistentVolumeInStatus(t testing.TestingT, options *KubectlOptions, pvName string, pvStatusPhase *corev1.PersistentVolumePhase, retries int, sleepBetweenRetries time.Duration) {\n\trequire.NoError(t, WaitUntilPersistentVolumeInStatusE(t, options, pvName, pvStatusPhase, retries, sleepBetweenRetries))\n}\n\n// WaitUntilPersistentVolumeInStatusE waits until the given PersistentVolume is in the given status phase,\n// retrying the check for the specified amount of times, sleeping\n// for the provided duration between each try.\nfunc WaitUntilPersistentVolumeInStatusE(\n\tt testing.TestingT,\n\toptions *KubectlOptions,\n\tpvName string,\n\tpvStatusPhase *corev1.PersistentVolumePhase,\n\tretries int,\n\tsleepBetweenRetries time.Duration,\n) error {\n\tstatusMsg := fmt.Sprintf(\"Wait for Persistent Volume %s to be '%s'\", pvName, *pvStatusPhase)\n\tmessage, err := retry.DoWithRetryE(\n\t\tt,\n\t\tstatusMsg,\n\t\tretries,\n\t\tsleepBetweenRetries,\n\t\tfunc() (string, error) {\n\t\t\tpv, err := GetPersistentVolumeE(t, options, pvName)\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", err\n\t\t\t}\n\t\t\tif !IsPersistentVolumeInStatus(pv, pvStatusPhase) {\n\t\t\t\treturn \"\", NewPersistentVolumeNotInStatusError(pv, pvStatusPhase)\n\t\t\t}\n\t\t\treturn fmt.Sprintf(\"Persistent Volume is now '%s'\", *pvStatusPhase), nil\n\t\t},\n\t)\n\tif err != nil {\n\t\toptions.Logger.Logf(t, \"Timeout waiting for PersistentVolume to be '%s': %s\", *pvStatusPhase, err)\n\t\treturn err\n\t}\n\toptions.Logger.Logf(t, \"%s\", message)\n\treturn nil\n}\n\n// IsPersistentVolumeInStatus returns true if the given PersistentVolume is in the given status phase\nfunc IsPersistentVolumeInStatus(pv *corev1.PersistentVolume, pvStatusPhase *corev1.PersistentVolumePhase) bool {\n\treturn pv != nil && pv.Status.Phase == *pvStatusPhase\n}\n"
  },
  {
    "path": "modules/k8s/persistent_volume_claim.go",
    "content": "package k8s\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/require\"\n\n\tcorev1 \"k8s.io/api/core/v1\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\n\t\"github.com/gruntwork-io/terratest/modules/logger\"\n\t\"github.com/gruntwork-io/terratest/modules/retry\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n)\n\n// ListPersistentVolumeClaims will look for PersistentVolumeClaims in the given namespace that match the given filters and return them. This will fail the\n// test if there is an error.\nfunc ListPersistentVolumeClaims(t testing.TestingT, options *KubectlOptions, filters metav1.ListOptions) []corev1.PersistentVolumeClaim {\n\tpvcs, err := ListPersistentVolumeClaimsE(t, options, filters)\n\trequire.NoError(t, err)\n\treturn pvcs\n}\n\n// ListPersistentVolumeClaimsE will look for PersistentVolumeClaims in the given namespace that match the given filters and return them.\nfunc ListPersistentVolumeClaimsE(t testing.TestingT, options *KubectlOptions, filters metav1.ListOptions) ([]corev1.PersistentVolumeClaim, error) {\n\tclientset, err := GetKubernetesClientFromOptionsE(t, options)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresp, err := clientset.CoreV1().PersistentVolumeClaims(options.Namespace).List(context.Background(), filters)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn resp.Items, nil\n}\n\n// GetPersistentVolumeClaim returns a Kubernetes PersistentVolumeClaim resource in the provided namespace with the given name. This will\n// fail the test if there is an error.\nfunc GetPersistentVolumeClaim(t testing.TestingT, options *KubectlOptions, pvcName string) *corev1.PersistentVolumeClaim {\n\tpvc, err := GetPersistentVolumeClaimE(t, options, pvcName)\n\trequire.NoError(t, err)\n\treturn pvc\n}\n\n// GetPersistentVolumeClaimE returns a Kubernetes PersistentVolumeClaim resource in the provided namespace with the given name.\nfunc GetPersistentVolumeClaimE(t testing.TestingT, options *KubectlOptions, pvcName string) (*corev1.PersistentVolumeClaim, error) {\n\tclientset, err := GetKubernetesClientFromOptionsE(t, options)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn clientset.CoreV1().PersistentVolumeClaims(options.Namespace).Get(context.Background(), pvcName, metav1.GetOptions{})\n}\n\n// WaitUntilPersistentVolumeClaimInStatus waits until the given PersistentVolumeClaim is the given status phase,\n// retrying the check for the specified amount of times, sleeping\n// for the provided duration between each try.\n// This will fail the test if there is an error.\nfunc WaitUntilPersistentVolumeClaimInStatus(t testing.TestingT, options *KubectlOptions, pvcName string, pvcStatusPhase *corev1.PersistentVolumeClaimPhase, retries int, sleepBetweenRetries time.Duration) {\n\trequire.NoError(t, WaitUntilPersistentVolumeClaimInStatusE(t, options, pvcName, pvcStatusPhase, retries, sleepBetweenRetries))\n}\n\n// WaitUntilPersistentVolumeClaimInStatusE waits until the given PersistentVolumeClaim is the given status phase,\n// retrying the check for the specified amount of times, sleeping\n// for the provided duration between each try.\n// This will fail the test if there is an error.\nfunc WaitUntilPersistentVolumeClaimInStatusE(t testing.TestingT, options *KubectlOptions, pvcName string, pvcStatusPhase *corev1.PersistentVolumeClaimPhase, retries int, sleepBetweenRetries time.Duration) error {\n\tstatusMsg := fmt.Sprintf(\"Wait for PersistentVolumeClaim %s to be '%s'.\", pvcName, *pvcStatusPhase)\n\tmessage, err := retry.DoWithRetryE(\n\t\tt,\n\t\tstatusMsg,\n\t\tretries,\n\t\tsleepBetweenRetries,\n\t\tfunc() (string, error) {\n\t\t\tpvc, err := GetPersistentVolumeClaimE(t, options, pvcName)\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", err\n\t\t\t}\n\t\t\tif !IsPersistentVolumeClaimInStatus(pvc, pvcStatusPhase) {\n\t\t\t\treturn \"\", NewPersistentVolumeClaimNotInStatusError(pvc, pvcStatusPhase)\n\t\t\t}\n\t\t\treturn fmt.Sprintf(\"PersistentVolumeClaim is now '%s'\", *pvcStatusPhase), nil\n\t\t},\n\t)\n\tif err != nil {\n\t\tlogger.Default.Logf(t, \"Timeout waiting for PersistentVolumeClaim to be '%s': %s\", *pvcStatusPhase, err)\n\t\treturn err\n\t}\n\tlogger.Default.Logf(t, \"%s\", message)\n\treturn nil\n}\n\n// IsPersistentVolumeClaimInStatus returns true if the given PersistentVolumeClaim is in the given status phase\nfunc IsPersistentVolumeClaimInStatus(pvc *corev1.PersistentVolumeClaim, pvcStatusPhase *corev1.PersistentVolumeClaimPhase) bool {\n\treturn pvc != nil && pvc.Status.Phase == *pvcStatusPhase\n}\n"
  },
  {
    "path": "modules/k8s/persistent_volume_claim_test.go",
    "content": "//go:build kubeall || kubernetes\n// +build kubeall kubernetes\n\n// NOTE: we have build tags to differentiate kubernetes tests from non-kubernetes tests. This is done because minikube\n// is heavy and can interfere with docker related tests in terratest. Specifically, many of the tests start to fail with\n// `connection refused` errors from `minikube`. To avoid overloading the system, we run the kubernetes tests and helm\n// tests separately from the others. This may not be necessary if you have a sufficiently powerful machine.  We\n// recommend at least 4 cores and 16GB of RAM if you want to run all the tests together.\n\npackage k8s\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/require\"\n\n\tcorev1 \"k8s.io/api/core/v1\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\t_ \"k8s.io/client-go/plugin/pkg/client/auth\"\n\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n)\n\nfunc TestListPersistentVolumeClaimsReturnsPersistentVolumeClaimsInNamespace(t *testing.T) {\n\tt.Parallel()\n\n\tpvcName := \"test-dummy-pvc\"\n\tnamespace := strings.ToLower(random.UniqueId())\n\toptions := NewKubectlOptions(\"\", \"\", namespace)\n\tconfigData := renderFixtureYamlTemplate(namespace, pvcName)\n\tdefer KubectlDeleteFromString(t, options, configData)\n\tKubectlApplyFromString(t, options, configData)\n\n\tpvcs := ListPersistentVolumeClaims(t, options, metav1.ListOptions{})\n\trequire.Equal(t, len(pvcs), 1)\n\tpvc := pvcs[0]\n\trequire.Equal(t, pvc.Name, pvcName)\n\trequire.Equal(t, pvc.Namespace, namespace)\n}\n\nfunc TestListPersistentVolumeClaimsReturnsZeroPersistentVolumeClaimsIfNoneCreated(t *testing.T) {\n\tt.Parallel()\n\n\tnamespace := strings.ToLower(random.UniqueId())\n\toptions := NewKubectlOptions(\"\", \"\", namespace)\n\tCreateNamespace(t, options, namespace)\n\tdefer DeleteNamespace(t, options, namespace)\n\n\tpvcs := ListPersistentVolumeClaims(t, options, metav1.ListOptions{})\n\trequire.Equal(t, len(pvcs), 0)\n}\n\nfunc TestGetPersistentVolumeClaimEReturnsErrorForNonExistantPersistentVolumeClaim(t *testing.T) {\n\tt.Parallel()\n\n\toptions := NewKubectlOptions(\"\", \"\", \"default\")\n\t_, err := GetPersistentVolumeClaimE(t, options, \"non-existent\")\n\trequire.Error(t, err)\n}\n\nfunc TestGetPersistentVolumeClaimReturnsCorrectPersistentVolumeClaimInCorrectNamespace(t *testing.T) {\n\tt.Parallel()\n\n\tpvcName := \"test-dummy-pvc\"\n\tnamespace := strings.ToLower(random.UniqueId())\n\toptions := NewKubectlOptions(\"\", \"\", namespace)\n\tconfigData := renderFixtureYamlTemplate(namespace, pvcName)\n\tdefer KubectlDeleteFromString(t, options, configData)\n\tKubectlApplyFromString(t, options, configData)\n\n\tpvc := GetPersistentVolumeClaim(t, options, pvcName)\n\trequire.Equal(t, pvc.Name, pvcName)\n\trequire.Equal(t, pvc.Namespace, namespace)\n}\n\nfunc TestWaitUntilPersistentVolumeClaimInGivenStatusPhase(t *testing.T) {\n\tt.Parallel()\n\n\tpvcName := \"test-dummy-pvc\"\n\tnamespace := strings.ToLower(random.UniqueId())\n\tpvcBoundStatusPhase := corev1.ClaimBound\n\toptions := NewKubectlOptions(\"\", \"\", namespace)\n\tconfigData := renderFixtureYamlTemplate(namespace, pvcName)\n\tdefer KubectlDeleteFromString(t, options, configData)\n\tKubectlApplyFromString(t, options, configData)\n\n\tWaitUntilPersistentVolumeClaimInStatus(t, options, pvcName, &pvcBoundStatusPhase, 60, 1*time.Second)\n}\n\nfunc TestWaitUntilPersistentVolumeClaimInStatusEReturnsErrorWhenWaitingForAnUnexistentPvc(t *testing.T) {\n\tt.Parallel()\n\n\tpvcBoundStatusPhase := corev1.ClaimBound\n\toptions := NewKubectlOptions(\"\", \"\", \"default\")\n\terr := WaitUntilPersistentVolumeClaimInStatusE(t, options, \"non-existent\", &pvcBoundStatusPhase, 3, 1*time.Second)\n\trequire.NotEqual(t, err, nil)\n}\n\nfunc TestWaitUntilPersistentVolumeClaimInStatusEReturnsErrorWhenTimesOut(t *testing.T) {\n\tt.Parallel()\n\n\tpvcName := \"test-dummy-pvc\"\n\tpvcLostStatusPhase := corev1.ClaimLost\n\tnamespace := strings.ToLower(random.UniqueId())\n\toptions := NewKubectlOptions(\"\", \"\", namespace)\n\tconfigData := renderFixtureYamlTemplate(namespace, pvcName)\n\tdefer KubectlDeleteFromString(t, options, configData)\n\tKubectlApplyFromString(t, options, configData)\n\n\terr := WaitUntilPersistentVolumeClaimInStatusE(t, options, pvcName, &pvcLostStatusPhase, 5, 1*time.Second)\n\trequire.NotEqual(t, err, nil)\n}\n\nfunc TestIsPersistentVolumeClaimInStatusReturnsFalseIfPvcIsNil(t *testing.T) {\n\tt.Parallel()\n\n\tresult := IsPersistentVolumeClaimInStatus(nil, nil)\n\trequire.Equal(t, result, false)\n}\n\nconst pvcFixtureYamlTemplate = `---\napiVersion: v1\nkind: Namespace\nmetadata:\n  name: __namespace__\n---\napiVersion: v1\nkind: PersistentVolume\nmetadata:\n  name: __namespace__\nspec:\n  capacity:\n    storage: 10Mi\n  accessModes:\n    - ReadWriteOnce\n  hostPath:\n    path: \"/tmp/__namespace__\"\n---\napiVersion: v1\nkind: PersistentVolumeClaim\nmetadata:\n    namespace: __namespace__\n    name: __pvcName__\nspec:\n    accessModes:\n        - ReadWriteOnce\n    resources:\n        requests:\n            storage: 10Mi\n---\napiVersion: v1\nkind: Pod\nmetadata:\n  name: test-pvc-pod\n  namespace: __namespace__\nspec:\n  volumes:\n    - name: test-pvc-volume\n      persistentVolumeClaim:\n        claimName: __pvcName__\n  containers:\n    - name: test-pvc-image\n      image: nginx\n      volumeMounts:\n        - mountPath: \"/tmp/foo\"\n          name: test-pvc-volume\n`\n\nfunc renderFixtureYamlTemplate(namespace, pvcName string) string {\n\treturn strings.Replace(strings.Replace(pvcFixtureYamlTemplate, \"__namespace__\", namespace, -1), \"__pvcName__\", pvcName, -1)\n}\n"
  },
  {
    "path": "modules/k8s/persistent_volume_test.go",
    "content": "//go:build kubeall || kubernetes\n// +build kubeall kubernetes\n\n// NOTE: we have build tags to differentiate kubernetes tests from non-kubernetes tests. This is done because minikube\n// is heavy and can interfere with docker related tests in terratest. Specifically, many of the tests start to fail with\n// `connection refused` errors from `minikube`. To avoid overloading the system, we run the kubernetes tests and helm\n// tests separately from the others. This may not be necessary if you have a sufficiently powerful machine.  We\n// recommend at least 4 cores and 16GB of RAM if you want to run all the tests together.\n\npackage k8s\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/require\"\n\n\tcorev1 \"k8s.io/api/core/v1\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\t_ \"k8s.io/client-go/plugin/pkg/client/auth\"\n\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n)\n\nfunc TestListPersistentVolumesReturnsAllPersistentVolumes(t *testing.T) {\n\tt.Parallel()\n\n\tnumPvFound := 0\n\tpvNames := map[string]struct{}{\n\t\tstrings.ToLower(random.UniqueId()): {},\n\t\tstrings.ToLower(random.UniqueId()): {},\n\t\tstrings.ToLower(random.UniqueId()): {},\n\t}\n\n\toptions := NewKubectlOptions(\"\", \"\", \"\")\n\tfor pvName := range pvNames {\n\t\tpv := fmt.Sprintf(PvFixtureYamlTemplate, pvName, pvName)\n\t\tdefer KubectlDeleteFromString(t, options, pv)\n\t\tKubectlApplyFromString(t, options, pv)\n\t}\n\n\tpvs := ListPersistentVolumes(t, options, metav1.ListOptions{})\n\tfor _, pv := range pvs {\n\t\tif _, ok := pvNames[pv.Name]; ok {\n\t\t\tnumPvFound++\n\t\t}\n\t}\n\n\trequire.Equal(t, numPvFound, len(pvNames))\n}\n\nfunc TestListPersistentVolumesReturnsZeroPersistentVolumesIfNoneCreated(t *testing.T) {\n\tt.Parallel()\n\n\toptions := NewKubectlOptions(\"\", \"\", \"\")\n\tpvs := ListPersistentVolumes(t, options, metav1.ListOptions{})\n\trequire.Equal(t, 0, len(pvs))\n}\n\nfunc TestGetPersistentVolumeEReturnsErrorForNonExistentPersistentVolumes(t *testing.T) {\n\tt.Parallel()\n\n\toptions := NewKubectlOptions(\"\", \"\", \"\")\n\t_, err := GetPersistentVolumeE(t, options, \"non-existent\")\n\trequire.Error(t, err)\n}\n\nfunc TestGetPersistentVolumeReturnsCorrectPersistentVolume(t *testing.T) {\n\tt.Parallel()\n\n\tpvName := strings.ToLower(random.UniqueId())\n\toptions := NewKubectlOptions(\"\", \"\", \"\")\n\tconfigData := fmt.Sprintf(PvFixtureYamlTemplate, pvName, pvName)\n\tdefer KubectlDeleteFromString(t, options, configData)\n\tKubectlApplyFromString(t, options, configData)\n\n\tpv := GetPersistentVolume(t, options, pvName)\n\trequire.Equal(t, pv.Name, pvName)\n}\n\nfunc TestWaitUntilPersistentVolumeInTheGivenStatusPhase(t *testing.T) {\n\tt.Parallel()\n\n\tpvName := strings.ToLower(random.UniqueId())\n\tpvAvailableStatusPhase := corev1.VolumeAvailable\n\n\toptions := NewKubectlOptions(\"\", \"\", pvName)\n\tconfigData := fmt.Sprintf(PvFixtureYamlTemplate, pvName, pvName)\n\tKubectlApplyFromString(t, options, configData)\n\tdefer KubectlDeleteFromString(t, options, configData)\n\n\tWaitUntilPersistentVolumeInStatus(t, options, pvName, &pvAvailableStatusPhase, 60, 1*time.Second)\n}\n\nconst PvFixtureYamlTemplate = `---\napiVersion: v1\nkind: PersistentVolume\nmetadata:\n  name: %s\nspec:\n  capacity:\n    storage: 10Mi\n  accessModes:\n    - ReadWriteOnce\n  hostPath:\n    path: \"/tmp/%s\"\n`\n"
  },
  {
    "path": "modules/k8s/pod.go",
    "content": "package k8s\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/require\"\n\tcorev1 \"k8s.io/api/core/v1\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\n\t\"github.com/gruntwork-io/terratest/modules/retry\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n)\n\n// ListPods will look for pods in the given namespace that match the given filters and return them. This will fail the\n// test if there is an error.\nfunc ListPods(t testing.TestingT, options *KubectlOptions, filters metav1.ListOptions) []corev1.Pod {\n\tpods, err := ListPodsE(t, options, filters)\n\trequire.NoError(t, err)\n\treturn pods\n}\n\n// ListPodsE will look for pods in the given namespace that match the given filters and return them.\nfunc ListPodsE(t testing.TestingT, options *KubectlOptions, filters metav1.ListOptions) ([]corev1.Pod, error) {\n\tclientset, err := GetKubernetesClientFromOptionsE(t, options)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresp, err := clientset.CoreV1().Pods(options.Namespace).List(context.Background(), filters)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn resp.Items, nil\n}\n\n// GetPod returns a Kubernetes pod resource in the provided namespace with the given name. This will\n// fail the test if there is an error.\nfunc GetPod(t testing.TestingT, options *KubectlOptions, podName string) *corev1.Pod {\n\tpod, err := GetPodE(t, options, podName)\n\trequire.NoError(t, err)\n\treturn pod\n}\n\n// GetPodE returns a Kubernetes pod resource in the provided namespace with the given name.\nfunc GetPodE(t testing.TestingT, options *KubectlOptions, podName string) (*corev1.Pod, error) {\n\tclientset, err := GetKubernetesClientFromOptionsE(t, options)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn clientset.CoreV1().Pods(options.Namespace).Get(context.Background(), podName, metav1.GetOptions{})\n}\n\n// WaitUntilNumPodsCreated waits until the desired number of pods are created that match the provided filter. This will\n// retry the check for the specified amount of times, sleeping for the provided duration between each try. This will\n// fail the test if the retry times out.\nfunc WaitUntilNumPodsCreated(\n\tt testing.TestingT,\n\toptions *KubectlOptions,\n\tfilters metav1.ListOptions,\n\tdesiredCount int,\n\tretries int,\n\tsleepBetweenRetries time.Duration,\n) {\n\trequire.NoError(t, WaitUntilNumPodsCreatedE(t, options, filters, desiredCount, retries, sleepBetweenRetries))\n}\n\n// WaitUntilNumPodsCreatedE waits until the desired number of pods are created that match the provided filter. This will\n// retry the check for the specified amount of times, sleeping for the provided duration between each try.\nfunc WaitUntilNumPodsCreatedE(\n\tt testing.TestingT,\n\toptions *KubectlOptions,\n\tfilters metav1.ListOptions,\n\tdesiredCount int,\n\tretries int,\n\tsleepBetweenRetries time.Duration,\n) error {\n\tstatusMsg := fmt.Sprintf(\"Wait for num pods created to match desired count %d.\", desiredCount)\n\tmessage, err := retry.DoWithRetryE(\n\t\tt,\n\t\tstatusMsg,\n\t\tretries,\n\t\tsleepBetweenRetries,\n\t\tfunc() (string, error) {\n\t\t\tpods, err := ListPodsE(t, options, filters)\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", err\n\t\t\t}\n\t\t\tif len(pods) != desiredCount {\n\t\t\t\treturn \"\", DesiredNumberOfPodsNotCreated{Filter: filters, DesiredCount: desiredCount}\n\t\t\t}\n\t\t\treturn \"Desired number of Pods created\", nil\n\t\t},\n\t)\n\tif err != nil {\n\t\toptions.Logger.Logf(t, \"Timedout waiting for the desired number of Pods to be created: %s\", err)\n\t\treturn err\n\t}\n\toptions.Logger.Logf(t, \"%s\", message)\n\treturn nil\n}\n\n// WaitUntilPodAvailable waits until all of the containers within the pod are ready and started, retrying the check for the specified amount of times, sleeping\n// for the provided duration between each try. This will fail the test if there is an error or if the check times out.\nfunc WaitUntilPodAvailable(t testing.TestingT, options *KubectlOptions, podName string, retries int, sleepBetweenRetries time.Duration) {\n\trequire.NoError(t, WaitUntilPodAvailableE(t, options, podName, retries, sleepBetweenRetries))\n}\n\n// WaitUntilPodAvailableE waits until all of the containers within the pod are ready and started, retrying the check for the specified amount of times, sleeping\n// for the provided duration between each try.\nfunc WaitUntilPodAvailableE(t testing.TestingT, options *KubectlOptions, podName string, retries int, sleepBetweenRetries time.Duration) error {\n\tstatusMsg := fmt.Sprintf(\"Wait for pod %s to be provisioned.\", podName)\n\tmessage, err := retry.DoWithRetryE(\n\t\tt,\n\t\tstatusMsg,\n\t\tretries,\n\t\tsleepBetweenRetries,\n\t\tfunc() (string, error) {\n\t\t\tpod, err := GetPodE(t, options, podName)\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", err\n\t\t\t}\n\t\t\tif !IsPodAvailable(pod) {\n\t\t\t\treturn \"\", NewPodNotAvailableError(pod)\n\t\t\t}\n\t\t\treturn \"Pod is now available\", nil\n\t\t},\n\t)\n\tif err != nil {\n\t\toptions.Logger.Logf(t, \"Timedout waiting for Pod to be provisioned: %s\", err)\n\t\treturn err\n\t}\n\toptions.Logger.Logf(t, \"%s\", message)\n\treturn nil\n}\n\n// IsPodAvailable returns true if the all of the containers within the pod are ready and started\nfunc IsPodAvailable(pod *corev1.Pod) bool {\n\t// Ensure all containers have reported their status\n\tif len(pod.Status.ContainerStatuses) != len(pod.Spec.Containers) {\n\t\treturn false\n\t}\n\tfor _, containerStatus := range pod.Status.ContainerStatuses {\n\t\tisContainerStarted := containerStatus.Started\n\t\tisContainerReady := containerStatus.Ready\n\n\t\tif !isContainerReady || (isContainerStarted != nil && *isContainerStarted == false) {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn pod.Status.Phase == corev1.PodRunning\n}\n\n// GetPodLogsE returns the logs of a Pod at the time when the function was called. Pass container name if there are more containers in the Pod or set to \"\" if there is only one.\n// If the Pod is not running an Error is returned.\n// If the provided containerName is not the name of a container in the Pod an Error is returned.\nfunc GetPodLogsE(t testing.TestingT, options *KubectlOptions, pod *corev1.Pod, containerName string) (string, error) {\n\tvar output string\n\tvar err error\n\tif containerName == \"\" {\n\t\toutput, err = RunKubectlAndGetOutputE(t, options, \"logs\", pod.Name)\n\t} else {\n\t\toutput, err = RunKubectlAndGetOutputE(t, options, \"logs\", pod.Name, fmt.Sprintf(\"-c%s\", containerName))\n\t}\n\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn output, nil\n}\n\n// GetPodLogs returns the logs of a Pod at the time when the function was called.  Pass container name if there are more containers in the Pod or set to \"\" if there is only one.\nfunc GetPodLogs(t testing.TestingT, options *KubectlOptions, pod *corev1.Pod, containerName string) string {\n\tlogs, err := GetPodLogsE(t, options, pod, containerName)\n\trequire.NoError(t, err)\n\treturn logs\n}\n\n// ExecPod executes a command in a container within a Kubernetes pod and returns the output. This will fail the test if\n// there is an error. Set containerName to \"\" if there is only one container in the pod.\nfunc ExecPod(t testing.TestingT, options *KubectlOptions, podName string, containerName string, command ...string) string {\n\to, err := ExecPodE(t, options, podName, containerName, command...)\n\trequire.NoError(t, err)\n\treturn o\n}\n\n// ExecPodE executes a command in a container within a Kubernetes pod and returns the output. Set containerName to \"\" if\n// there is only one container in the pod.\nfunc ExecPodE(t testing.TestingT, options *KubectlOptions, podName string, containerName string, command ...string) (string, error) {\n\tvar args []string\n\tif containerName == \"\" {\n\t\targs = append([]string{\"exec\", podName, \"--\"}, command...)\n\t} else {\n\t\targs = append([]string{\"exec\", podName, fmt.Sprintf(\"-c%s\", containerName), \"--\"}, command...)\n\t}\n\treturn RunKubectlAndGetOutputE(t, options, args...)\n}\n"
  },
  {
    "path": "modules/k8s/pod_test.go",
    "content": "//go:build kubeall || kubernetes\n// +build kubeall kubernetes\n\n// NOTE: we have build tags to differentiate kubernetes tests from non-kubernetes tests. This is done because minikube\n// is heavy and can interfere with docker related tests in terratest. Specifically, many of the tests start to fail with\n// `connection refused` errors from `minikube`. To avoid overloading the system, we run the kubernetes tests and helm\n// tests separately from the others. This may not be necessary if you have a sufficiently powerful machine.  We\n// recommend at least 4 cores and 16GB of RAM if you want to run all the tests together.\n\npackage k8s\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/require\"\n\tcorev1 \"k8s.io/api/core/v1\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n)\n\nfunc TestListPodsReturnsPodsInNamespace(t *testing.T) {\n\tt.Parallel()\n\n\tuniqueID := strings.ToLower(random.UniqueId())\n\toptions := NewKubectlOptions(\"\", \"\", uniqueID)\n\tconfigData := fmt.Sprintf(EXAMPLE_POD_YAML_TEMPLATE, uniqueID, uniqueID)\n\tdefer KubectlDeleteFromString(t, options, configData)\n\tKubectlApplyFromString(t, options, configData)\n\n\tpods := ListPods(t, options, metav1.ListOptions{})\n\trequire.Equal(t, len(pods), 1)\n\tpod := pods[0]\n\trequire.Equal(t, pod.Name, \"nginx-pod\")\n\trequire.Equal(t, pod.Namespace, uniqueID)\n}\n\nfunc TestGetPodEReturnsErrorForNonExistantPod(t *testing.T) {\n\tt.Parallel()\n\n\toptions := NewKubectlOptions(\"\", \"\", \"default\")\n\t_, err := GetPodE(t, options, \"nginx-pod\")\n\trequire.Error(t, err)\n}\n\nfunc TestGetPodEReturnsCorrectPodInCorrectNamespace(t *testing.T) {\n\tt.Parallel()\n\n\tuniqueID := strings.ToLower(random.UniqueId())\n\toptions := NewKubectlOptions(\"\", \"\", uniqueID)\n\tconfigData := fmt.Sprintf(EXAMPLE_POD_YAML_TEMPLATE, uniqueID, uniqueID)\n\tdefer KubectlDeleteFromString(t, options, configData)\n\tKubectlApplyFromString(t, options, configData)\n\n\tpod := GetPod(t, options, \"nginx-pod\")\n\trequire.Equal(t, pod.Name, \"nginx-pod\")\n\trequire.Equal(t, pod.Namespace, uniqueID)\n}\n\nfunc TestWaitUntilNumPodsCreatedReturnsSuccessfully(t *testing.T) {\n\tt.Parallel()\n\n\tuniqueID := strings.ToLower(random.UniqueId())\n\toptions := NewKubectlOptions(\"\", \"\", uniqueID)\n\tconfigData := fmt.Sprintf(EXAMPLE_POD_YAML_TEMPLATE, uniqueID, uniqueID)\n\tdefer KubectlDeleteFromString(t, options, configData)\n\tKubectlApplyFromString(t, options, configData)\n\n\tWaitUntilNumPodsCreated(t, options, metav1.ListOptions{}, 1, 60, 1*time.Second)\n}\n\nfunc TestWaitUntilPodAvailableReturnsSuccessfully(t *testing.T) {\n\tt.Parallel()\n\n\tuniqueID := strings.ToLower(random.UniqueId())\n\toptions := NewKubectlOptions(\"\", \"\", uniqueID)\n\tconfigData := fmt.Sprintf(EXAMPLE_POD_YAML_TEMPLATE, uniqueID, uniqueID)\n\tdefer KubectlDeleteFromString(t, options, configData)\n\tKubectlApplyFromString(t, options, configData)\n\n\tWaitUntilPodAvailable(t, options, \"nginx-pod\", 60, 1*time.Second)\n}\n\nfunc TestWaitUntilPodWithMultipleContainersAvailableReturnsSuccessfully(t *testing.T) {\n\tt.Parallel()\n\n\tuniqueID := strings.ToLower(random.UniqueId())\n\toptions := NewKubectlOptions(\"\", \"\", uniqueID)\n\tconfigData := fmt.Sprintf(EXAMPLE_POD_WITH_MULTIPLE_CONTAINERS_YAML_TEMPLATE, uniqueID, uniqueID)\n\tdefer KubectlDeleteFromString(t, options, configData)\n\tKubectlApplyFromString(t, options, configData)\n\n\tWaitUntilPodAvailable(t, options, \"nginx-pod\", 60, 1*time.Second)\n}\n\nfunc TestWaitUntilPodAvailableWithReadinessProbe(t *testing.T) {\n\tt.Parallel()\n\n\tuniqueID := strings.ToLower(random.UniqueId())\n\toptions := NewKubectlOptions(\"\", \"\", uniqueID)\n\tconfigData := fmt.Sprintf(EXAMPLE_POD_WITH_READINESS_PROBE, uniqueID, uniqueID)\n\tdefer KubectlDeleteFromString(t, options, configData)\n\tKubectlApplyFromString(t, options, configData)\n\n\tWaitUntilPodAvailable(t, options, \"nginx-pod\", 60, 1*time.Second)\n}\n\nfunc TestWaitUntilPodAvailableWithFailingReadinessProbe(t *testing.T) {\n\tt.Parallel()\n\n\tuniqueID := strings.ToLower(random.UniqueId())\n\toptions := NewKubectlOptions(\"\", \"\", uniqueID)\n\tconfigData := fmt.Sprintf(EXAMPLE_POD_WITH_FAILING_READINESS_PROBE, uniqueID, uniqueID)\n\tdefer KubectlDeleteFromString(t, options, configData)\n\tKubectlApplyFromString(t, options, configData)\n\n\terr := WaitUntilPodAvailableE(t, options, \"nginx-pod\", 60, 1*time.Second)\n\trequire.Error(t, err)\n}\n\nconst EXAMPLE_POD_YAML_TEMPLATE = `---\napiVersion: v1\nkind: Namespace\nmetadata:\n  name: %s\n---\napiVersion: v1\nkind: Pod\nmetadata:\n  name: nginx-pod\n  namespace: %s\nspec:\n  containers:\n  - name: nginx\n    image: nginx:1.15.7\n    env:\n        - name: NAME\n          value: \"nginx\"\n    ports:\n    - containerPort: 80\n`\n\nconst EXAMPLE_POD_WITH_MULTIPLE_CONTAINERS_YAML_TEMPLATE = `---\napiVersion: v1\nkind: Namespace\nmetadata:\n  name: %s\n---\napiVersion: v1\nkind: Pod\nmetadata:\n  name: nginx-pod\n  namespace: %s\nspec:\n  containers:\n  - name: nginx\n    image: nginx:1.15.7\n    env:\n        - name: NAME\n          value: \"nginx\"\n    ports:\n    - containerPort: 80\n  - name: nginx-two\n    image: nginx:1.15.7\n    env:\n        - name: NAME\n          value: \"nginx-two\"\n    ports:\n    - containerPort: 8080\n    command: [\"sh\", \"-c\", \"sed -i 's/80/8080/' /etc/nginx/conf.d/default.conf && nginx -g 'daemon off;'\"]\n`\n\nconst EXAMPLE_POD_WITH_READINESS_PROBE = EXAMPLE_POD_YAML_TEMPLATE + `\n    readinessProbe:\n      httpGet:\n        path: /\n        port: 80\n`\n\nconst EXAMPLE_POD_WITH_FAILING_READINESS_PROBE = EXAMPLE_POD_YAML_TEMPLATE + `\n    readinessProbe:\n      httpGet:\n        path: /not-ready\n        port: 80\n      periodSeconds: 1\n`\n\nfunc TestIsPodAvailable(t *testing.T) {\n\tt.Parallel()\n\n\tcases := []struct {\n\t\ttitle          string\n\t\tpod            *corev1.Pod\n\t\texpectedResult bool\n\t}{\n\t\t{\n\t\t\ttitle: \"TestIsPodAvailableStartedButNotReady\",\n\t\t\tpod: &corev1.Pod{\n\t\t\t\tSpec: corev1.PodSpec{\n\t\t\t\t\tContainers: []corev1.Container{{Name: \"container1\"}},\n\t\t\t\t},\n\t\t\t\tStatus: corev1.PodStatus{\n\t\t\t\t\tContainerStatuses: []corev1.ContainerStatus{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName:    \"container1\",\n\t\t\t\t\t\t\tReady:   false,\n\t\t\t\t\t\t\tStarted: &[]bool{true}[0],\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tPhase: corev1.PodRunning,\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedResult: false,\n\t\t},\n\t\t{\n\t\t\ttitle: \"TestIsPodAvailableStartedAndReady\",\n\t\t\tpod: &corev1.Pod{\n\t\t\t\tSpec: corev1.PodSpec{\n\t\t\t\t\tContainers: []corev1.Container{{Name: \"container1\"}},\n\t\t\t\t},\n\t\t\t\tStatus: corev1.PodStatus{\n\t\t\t\t\tContainerStatuses: []corev1.ContainerStatus{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName:    \"container1\",\n\t\t\t\t\t\t\tReady:   true,\n\t\t\t\t\t\t\tStarted: &[]bool{true}[0],\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tPhase: corev1.PodRunning,\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedResult: true,\n\t\t},\n\t\t{\n\t\t\ttitle: \"TestIsPodAvailableMissingContainerStatus\",\n\t\t\tpod: &corev1.Pod{\n\t\t\t\tSpec: corev1.PodSpec{\n\t\t\t\t\tContainers: []corev1.Container{{Name: \"container1\"}, {Name: \"container2\"}},\n\t\t\t\t},\n\t\t\t\tStatus: corev1.PodStatus{\n\t\t\t\t\tContainerStatuses: []corev1.ContainerStatus{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName:    \"container1\",\n\t\t\t\t\t\t\tReady:   true,\n\t\t\t\t\t\t\tStarted: &[]bool{true}[0],\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tPhase: corev1.PodRunning,\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedResult: false,\n\t\t},\n\t}\n\tfor _, tc := range cases {\n\t\ttc := tc\n\t\tt.Run(tc.title, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tactualResult := IsPodAvailable(tc.pod)\n\t\t\trequire.Equal(t, tc.expectedResult, actualResult)\n\t\t})\n\t}\n}\n\nfunc TestExecPod(t *testing.T) {\n\tt.Parallel()\n\n\tuniqueID := strings.ToLower(random.UniqueId())\n\toptions := NewKubectlOptions(\"\", \"\", uniqueID)\n\tconfigData := fmt.Sprintf(EXAMPLE_POD_WITH_MULTIPLE_CONTAINERS_YAML_TEMPLATE, uniqueID, uniqueID)\n\tdefer KubectlDeleteFromString(t, options, configData)\n\tKubectlApplyFromString(t, options, configData)\n\n\tWaitUntilPodAvailable(t, options, \"nginx-pod\", 60, 1*time.Second)\n\n\tt.Run(\"TestExecPodWithoutContainer\", func(t *testing.T) {\n\t\tstdout, err := ExecPodE(t, options, \"nginx-pod\", \"\", \"env\")\n\t\trequire.NoError(t, err)\n\t\trequire.Contains(t, stdout, \"NAME=nginx\\n\")\n\t})\n\n\tt.Run(\"TestExecPodWithContainer\", func(t *testing.T) {\n\t\tstdout, err := ExecPodE(t, options, \"nginx-pod\", \"nginx-two\", \"env\")\n\t\trequire.NoError(t, err)\n\t\trequire.Contains(t, stdout, \"NAME=nginx-two\\n\")\n\t})\n\n}\n"
  },
  {
    "path": "modules/k8s/replicaset.go",
    "content": "package k8s\n\nimport (\n\t\"context\"\n\n\t\"github.com/stretchr/testify/require\"\n\tappsv1 \"k8s.io/api/apps/v1\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n)\n\n// ListReplicaSets will look for replicasets in the given namespace that match the given filters and return them. This will\n// fail the test if there is an error.\nfunc ListReplicaSets(t testing.TestingT, options *KubectlOptions, filters metav1.ListOptions) []appsv1.ReplicaSet {\n\treplicaset, err := ListReplicaSetsE(t, options, filters)\n\trequire.NoError(t, err)\n\treturn replicaset\n}\n\n// ListReplicaSetsE will look for replicasets in the given namespace that match the given filters and return them.\nfunc ListReplicaSetsE(t testing.TestingT, options *KubectlOptions, filters metav1.ListOptions) ([]appsv1.ReplicaSet, error) {\n\tclientset, err := GetKubernetesClientFromOptionsE(t, options)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treplicasets, err := clientset.AppsV1().ReplicaSets(options.Namespace).List(context.Background(), filters)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn replicasets.Items, nil\n}\n\n// GetReplicaSet returns a Kubernetes replicaset resource in the provided namespace with the given name. This will\n// fail the test if there is an error.\nfunc GetReplicaSet(t testing.TestingT, options *KubectlOptions, replicaSetName string) *appsv1.ReplicaSet {\n\treplicaset, err := GetReplicaSetE(t, options, replicaSetName)\n\trequire.NoError(t, err)\n\treturn replicaset\n}\n\n// GetReplicaSetE returns a Kubernetes replicaset resource in the provided namespace with the given name.\nfunc GetReplicaSetE(t testing.TestingT, options *KubectlOptions, replicaSetName string) (*appsv1.ReplicaSet, error) {\n\tclientset, err := GetKubernetesClientFromOptionsE(t, options)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn clientset.AppsV1().ReplicaSets(options.Namespace).Get(context.Background(), replicaSetName, metav1.GetOptions{})\n}\n"
  },
  {
    "path": "modules/k8s/replicaset_test.go",
    "content": "//go:build kubeall || kubernetes\n// +build kubeall kubernetes\n\n// NOTE: we have build tags to differentiate kubernetes tests from non-kubernetes tests. This is done because minikube\n// is heavy and can interfere with docker related tests in terratest. Specifically, many of the tests start to fail with\n// `connection refused` errors from `minikube`. To avoid overloading the system, we run the kubernetes tests and helm\n// tests separately from the others. This may not be necessary if you have a sufficiently powerful machine.  We\n// recommend at least 4 cores and 16GB of RAM if you want to run all the tests together.\n\npackage k8s\n\nimport (\n\t\"fmt\"\n\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/stretchr/testify/require\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n)\n\nfunc TestGetReplicaSetEReturnsError(t *testing.T) {\n\tt.Parallel()\n\n\toptions := NewKubectlOptions(\"\", \"\", \"\")\n\t_, err := GetReplicaSetE(t, options, \"sample-rs\")\n\trequire.Error(t, err)\n}\n\nfunc TestGetReplicaSets(t *testing.T) {\n\tt.Parallel()\n\n\tuniqueID := strings.ToLower(random.UniqueId())\n\toptions := NewKubectlOptions(\"\", \"\", uniqueID)\n\tconfigData := fmt.Sprintf(EXAMPLE_REPLICASET_YAML_TEMPLATE, uniqueID, uniqueID)\n\tdefer KubectlDeleteFromString(t, options, configData)\n\tKubectlApplyFromString(t, options, configData)\n\n\treplicaSet := GetReplicaSet(t, options, \"sample-rs\")\n\trequire.Equal(t, replicaSet.Name, \"sample-rs\")\n\trequire.Equal(t, replicaSet.Namespace, uniqueID)\n}\n\nfunc TestListReplicaSets(t *testing.T) {\n\tt.Parallel()\n\n\tuniqueID := strings.ToLower(random.UniqueId())\n\toptions := NewKubectlOptions(\"\", \"\", uniqueID)\n\tconfigData := fmt.Sprintf(EXAMPLE_REPLICASET_YAML_TEMPLATE, uniqueID, uniqueID)\n\tdefer KubectlDeleteFromString(t, options, configData)\n\tKubectlApplyFromString(t, options, configData)\n\n\treplicaSets := ListReplicaSets(t, options, metav1.ListOptions{})\n\trequire.Equal(t, len(replicaSets), 1)\n\n\treplicaSet := replicaSets[0]\n\trequire.Equal(t, replicaSet.Name, \"sample-rs\")\n\trequire.Equal(t, replicaSet.Namespace, uniqueID)\n}\n\nconst EXAMPLE_REPLICASET_YAML_TEMPLATE = `---\napiVersion: v1\nkind: Namespace\nmetadata:\n  name: %s\n---\napiVersion: apps/v1\nkind: ReplicaSet\nmetadata:\n  name: sample-rs\n  namespace: %s\n  labels:\n    app: sample-rs\nspec:\n  selector:\n    matchLabels:\n      name: sample-rs\n  template:\n    metadata:\n      labels:\n        name: sample-rs\n    spec:\n      containers:\n      - name: alpine\n        image: alpine:3.8\n        command: ['sh', '-c', 'echo Hello Terratest! && sleep 99999']\n`\n"
  },
  {
    "path": "modules/k8s/role.go",
    "content": "package k8s\n\nimport (\n\t\"context\"\n\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/stretchr/testify/require\"\n\trbacv1 \"k8s.io/api/rbac/v1\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n)\n\n// GetRole returns a Kubernetes role resource in the provided namespace with the given name. The namespace used\n// is the one provided in the KubectlOptions. This will fail the test if there is an error.\nfunc GetRole(t testing.TestingT, options *KubectlOptions, roleName string) *rbacv1.Role {\n\trole, err := GetRoleE(t, options, roleName)\n\trequire.NoError(t, err)\n\treturn role\n}\n\n// GetRoleE returns a Kubernetes role resource in the provided namespace with the given name. The namespace used\n// is the one provided in the KubectlOptions.\nfunc GetRoleE(t testing.TestingT, options *KubectlOptions, roleName string) (*rbacv1.Role, error) {\n\tclientset, err := GetKubernetesClientFromOptionsE(t, options)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn clientset.RbacV1().Roles(options.Namespace).Get(context.Background(), roleName, metav1.GetOptions{})\n}\n"
  },
  {
    "path": "modules/k8s/role_test.go",
    "content": "//go:build kubeall || kubernetes\n// +build kubeall kubernetes\n\n// NOTE: we have build tags to differentiate kubernetes tests from non-kubernetes tests. This is done because minikube\n// is heavy and can interfere with docker related tests in terratest. Specifically, many of the tests start to fail with\n// `connection refused` errors from `minikube`. To avoid overloading the system, we run the kubernetes tests and helm\n// tests separately from the others. This may not be necessary if you have a sufficiently powerful machine.  We\n// recommend at least 4 cores and 16GB of RAM if you want to run all the tests together.\n\npackage k8s\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n)\n\nfunc TestGetRoleEReturnsErrorForNonExistantRole(t *testing.T) {\n\tt.Parallel()\n\n\toptions := NewKubectlOptions(\"\", \"\", \"default\")\n\t_, err := GetRoleE(t, options, \"non-existing-role\")\n\trequire.Error(t, err)\n}\n\nfunc TestGetRoleEReturnsCorrectRoleInCorrectNamespace(t *testing.T) {\n\tt.Parallel()\n\n\tuniqueID := strings.ToLower(random.UniqueId())\n\toptions := NewKubectlOptions(\"\", \"\", uniqueID)\n\tconfigData := fmt.Sprintf(EXAMPLE_ROLE_YAML_TEMPLATE, uniqueID, uniqueID)\n\tdefer KubectlDeleteFromString(t, options, configData)\n\tKubectlApplyFromString(t, options, configData)\n\n\trole := GetRole(t, options, \"terratest-role\")\n\trequire.Equal(t, role.Name, \"terratest-role\")\n\trequire.Equal(t, role.Namespace, uniqueID)\n}\n\nconst EXAMPLE_ROLE_YAML_TEMPLATE = `---\napiVersion: v1\nkind: Namespace\nmetadata:\n  name: '%s'\n---\napiVersion: rbac.authorization.k8s.io/v1\nkind: Role\nmetadata:\n  name: 'terratest-role'\n  namespace: '%s'\nrules:\n- apiGroups:\n  - '*'\n  resources:\n  - '*'\n  verbs:\n  - '*'\n`\n"
  },
  {
    "path": "modules/k8s/secret.go",
    "content": "package k8s\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/gruntwork-io/terratest/modules/retry\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/stretchr/testify/require\"\n\tcorev1 \"k8s.io/api/core/v1\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n)\n\n// GetSecret returns a Kubernetes secret resource in the provided namespace with the given name. The namespace used\n// is the one provided in the KubectlOptions. This will fail the test if there is an error.\nfunc GetSecret(t testing.TestingT, options *KubectlOptions, secretName string) *corev1.Secret {\n\tsecret, err := GetSecretE(t, options, secretName)\n\trequire.NoError(t, err)\n\treturn secret\n}\n\n// GetSecretE returns a Kubernetes secret resource in the provided namespace with the given name. The namespace used\n// is the one provided in the KubectlOptions.\nfunc GetSecretE(t testing.TestingT, options *KubectlOptions, secretName string) (*corev1.Secret, error) {\n\tclientset, err := GetKubernetesClientFromOptionsE(t, options)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn clientset.CoreV1().Secrets(options.Namespace).Get(context.Background(), secretName, metav1.GetOptions{})\n}\n\n// WaitUntilSecretAvailable waits until the secret is present on the cluster in cases where it is not immediately\n// available (for example, when using ClusterIssuer to request a certificate).\nfunc WaitUntilSecretAvailable(t testing.TestingT, options *KubectlOptions, secretName string, retries int, sleepBetweenRetries time.Duration) {\n\tstatusMsg := fmt.Sprintf(\"Wait for secret %s to be provisioned.\", secretName)\n\tmessage := retry.DoWithRetry(\n\t\tt,\n\t\tstatusMsg,\n\t\tretries,\n\t\tsleepBetweenRetries,\n\t\tfunc() (string, error) {\n\t\t\t_, err := GetSecretE(t, options, secretName)\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", err\n\t\t\t}\n\n\t\t\treturn \"Secret is now available\", nil\n\t\t},\n\t)\n\toptions.Logger.Logf(t, \"%s\", message)\n}\n"
  },
  {
    "path": "modules/k8s/secret_test.go",
    "content": "//go:build kubeall || kubernetes\n// +build kubeall kubernetes\n\n// NOTE: we have build tags to differentiate kubernetes tests from non-kubernetes tests. This is done because minikube\n// is heavy and can interfere with docker related tests in terratest. Specifically, many of the tests start to fail with\n// `connection refused` errors from `minikube`. To avoid overloading the system, we run the kubernetes tests and helm\n// tests separately from the others. This may not be necessary if you have a sufficiently powerful machine.  We\n// recommend at least 4 cores and 16GB of RAM if you want to run all the tests together.\n\npackage k8s\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n)\n\nfunc TestGetSecretEReturnsErrorForNonExistantSecret(t *testing.T) {\n\tt.Parallel()\n\n\toptions := NewKubectlOptions(\"\", \"\", \"default\")\n\t_, err := GetSecretE(t, options, \"master-password\")\n\trequire.Error(t, err)\n}\n\nfunc TestGetSecretEReturnsCorrectSecretInCorrectNamespace(t *testing.T) {\n\tt.Parallel()\n\n\tuniqueID := strings.ToLower(random.UniqueId())\n\toptions := NewKubectlOptions(\"\", \"\", uniqueID)\n\tconfigData := fmt.Sprintf(EXAMPLE_SECRET_YAML_TEMPLATE, uniqueID, uniqueID)\n\tdefer KubectlDeleteFromString(t, options, configData)\n\tKubectlApplyFromString(t, options, configData)\n\n\tsecret := GetSecret(t, options, \"master-password\")\n\trequire.Equal(t, secret.Name, \"master-password\")\n\trequire.Equal(t, secret.Namespace, uniqueID)\n}\n\nfunc TestWaitUntilSecretAvailableReturnsSuccessfully(t *testing.T) {\n\tt.Parallel()\n\n\tuniqueID := strings.ToLower(random.UniqueId())\n\toptions := NewKubectlOptions(\"\", \"\", uniqueID)\n\tconfigData := fmt.Sprintf(EXAMPLE_SECRET_YAML_TEMPLATE, uniqueID, uniqueID)\n\tdefer KubectlDeleteFromString(t, options, configData)\n\n\tKubectlApplyFromString(t, options, configData)\n\tWaitUntilSecretAvailable(t, options, \"master-password\", 10, 1*time.Second)\n}\n\nconst EXAMPLE_SECRET_YAML_TEMPLATE = `---\napiVersion: v1\nkind: Namespace\nmetadata:\n  name: %s\n---\napiVersion: v1\nkind: Secret\nmetadata:\n  name: master-password\n  namespace: %s\n`\n"
  },
  {
    "path": "modules/k8s/self_subject_access_review.go",
    "content": "package k8s\n\nimport (\n\t\"context\"\n\n\t\"github.com/gruntwork-io/go-commons/errors\"\n\t\"github.com/stretchr/testify/require\"\n\tauthv1 \"k8s.io/api/authorization/v1\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n)\n\n// CanIDo returns whether or not the provided action is allowed by the client configured by the provided kubectl option.\n// This will fail if there are any errors accessing the kubernetes API (but not if the action is denied).\nfunc CanIDo(t testing.TestingT, options *KubectlOptions, action authv1.ResourceAttributes) bool {\n\tallowed, err := CanIDoE(t, options, action)\n\trequire.NoError(t, err)\n\treturn allowed\n}\n\n// CanIDoE returns whether or not the provided action is allowed by the client configured by the provided kubectl option.\n// This will an error if there are problems accessing the kubernetes API (but not if the action is simply denied).\nfunc CanIDoE(t testing.TestingT, options *KubectlOptions, action authv1.ResourceAttributes) (bool, error) {\n\tclientset, err := GetKubernetesClientFromOptionsE(t, options)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tcheck := authv1.SelfSubjectAccessReview{\n\t\tSpec: authv1.SelfSubjectAccessReviewSpec{ResourceAttributes: &action},\n\t}\n\tresp, err := clientset.AuthorizationV1().SelfSubjectAccessReviews().Create(context.Background(), &check, metav1.CreateOptions{})\n\tif err != nil {\n\t\treturn false, errors.WithStackTrace(err)\n\t}\n\tif !resp.Status.Allowed {\n\t\toptions.Logger.Logf(t, \"Denied action %s on resource %s with name '%s' for reason %s\", action.Verb, action.Resource, action.Name, resp.Status.Reason)\n\t}\n\treturn resp.Status.Allowed, nil\n}\n"
  },
  {
    "path": "modules/k8s/self_subject_access_review_test.go",
    "content": "//go:build kubeall || kubernetes\n// +build kubeall kubernetes\n\n// NOTE: we have build tags to differentiate kubernetes tests from non-kubernetes tests. This is done because minikube\n// is heavy and can interfere with docker related tests in terratest. Specifically, many of the tests start to fail with\n// `connection refused` errors from `minikube`. To avoid overloading the system, we run the kubernetes tests and helm\n// tests separately from the others. This may not be necessary if you have a sufficiently powerful machine.  We\n// recommend at least 4 cores and 16GB of RAM if you want to run all the tests together.\n\npackage k8s\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\tauthv1 \"k8s.io/api/authorization/v1\"\n)\n\n// NOTE: See service_account_test.go:TestGetServiceAccountWithAuthTokenGetsTokenThatCanBeUsedForAuth for the deny case,\n// as the current authed user is assumed to be a super user and so there is nothing they can't do.\n\nfunc TestCanIDoReturnsTrueForAllowedAction(t *testing.T) {\n\tt.Parallel()\n\n\taction := authv1.ResourceAttributes{\n\t\tNamespace: \"kube-system\",\n\t\tVerb:      \"list\",\n\t\tResource:  \"pod\",\n\t}\n\toptions := NewKubectlOptions(\"\", \"\", \"kube-system\")\n\tassert.True(t, CanIDo(t, options, action))\n}\n"
  },
  {
    "path": "modules/k8s/service.go",
    "content": "package k8s\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/url\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/require\"\n\tcorev1 \"k8s.io/api/core/v1\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\n\t\"github.com/gruntwork-io/terratest/modules/aws\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/gruntwork-io/terratest/modules/retry\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n)\n\n// ListServices will look for services in the given namespace that match the given filters and return them. This will\n// fail the test if there is an error.\nfunc ListServices(t testing.TestingT, options *KubectlOptions, filters metav1.ListOptions) []corev1.Service {\n\tservice, err := ListServicesE(t, options, filters)\n\trequire.NoError(t, err)\n\treturn service\n}\n\n// ListServicesE will look for services in the given namespace that match the given filters and return them.\nfunc ListServicesE(t testing.TestingT, options *KubectlOptions, filters metav1.ListOptions) ([]corev1.Service, error) {\n\tclientset, err := GetKubernetesClientFromOptionsE(t, options)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tresp, err := clientset.CoreV1().Services(options.Namespace).List(context.Background(), filters)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn resp.Items, nil\n}\n\n// GetService returns a Kubernetes service resource in the provided namespace with the given name. This will\n// fail the test if there is an error.\nfunc GetService(t testing.TestingT, options *KubectlOptions, serviceName string) *corev1.Service {\n\tservice, err := GetServiceE(t, options, serviceName)\n\trequire.NoError(t, err)\n\treturn service\n}\n\n// GetServiceE returns a Kubernetes service resource in the provided namespace with the given name.\nfunc GetServiceE(t testing.TestingT, options *KubectlOptions, serviceName string) (*corev1.Service, error) {\n\tclientset, err := GetKubernetesClientFromOptionsE(t, options)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn clientset.CoreV1().Services(options.Namespace).Get(context.Background(), serviceName, metav1.GetOptions{})\n}\n\n// WaitUntilServiceAvailable waits until the service endpoint is ready to accept traffic.\nfunc WaitUntilServiceAvailable(t testing.TestingT, options *KubectlOptions, serviceName string, retries int, sleepBetweenRetries time.Duration) {\n\tstatusMsg := fmt.Sprintf(\"Wait for service %s to be provisioned.\", serviceName)\n\tmessage := retry.DoWithRetry(\n\t\tt,\n\t\tstatusMsg,\n\t\tretries,\n\t\tsleepBetweenRetries,\n\t\tfunc() (string, error) {\n\t\t\tservice, err := GetServiceE(t, options, serviceName)\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", err\n\t\t\t}\n\n\t\t\tisMinikube, err := IsMinikubeE(t, options)\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", err\n\t\t\t}\n\n\t\t\t// For minikube, all services will be available immediately so we only do the check if we are not on\n\t\t\t// minikube.\n\t\t\tif !isMinikube && !IsServiceAvailable(service) {\n\t\t\t\treturn \"\", NewServiceNotAvailableError(service)\n\t\t\t}\n\t\t\treturn \"Service is now available\", nil\n\t\t},\n\t)\n\toptions.Logger.Logf(t, \"%s\", message)\n}\n\n// IsServiceAvailable returns true if the service endpoint is ready to accept traffic. Note that for Minikube, this\n// function is moot as all services, even LoadBalancer, is available immediately.\nfunc IsServiceAvailable(service *corev1.Service) bool {\n\t// Only the LoadBalancer type has a delay. All other service types are available if the resource exists.\n\tswitch service.Spec.Type {\n\tcase corev1.ServiceTypeLoadBalancer:\n\t\tingress := service.Status.LoadBalancer.Ingress\n\t\t// The load balancer is ready if it has at least one ingress point\n\t\treturn len(ingress) > 0\n\tdefault:\n\t\treturn true\n\t}\n}\n\n// GetServiceEndpoint will return the service access point. If the service endpoint is not ready, will fail the test\n// immediately.\nfunc GetServiceEndpoint(t testing.TestingT, options *KubectlOptions, service *corev1.Service, servicePort int) string {\n\tendpoint, err := GetServiceEndpointE(t, options, service, servicePort)\n\trequire.NoError(t, err)\n\treturn endpoint\n}\n\n// GetServiceEndpointE will return the service access point using the following logic:\n//   - For ClusterIP service type, return the URL that maps to ClusterIP and Service Port\n//   - For NodePort service type, identify the public IP of the node (if it exists, otherwise return the bound hostname),\n//     and the assigned node port for the provided service port, and return the URL that maps to node ip and node port.\n//   - For LoadBalancer service type, return the publicly accessible hostname of the load balancer.\n//     If the hostname is empty, it will return the public IP of the LoadBalancer.\n//   - All other service types are not supported.\nfunc GetServiceEndpointE(t testing.TestingT, options *KubectlOptions, service *corev1.Service, servicePort int) (string, error) {\n\tswitch service.Spec.Type {\n\tcase corev1.ServiceTypeClusterIP:\n\t\t// ClusterIP service type will map directly to service port\n\t\treturn fmt.Sprintf(\"%s:%d\", service.Spec.ClusterIP, servicePort), nil\n\tcase corev1.ServiceTypeNodePort:\n\t\treturn findEndpointForNodePortService(t, options, service, int32(servicePort))\n\tcase corev1.ServiceTypeLoadBalancer:\n\t\t// For minikube, LoadBalancer service is exactly the same as NodePort service\n\t\tisMinikube, err := IsMinikubeE(t, options)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\tif isMinikube {\n\t\t\treturn findEndpointForNodePortService(t, options, service, int32(servicePort))\n\t\t}\n\n\t\tingress := service.Status.LoadBalancer.Ingress\n\t\tif len(ingress) == 0 {\n\t\t\treturn \"\", NewServiceNotAvailableError(service)\n\t\t}\n\t\tif ingress[0].Hostname == \"\" {\n\t\t\treturn fmt.Sprintf(\"%s:%d\", ingress[0].IP, servicePort), nil\n\t\t}\n\t\t// Load Balancer service type will map directly to service port\n\t\treturn fmt.Sprintf(\"%s:%d\", ingress[0].Hostname, servicePort), nil\n\tdefault:\n\t\treturn \"\", NewUnknownServiceTypeError(service)\n\t}\n}\n\n// Extracts a endpoint that can be reached outside the kubernetes cluster. NodePort type needs to find the right\n// allocated node port mapped to the service port, as well as find out the externally reachable ip (if available).\nfunc findEndpointForNodePortService(\n\tt testing.TestingT,\n\toptions *KubectlOptions,\n\tservice *corev1.Service,\n\tservicePort int32,\n) (string, error) {\n\tnodePort, err := FindNodePortE(service, int32(servicePort))\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tnode, err := pickRandomNodeE(t, options)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tnodeHostname, err := FindNodeHostnameE(t, node)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn fmt.Sprintf(\"%s:%d\", nodeHostname, nodePort), nil\n}\n\n// Given the desired servicePort, return the allocated nodeport\nfunc FindNodePortE(service *corev1.Service, servicePort int32) (int32, error) {\n\tfor _, port := range service.Spec.Ports {\n\t\tif port.Port == servicePort {\n\t\t\treturn port.NodePort, nil\n\t\t}\n\t}\n\treturn -1, NewUnknownServicePortError(service, servicePort)\n}\n\n// pickRandomNode will pick a random node in the kubernetes cluster\nfunc pickRandomNodeE(t testing.TestingT, options *KubectlOptions) (corev1.Node, error) {\n\tnodes, err := GetNodesE(t, options)\n\tif err != nil {\n\t\treturn corev1.Node{}, err\n\t}\n\tif len(nodes) == 0 {\n\t\treturn corev1.Node{}, NewNoNodesInKubernetesError()\n\t}\n\tindex := random.Random(0, len(nodes)-1)\n\treturn nodes[index], nil\n}\n\n// Given a node, return the ip address, preferring the external IP\nfunc FindNodeHostnameE(t testing.TestingT, node corev1.Node) (string, error) {\n\tnodeIDUri, err := url.Parse(node.Spec.ProviderID)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tswitch nodeIDUri.Scheme {\n\tcase \"aws\":\n\t\treturn findAwsNodeHostnameE(t, node, nodeIDUri)\n\tdefault:\n\t\treturn findDefaultNodeHostnameE(node)\n\t}\n}\n\n// findAwsNodeHostname will return the public ip of the node, assuming the node is an AWS EC2 instance.\n// If the instance does not have a public IP, will return the internal hostname as recorded on the Kubernetes node\n// object.\nfunc findAwsNodeHostnameE(t testing.TestingT, node corev1.Node, awsIDUri *url.URL) (string, error) {\n\t// Path is /AVAILABILITY_ZONE/INSTANCE_ID\n\tparts := strings.Split(awsIDUri.Path, \"/\")\n\tif len(parts) != 3 {\n\t\treturn \"\", NewMalformedNodeIDError(&node)\n\t}\n\tinstanceID := parts[2]\n\tavailabilityZone := parts[1]\n\t// Availability Zone name is known to be region code + 1 letter\n\t// https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html\n\tregion := availabilityZone[:len(availabilityZone)-1]\n\n\tipMap, err := aws.GetPublicIpsOfEc2InstancesE(t, []string{instanceID}, region)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tpublicIP, containsIP := ipMap[instanceID]\n\tif !containsIP || publicIP == \"\" {\n\t\t// return default hostname\n\t\treturn findDefaultNodeHostnameE(node)\n\t}\n\treturn publicIP, nil\n}\n\n// findDefaultNodeHostname returns the hostname recorded on the Kubernetes node object.\nfunc findDefaultNodeHostnameE(node corev1.Node) (string, error) {\n\tfor _, address := range node.Status.Addresses {\n\t\tif address.Type == corev1.NodeHostName {\n\t\t\treturn address.Address, nil\n\t\t}\n\t}\n\treturn \"\", NewNodeHasNoHostnameError(&node)\n}\n"
  },
  {
    "path": "modules/k8s/service_account.go",
    "content": "package k8s\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/gruntwork-io/go-commons/errors\"\n\t\"github.com/stretchr/testify/require\"\n\tauthenticationv1 \"k8s.io/api/authentication/v1\"\n\tcorev1 \"k8s.io/api/core/v1\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\t\"k8s.io/client-go/tools/clientcmd\"\n\t\"k8s.io/client-go/tools/clientcmd/api\"\n\n\t\"github.com/gruntwork-io/terratest/modules/retry\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n)\n\n// GetServiceAccount returns a Kubernetes service account resource in the provided namespace with the given name. The\n// namespace used is the one provided in the KubectlOptions. This will fail the test if there is an error.\nfunc GetServiceAccount(t testing.TestingT, options *KubectlOptions, serviceAccountName string) *corev1.ServiceAccount {\n\tserviceAccount, err := GetServiceAccountE(t, options, serviceAccountName)\n\trequire.NoError(t, err)\n\treturn serviceAccount\n}\n\n// GetServiceAccountE returns a Kubernetes service account resource in the provided namespace with the given name. The\n// namespace used is the one provided in the KubectlOptions.\nfunc GetServiceAccountE(t testing.TestingT, options *KubectlOptions, serviceAccountName string) (*corev1.ServiceAccount, error) {\n\tclientset, err := GetKubernetesClientFromOptionsE(t, options)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn clientset.CoreV1().ServiceAccounts(options.Namespace).Get(context.Background(), serviceAccountName, metav1.GetOptions{})\n}\n\n// CreateServiceAccount will create a new service account resource in the provided namespace with the given name. The\n// namespace used is the one provided in the KubectlOptions. This will fail the test if there is an error.\nfunc CreateServiceAccount(t testing.TestingT, options *KubectlOptions, serviceAccountName string) {\n\trequire.NoError(t, CreateServiceAccountE(t, options, serviceAccountName))\n}\n\n// CreateServiceAccountE will create a new service account resource in the provided namespace with the given name. The\n// namespace used is the one provided in the KubectlOptions.\nfunc CreateServiceAccountE(t testing.TestingT, options *KubectlOptions, serviceAccountName string) error {\n\tclientset, err := GetKubernetesClientFromOptionsE(t, options)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tserviceAccount := corev1.ServiceAccount{\n\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\tName:      serviceAccountName,\n\t\t\tNamespace: options.Namespace,\n\t\t},\n\t}\n\t_, err = clientset.CoreV1().ServiceAccounts(options.Namespace).Create(context.Background(), &serviceAccount, metav1.CreateOptions{})\n\treturn err\n}\n\n// GetServiceAccountAuthToken will retrieve the ServiceAccount token from the cluster so it can be used to\n// authenticate requests as that ServiceAccount. This will fail the test if there is an error.\nfunc GetServiceAccountAuthToken(t testing.TestingT, kubectlOptions *KubectlOptions, serviceAccountName string) string {\n\ttoken, err := GetServiceAccountAuthTokenE(t, kubectlOptions, serviceAccountName)\n\trequire.NoError(t, err)\n\treturn token\n}\n\n// GetServiceAccountAuthTokenE will retrieve the ServiceAccount token from the cluster so it can be used to\n// authenticate requests as that ServiceAccount.\n// On K8s 1.24+, service account tokens are no longer auto-created as secrets, so this uses the TokenRequest API.\nfunc GetServiceAccountAuthTokenE(t testing.TestingT, kubectlOptions *KubectlOptions, serviceAccountName string) (string, error) {\n\tclientset, err := GetKubernetesClientFromOptionsE(t, kubectlOptions)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// First try the TokenRequest API (K8s 1.24+)\n\ttokenRequest := &authenticationv1.TokenRequest{\n\t\tSpec: authenticationv1.TokenRequestSpec{\n\t\t\t// Use a long expiration for test purposes\n\t\t\tExpirationSeconds: new(int64(3600)),\n\t\t},\n\t}\n\ttokenResponse, err := clientset.CoreV1().ServiceAccounts(kubectlOptions.Namespace).CreateToken(\n\t\tcontext.Background(),\n\t\tserviceAccountName,\n\t\ttokenRequest,\n\t\tmetav1.CreateOptions{},\n\t)\n\tif err == nil {\n\t\treturn tokenResponse.Status.Token, nil\n\t}\n\n\t// Fall back to legacy secret-based tokens for older K8s versions\n\tkubectlOptions.Logger.Logf(t, \"TokenRequest API failed (%s), falling back to secret-based tokens\", err)\n\tmsg, retryErr := retry.DoWithRetryE(\n\t\tt,\n\t\t\"Waiting for ServiceAccount Token to be provisioned\",\n\t\t30,\n\t\t10*time.Second,\n\t\tfunc() (string, error) {\n\t\t\tkubectlOptions.Logger.Logf(t, \"Checking if service account has secret\")\n\t\t\tserviceAccount := GetServiceAccount(t, kubectlOptions, serviceAccountName)\n\t\t\tif len(serviceAccount.Secrets) == 0 {\n\t\t\t\tmsg := \"No secrets on the service account yet\"\n\t\t\t\tkubectlOptions.Logger.Logf(t, \"%s\", msg)\n\t\t\t\treturn \"\", fmt.Errorf(\"%s\", msg)\n\t\t\t}\n\t\t\treturn \"Service Account has secret\", nil\n\t\t},\n\t)\n\tif retryErr != nil {\n\t\treturn \"\", retryErr\n\t}\n\tkubectlOptions.Logger.Logf(t, \"%s\", msg)\n\n\tserviceAccount, err := GetServiceAccountE(t, kubectlOptions, serviceAccountName)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tif len(serviceAccount.Secrets) != 1 {\n\t\treturn \"\", errors.WithStackTrace(ServiceAccountTokenNotAvailable{serviceAccountName})\n\t}\n\tsecret := GetSecret(t, kubectlOptions, serviceAccount.Secrets[0].Name)\n\treturn string(secret.Data[\"token\"]), nil\n}\n\n// AddConfigContextForServiceAccountE will add a new config context that binds the ServiceAccount auth token to the\n// Kubernetes cluster of the current config context.\nfunc AddConfigContextForServiceAccountE(\n\tt testing.TestingT,\n\tkubectlOptions *KubectlOptions,\n\tcontextName string,\n\tserviceAccountName string,\n\ttoken string,\n) error {\n\t// First load the config context\n\tconfig := LoadConfigFromPath(kubectlOptions.ConfigPath)\n\trawConfig, err := config.RawConfig()\n\tif err != nil {\n\t\treturn errors.WithStackTrace(err)\n\t}\n\n\t// Next get the current cluster\n\tcurrentContext := rawConfig.Contexts[rawConfig.CurrentContext]\n\tcurrentCluster := currentContext.Cluster\n\n\t// Now insert the auth info for the service account\n\trawConfig.AuthInfos[serviceAccountName] = &api.AuthInfo{Token: token}\n\n\t// We now have enough info to add the new context\n\tUpsertConfigContext(&rawConfig, contextName, currentCluster, serviceAccountName)\n\n\t// Finally, overwrite the config\n\tif err := clientcmd.ModifyConfig(config.ConfigAccess(), rawConfig, false); err != nil {\n\t\treturn errors.WithStackTrace(err)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "modules/k8s/service_account_test.go",
    "content": "//go:build kubeall || kubernetes\n// +build kubeall kubernetes\n\n// NOTE: we have build tags to differentiate kubernetes tests from non-kubernetes tests. This is done because minikube\n// is heavy and can interfere with docker related tests in terratest. Specifically, many of the tests start to fail with\n// `connection refused` errors from `minikube`. To avoid overloading the system, we run the kubernetes tests and helm\n// tests separately from the others. This may not be necessary if you have a sufficiently powerful machine.  We\n// recommend at least 4 cores and 16GB of RAM if you want to run all the tests together.\n\npackage k8s\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n\tauthv1 \"k8s.io/api/authorization/v1\"\n\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n)\n\nfunc TestGetServiceAccountWithAuthTokenGetsTokenThatCanBeUsedForAuth(t *testing.T) {\n\tt.Parallel()\n\n\t// make a copy of kubeconfig to namespace it\n\ttmpConfigPath := CopyHomeKubeConfigToTemp(t)\n\n\t// Create a new namespace to work in\n\tnamespaceName := strings.ToLower(random.UniqueId())\n\n\toptions := NewKubectlOptions(\"\", tmpConfigPath, namespaceName)\n\n\tCreateNamespace(t, options, namespaceName)\n\tdefer DeleteNamespace(t, options, namespaceName)\n\n\t// Create service account\n\tserviceAccountName := strings.ToLower(random.UniqueId())\n\tCreateServiceAccount(t, options, serviceAccountName)\n\ttoken := GetServiceAccountAuthToken(t, options, serviceAccountName)\n\trequire.NoError(t, AddConfigContextForServiceAccountE(t, options, serviceAccountName, serviceAccountName, token))\n\n\t// Now validate auth as service account. This is a bit tricky because we don't have an API endpoint in k8s that\n\t// tells you who you are, so we will rely on the self subject access review and see if we have access to the\n\t// kube-system namespace.\n\tserviceAccountOptions := NewKubectlOptions(serviceAccountName, tmpConfigPath, namespaceName)\n\taction := authv1.ResourceAttributes{\n\t\tNamespace: \"kube-system\",\n\t\tVerb:      \"list\",\n\t\tResource:  \"pod\",\n\t}\n\trequire.False(t, CanIDo(t, serviceAccountOptions, action))\n}\n\nfunc TestGetServiceAccountEReturnsErrorForNonExistantServiceAccount(t *testing.T) {\n\tt.Parallel()\n\n\toptions := NewKubectlOptions(\"\", \"\", \"default\")\n\t_, err := GetServiceAccountE(t, options, \"terratest\")\n\trequire.Error(t, err)\n}\n\nfunc TestGetServiceAccountEReturnsCorrectServiceAccountInCorrectNamespace(t *testing.T) {\n\tt.Parallel()\n\n\tuniqueID := strings.ToLower(random.UniqueId())\n\toptions := NewKubectlOptions(\"\", \"\", uniqueID)\n\tconfigData := fmt.Sprintf(EXAMPLE_SERVICEACCOUNT_YAML_TEMPLATE, uniqueID, uniqueID)\n\tdefer KubectlDeleteFromString(t, options, configData)\n\tKubectlApplyFromString(t, options, configData)\n\n\tserviceAccount := GetServiceAccount(t, options, \"terratest\")\n\trequire.Equal(t, serviceAccount.Name, \"terratest\")\n\trequire.Equal(t, serviceAccount.Namespace, uniqueID)\n}\n\nfunc TestCreateServiceAccountECreatesServiceAccountInNamespaceWithGivenName(t *testing.T) {\n\tt.Parallel()\n\n\tuniqueID := strings.ToLower(random.UniqueId())\n\toptions := NewKubectlOptions(\"\", \"\", uniqueID)\n\tdefer DeleteNamespace(t, options, options.Namespace)\n\tCreateNamespace(t, options, options.Namespace)\n\n\t// Note: We don't need to delete this at the end of test, because deleting the namespace automatically deletes\n\t// everything created in the namespace.\n\tCreateServiceAccount(t, options, \"terratest\")\n\tserviceAccount := GetServiceAccount(t, options, \"terratest\")\n\trequire.Equal(t, serviceAccount.Name, \"terratest\")\n\trequire.Equal(t, serviceAccount.Namespace, uniqueID)\n}\n\nconst EXAMPLE_SERVICEACCOUNT_YAML_TEMPLATE = `---\napiVersion: v1\nkind: Namespace\nmetadata:\n  name: %s\n---\napiVersion: v1\nkind: ServiceAccount\nmetadata:\n  name: terratest\n  namespace: %s\n`\n"
  },
  {
    "path": "modules/k8s/service_test.go",
    "content": "//go:build kubernetes\n// +build kubernetes\n\n// NOTE: we have build tags to differentiate kubernetes tests from non-kubernetes tests. This is done because minikube\n// is heavy and can interfere with docker related tests in terratest. Specifically, many of the tests start to fail with\n// `connection refused` errors from `minikube`. To avoid overloading the system, we run the kubernetes tests and helm\n// tests separately from the others. This may not be necessary if you have a sufficiently powerful machine.  We\n// recommend at least 4 cores and 16GB of RAM if you want to run all the tests together.\n\npackage k8s\n\nimport (\n\t\"crypto/tls\"\n\t\"fmt\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/require\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\n\thttp_helper \"github.com/gruntwork-io/terratest/modules/http-helper\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n)\n\nfunc TestGetServiceEReturnsErrorForNonExistantService(t *testing.T) {\n\tt.Parallel()\n\n\toptions := NewKubectlOptions(\"\", \"\", \"default\")\n\t_, err := GetServiceE(t, options, \"nginx-service\")\n\trequire.Error(t, err)\n}\n\nfunc TestGetServiceEReturnsCorrectServiceInCorrectNamespace(t *testing.T) {\n\tt.Parallel()\n\n\tuniqueID := strings.ToLower(random.UniqueId())\n\toptions := NewKubectlOptions(\"\", \"\", uniqueID)\n\tconfigData := fmt.Sprintf(EXAMPLE_DEPLOYMENT_YAML_TEMPLATE, uniqueID, uniqueID, uniqueID)\n\tKubectlApplyFromString(t, options, configData)\n\tdefer KubectlDeleteFromString(t, options, configData)\n\n\tservice := GetService(t, options, \"nginx-service\")\n\trequire.Equal(t, service.Name, \"nginx-service\")\n\trequire.Equal(t, service.Namespace, uniqueID)\n}\n\nfunc TestListServicesReturnsCorrectServiceInCorrectNamespace(t *testing.T) {\n\tt.Parallel()\n\n\tuniqueID := strings.ToLower(random.UniqueId())\n\toptions := NewKubectlOptions(\"\", \"\", uniqueID)\n\tconfigData := fmt.Sprintf(EXAMPLE_DEPLOYMENT_YAML_TEMPLATE, uniqueID, uniqueID, uniqueID)\n\tKubectlApplyFromString(t, options, configData)\n\tdefer KubectlDeleteFromString(t, options, configData)\n\n\tservices := ListServices(t, options, metav1.ListOptions{})\n\trequire.Equal(t, len(services), 1)\n\n\tservice := services[0]\n\trequire.Equal(t, service.Name, \"nginx-service\")\n\trequire.Equal(t, service.Namespace, uniqueID)\n}\n\nfunc TestWaitUntilServiceAvailableReturnsSuccessfullyOnNodePortType(t *testing.T) {\n\tt.Parallel()\n\n\tuniqueID := strings.ToLower(random.UniqueId())\n\toptions := NewKubectlOptions(\"\", \"\", uniqueID)\n\tconfigData := fmt.Sprintf(EXAMPLE_DEPLOYMENT_YAML_TEMPLATE, uniqueID, uniqueID, uniqueID)\n\tKubectlApplyFromString(t, options, configData)\n\tdefer KubectlDeleteFromString(t, options, configData)\n\n\tWaitUntilServiceAvailable(t, options, \"nginx-service\", 10, 1*time.Second)\n}\n\nfunc TestGetServiceEndpointEReturnsAccessibleEndpointForNodePort(t *testing.T) {\n\tt.Parallel()\n\n\tuniqueID := strings.ToLower(random.UniqueId())\n\toptions := NewKubectlOptions(\"\", \"\", uniqueID)\n\tconfigData := fmt.Sprintf(EXAMPLE_DEPLOYMENT_YAML_TEMPLATE, uniqueID, uniqueID, uniqueID)\n\tKubectlApplyFromString(t, options, configData)\n\tdefer KubectlDeleteFromString(t, options, configData)\n\n\tservice := GetService(t, options, \"nginx-service\")\n\tendpoint := GetServiceEndpoint(t, options, service, 80)\n\n\t// Setup a TLS configuration to submit with the helper, a blank struct is acceptable\n\ttlsConfig := tls.Config{}\n\n\t// Test up to 5 minutes\n\thttp_helper.HttpGetWithRetryWithCustomValidation(\n\t\tt,\n\t\tfmt.Sprintf(\"http://%s\", endpoint),\n\t\t&tlsConfig,\n\t\t30,\n\t\t10*time.Second,\n\t\tfunc(statusCode int, body string) bool {\n\t\t\treturn statusCode == 200\n\t\t},\n\t)\n}\n\nconst EXAMPLE_DEPLOYMENT_YAML_TEMPLATE = `---\napiVersion: v1\nkind: Namespace\nmetadata:\n  name: %s\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: nginx-deployment\n  namespace: %s\nspec:\n  selector:\n    matchLabels:\n      app: nginx\n  replicas: 1\n  template:\n    metadata:\n      labels:\n        app: nginx\n    spec:\n      containers:\n      - name: nginx\n        image: nginx:1.15.7\n        ports:\n        - containerPort: 80\n---\nkind: Service\napiVersion: v1\nmetadata:\n  name: nginx-service\n  namespace: %s\nspec:\n  selector:\n    app: nginx\n  ports:\n  - protocol: TCP\n    targetPort: 80\n    port: 80\n  type: NodePort\n`\n"
  },
  {
    "path": "modules/k8s/tunnel.go",
    "content": "package k8s\n\n// The following code is a fork of the Helm client. The main differences are:\n// - Support testing context for better logging\n// - Support resources other than pods\n// See: https://github.com/helm/helm/blob/master/pkg/kube/tunnel.go\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/stretchr/testify/require\"\n\tcorev1 \"k8s.io/api/core/v1\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\t\"k8s.io/apimachinery/pkg/util/intstr\"\n\t\"k8s.io/client-go/tools/portforward\"\n\t\"k8s.io/client-go/transport/spdy\"\n\n\t\"github.com/gruntwork-io/terratest/modules/logger\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n)\n\n// Global lock to synchronize port selections\nvar globalMutex sync.Mutex\n\n// KubeResourceType is an enum representing known resource types that can support port forwarding\ntype KubeResourceType int\n\nconst (\n\t// ResourceTypePod is a k8s pod kind identifier\n\tResourceTypePod KubeResourceType = iota\n\t// ResourceTypeDeployment is a k8s deployment kind identifier\n\tResourceTypeDeployment\n\t// ResourceTypeService is a k8s service kind identifier\n\tResourceTypeService\n)\n\nfunc (resourceType KubeResourceType) String() string {\n\tswitch resourceType {\n\tcase ResourceTypeDeployment:\n\t\treturn \"deploy\"\n\tcase ResourceTypePod:\n\t\treturn \"pod\"\n\tcase ResourceTypeService:\n\t\treturn \"svc\"\n\tdefault:\n\t\t// This should not happen\n\t\treturn \"UNKNOWN_RESOURCE_TYPE\"\n\t}\n}\n\n// makeLabels is a helper to format a map of label key and value pairs into a single string for use as a selector.\nfunc makeLabels(labels map[string]string) string {\n\tout := []string{}\n\tfor key, value := range labels {\n\t\tout = append(out, fmt.Sprintf(\"%s=%s\", key, value))\n\t}\n\treturn strings.Join(out, \",\")\n}\n\n// Tunnel is the main struct that configures and manages port forwading tunnels to Kubernetes resources.\ntype Tunnel struct {\n\tout            io.Writer\n\tlocalPort      int\n\tremotePort     int\n\tkubectlOptions *KubectlOptions\n\tresourceType   KubeResourceType\n\tresourceName   string\n\tlogger         logger.TestLogger\n\tstopChan       chan struct{}\n\treadyChan      chan struct{}\n}\n\n// NewTunnel creates a new tunnel with NewTunnelWithLogger, setting logger.Terratest as the logger.\nfunc NewTunnel(kubectlOptions *KubectlOptions, resourceType KubeResourceType, resourceName string, local int, remote int) *Tunnel {\n\treturn NewTunnelWithLogger(kubectlOptions, resourceType, resourceName, local, remote, logger.Terratest)\n}\n\n// NewTunnelWithLogger will create a new Tunnel struct with the provided logger.\n// Note that if you use 0 for the local port, an open port on the host system\n// will be selected automatically, and the Tunnel struct will be updated with the selected port.\nfunc NewTunnelWithLogger(\n\tkubectlOptions *KubectlOptions,\n\tresourceType KubeResourceType,\n\tresourceName string,\n\tlocal int,\n\tremote int,\n\tlogger logger.TestLogger,\n) *Tunnel {\n\treturn &Tunnel{\n\t\tout:            io.Discard,\n\t\tlocalPort:      local,\n\t\tremotePort:     remote,\n\t\tkubectlOptions: kubectlOptions,\n\t\tresourceType:   resourceType,\n\t\tresourceName:   resourceName,\n\t\tlogger:         logger,\n\t\tstopChan:       make(chan struct{}, 1),\n\t\treadyChan:      make(chan struct{}, 1),\n\t}\n}\n\n// Endpoint returns the tunnel endpoint\nfunc (tunnel *Tunnel) Endpoint() string {\n\treturn fmt.Sprintf(\"localhost:%d\", tunnel.localPort)\n}\n\n// Close disconnects a tunnel connection by closing the StopChan, thereby stopping the goroutine.\nfunc (tunnel *Tunnel) Close() {\n\tclose(tunnel.stopChan)\n}\n\n// getAttachablePodForResource will find a pod that can be port forwarded to given the provided resource type and return\n// the name.\nfunc (tunnel *Tunnel) getAttachablePodForResourceE(t testing.TestingT) (string, error) {\n\tswitch tunnel.resourceType {\n\tcase ResourceTypePod:\n\t\treturn tunnel.resourceName, nil\n\tcase ResourceTypeService:\n\t\treturn tunnel.getAttachablePodForServiceE(t)\n\tcase ResourceTypeDeployment:\n\t\treturn tunnel.getAttachablePodForDeploymentE(t)\n\tdefault:\n\t\treturn \"\", UnknownKubeResourceType{tunnel.resourceType}\n\t}\n}\n\n// getAttachablePodForDeploymentE will find an active pod associated with the Deployment and return the pod name.\nfunc (tunnel *Tunnel) getAttachablePodForDeploymentE(t testing.TestingT) (string, error) {\n\tdeploy, err := GetDeploymentE(t, tunnel.kubectlOptions, tunnel.resourceName)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tselectorLabelsOfPods := makeLabels(deploy.Spec.Selector.MatchLabels)\n\tdeploymentPods, err := ListPodsE(t, tunnel.kubectlOptions, metav1.ListOptions{LabelSelector: selectorLabelsOfPods})\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tfor _, pod := range deploymentPods {\n\t\tif IsPodAvailable(&pod) {\n\t\t\treturn pod.Name, nil\n\t\t}\n\t}\n\treturn \"\", DeploymentNotAvailable{deploy}\n}\n\n// getAttachablePodForServiceE will find an active pod associated with the Service and return the pod name.\nfunc (tunnel *Tunnel) getAttachablePodForServiceE(t testing.TestingT) (string, error) {\n\tservice, err := GetServiceE(t, tunnel.kubectlOptions, tunnel.resourceName)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tselectorLabelsOfPods := makeLabels(service.Spec.Selector)\n\tservicePods, err := ListPodsE(t, tunnel.kubectlOptions, metav1.ListOptions{LabelSelector: selectorLabelsOfPods})\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tfor _, pod := range servicePods {\n\t\tif IsPodAvailable(&pod) {\n\t\t\treturn pod.Name, nil\n\t\t}\n\t}\n\treturn \"\", ServiceNotAvailable{service}\n}\n\n// ForwardPort opens a tunnel to a kubernetes resource, as specified by the provided tunnel struct. This will fail the\n// test if there is an error attempting to open the port.\nfunc (tunnel *Tunnel) ForwardPort(t testing.TestingT) {\n\trequire.NoError(t, tunnel.ForwardPortE(t))\n}\n\n// ForwardPortE opens a tunnel to a kubernetes resource, as specified by the provided tunnel struct.\nfunc (tunnel *Tunnel) ForwardPortE(t testing.TestingT) error {\n\ttunnel.logger.Logf(\n\t\tt,\n\t\t\"Creating a port forwarding tunnel for resource %s/%s routing local port %d to remote port %d\",\n\t\ttunnel.resourceType.String(),\n\t\ttunnel.resourceName,\n\t\ttunnel.localPort,\n\t\ttunnel.remotePort,\n\t)\n\n\t// Prepare a kubernetes client for the client-go library\n\tclientset, err := GetKubernetesClientFromOptionsE(t, tunnel.kubectlOptions)\n\tif err != nil {\n\t\ttunnel.logger.Logf(t, \"Error creating a new Kubernetes client: %s\", err)\n\t\treturn err\n\t}\n\tconfig := tunnel.kubectlOptions.RestConfig\n\tif config == nil {\n\t\tkubeConfigPath, err := tunnel.kubectlOptions.GetConfigPath(t)\n\t\tif err != nil {\n\t\t\ttunnel.logger.Logf(t, \"Error getting kube config path: %s\", err)\n\t\t\treturn err\n\t\t}\n\t\tconfig, err = LoadApiClientConfigE(kubeConfigPath, tunnel.kubectlOptions.ContextName)\n\t\tif err != nil {\n\t\t\ttunnel.logger.Logf(t, \"Error loading Kubernetes config: %s\", err)\n\t\t\treturn err\n\t\t}\n\t}\n\n\t// Find the pod to port forward to\n\tpodName, err := tunnel.getAttachablePodForResourceE(t)\n\tif err != nil {\n\t\ttunnel.logger.Logf(t, \"Error finding available pod: %s\", err)\n\t\treturn err\n\t}\n\ttunnel.logger.Logf(t, \"Selected pod %s to open port forward to\", podName)\n\n\tvar targetPort = tunnel.remotePort\n\n\t// in case of services, find target port on pod based on service definition\n\tif tunnel.resourceType == ResourceTypeService {\n\t\tservice := GetService(t, tunnel.kubectlOptions, tunnel.resourceName)\n\t\tvar portFound = false\n\t\tfor _, portSpec := range service.Spec.Ports {\n\t\t\tif portSpec.Port == int32(targetPort) {\n\t\t\t\tif portSpec.TargetPort.Type == intstr.String {\n\t\t\t\t\tpod, err := GetPodE(t, tunnel.kubectlOptions, podName)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\t\t\t\t\ttargetPort, err = getPodPortByName(pod, portSpec.TargetPort.String())\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\ttunnel.logger.Logf(t, \"Error selecting port by name: %s\", err)\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\t\t\t\t\tportFound = true\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\ttargetPort = portSpec.TargetPort.IntValue()\n\t\t\t\tportFound = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif !portFound {\n\t\t\treturn errors.New(fmt.Sprintf(\"Target port %d not found in service %s definition.\", targetPort, tunnel.resourceName))\n\t\t}\n\t}\n\n\t// Build a url to the portforward endpoint\n\t// example: http://localhost:8080/api/v1/namespaces/helm/pods/tiller-deploy-9itlq/portforward\n\tpostEndpoint := clientset.CoreV1().RESTClient().Post()\n\tnamespace := tunnel.kubectlOptions.Namespace\n\tportForwardCreateURL := postEndpoint.\n\t\tResource(\"pods\").\n\t\tNamespace(namespace).\n\t\tName(podName).\n\t\tSubResource(\"portforward\").\n\t\tURL()\n\n\ttunnel.logger.Logf(t, \"Using URL %s to create portforward\", portForwardCreateURL)\n\n\t// Construct the spdy client required by the client-go portforward library\n\ttransport, upgrader, err := spdy.RoundTripperFor(config)\n\tif err != nil {\n\t\ttunnel.logger.Logf(t, \"Error creating http client: %s\", err)\n\t\treturn err\n\t}\n\tdialer := spdy.NewDialer(upgrader, &http.Client{Transport: transport}, \"POST\", portForwardCreateURL)\n\n\t// If the localport is 0, get an available port before continuing. We do this here instead of relying on the\n\t// underlying portforwarder library, because the portforwarder library does not expose the selected local port in a\n\t// machine readable manner.\n\t// Synchronize on the global lock to avoid race conditions with concurrently selecting the same available port,\n\t// since there is a brief moment between `GetAvailablePort` and `portforwader.ForwardPorts` where the selected port\n\t// is available for selection again.\n\tif tunnel.localPort == 0 {\n\t\ttunnel.logger.Logf(t, \"Requested local port is 0. Selecting an open port on host system\")\n\t\ttunnel.localPort, err = GetAvailablePortE(t)\n\t\tif err != nil {\n\t\t\ttunnel.logger.Logf(t, \"Error getting available port: %s\", err)\n\t\t\treturn err\n\t\t}\n\t\ttunnel.logger.Logf(t, \"Selected port %d\", tunnel.localPort)\n\t\tglobalMutex.Lock()\n\t\tdefer globalMutex.Unlock()\n\t}\n\n\t// Construct a new PortForwarder struct that manages the instructed port forward tunnel\n\tports := []string{fmt.Sprintf(\"%d:%d\", tunnel.localPort, targetPort)}\n\tportforwarder, err := portforward.New(dialer, ports, tunnel.stopChan, tunnel.readyChan, tunnel.out, tunnel.out)\n\tif err != nil {\n\t\ttunnel.logger.Logf(t, \"Error creating port forwarding tunnel: %s\", err)\n\t\treturn err\n\t}\n\n\t// Open the tunnel in a goroutine so that it is available in the background. Report errors to the main goroutine via\n\t// a new channel.\n\terrChan := make(chan error)\n\tgo func() {\n\t\terrChan <- portforwarder.ForwardPorts()\n\t}()\n\n\t// Wait for an error or the tunnel to be ready\n\tselect {\n\tcase err = <-errChan:\n\t\ttunnel.logger.Logf(t, \"Error starting port forwarding tunnel: %s\", err)\n\t\treturn err\n\tcase <-portforwarder.Ready:\n\t\ttunnel.logger.Logf(t, \"Successfully created port forwarding tunnel\")\n\t\treturn nil\n\t}\n}\n\n// GetAvailablePort retrieves an available port on the host machine. This delegates the port selection to the golang net\n// library by starting a server and then checking the port that the server is using. This will fail the test if it could\n// not find an available port.\nfunc GetAvailablePort(t testing.TestingT) int {\n\tport, err := GetAvailablePortE(t)\n\trequire.NoError(t, err)\n\treturn port\n}\n\n// GetAvailablePortE retrieves an available port on the host machine. This delegates the port selection to the golang net\n// library by starting a server and then checking the port that the server is using.\nfunc GetAvailablePortE(t testing.TestingT) (int, error) {\n\tl, err := net.Listen(\"tcp\", \":0\")\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\tdefer l.Close()\n\n\t_, p, err := net.SplitHostPort(l.Addr().String())\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\tport, err := strconv.Atoi(p)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\treturn port, err\n}\n\nfunc getPodPortByName(pod *corev1.Pod, portName string) (int, error) {\n\tif pod == nil {\n\t\treturn 0, errors.New(\"cannot get port for pod which is nil\")\n\t}\n\tfor _, container := range pod.Spec.Containers {\n\t\tfor _, port := range container.Ports {\n\t\t\tif port.Name == portName {\n\t\t\t\treturn int(port.ContainerPort), nil\n\t\t\t}\n\t\t}\n\t}\n\treturn 0, fmt.Errorf(\"could not find port %s in pod %s\", portName, pod.Name)\n}\n"
  },
  {
    "path": "modules/k8s/tunnel_test.go",
    "content": "//go:build kubeall || kubernetes\n// +build kubeall kubernetes\n\n// NOTE: we have build tags to differentiate kubernetes tests from non-kubernetes tests. This is done because minikube\n// is heavy and can interfere with docker related tests in terratest. Specifically, many of the tests start to fail with\n// `connection refused` errors from `minikube`. To avoid overloading the system, we run the kubernetes tests and helm\n// tests separately from the others. This may not be necessary if you have a sufficiently powerful machine.  We\n// recommend at least 4 cores and 16GB of RAM if you want to run all the tests together.\n\npackage k8s\n\nimport (\n\t\"crypto/tls\"\n\t\"fmt\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\thttp_helper \"github.com/gruntwork-io/terratest/modules/http-helper\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n)\n\nfunc TestTunnelOpensAPortForwardTunnelToPod(t *testing.T) {\n\tt.Parallel()\n\n\tuniqueID := strings.ToLower(random.UniqueId())\n\toptions := NewKubectlOptions(\"\", \"\", uniqueID)\n\tconfigData := fmt.Sprintf(EXAMPLE_POD_YAML_TEMPLATE, uniqueID, uniqueID)\n\tdefer KubectlDeleteFromString(t, options, configData)\n\tKubectlApplyFromString(t, options, configData)\n\tWaitUntilPodAvailable(t, options, \"nginx-pod\", 60, 1*time.Second)\n\n\t// Open a tunnel to pod from any available port locally\n\ttunnel := NewTunnel(options, ResourceTypePod, \"nginx-pod\", 0, 80)\n\tdefer tunnel.Close()\n\ttunnel.ForwardPort(t)\n\n\t// Setup a TLS configuration to submit with the helper, a blank struct is acceptable\n\ttlsConfig := tls.Config{}\n\n\t// Try to access the nginx service on the local port, retrying until we get a good response for up to 5 minutes\n\thttp_helper.HttpGetWithRetryWithCustomValidation(\n\t\tt,\n\t\tfmt.Sprintf(\"http://%s\", tunnel.Endpoint()),\n\t\t&tlsConfig,\n\t\t60,\n\t\t5*time.Second,\n\t\tverifyNginxWelcomePage,\n\t)\n}\n\nfunc TestTunnelOpensAPortForwardTunnelToDeployment(t *testing.T) {\n\tt.Parallel()\n\n\tuniqueID := strings.ToLower(random.UniqueId())\n\toptions := NewKubectlOptions(\"\", \"\", uniqueID)\n\tconfigData := fmt.Sprintf(ExampleDeploymentYAMLTemplate, uniqueID)\n\tKubectlApplyFromString(t, options, configData)\n\tdefer KubectlDeleteFromString(t, options, configData)\n\tWaitUntilDeploymentAvailable(t, options, \"nginx-deployment\", 60, 1*time.Second)\n\n\t// Open a tunnel to pod from any available port locally\n\ttunnel := NewTunnel(options, ResourceTypeDeployment, \"nginx-deployment\", 0, 80)\n\tdefer tunnel.Close()\n\ttunnel.ForwardPort(t)\n\n\t// Setup a TLS configuration to submit with the helper, a blank struct is acceptable\n\ttlsConfig := tls.Config{}\n\n\t// Try to access the nginx service on the local port, retrying until we get a good response for up to 5 minutes\n\thttp_helper.HttpGetWithRetryWithCustomValidation(\n\t\tt,\n\t\tfmt.Sprintf(\"http://%s\", tunnel.Endpoint()),\n\t\t&tlsConfig,\n\t\t60,\n\t\t5*time.Second,\n\t\tverifyNginxWelcomePage,\n\t)\n}\n\nfunc TestTunnelOpensAPortForwardTunnelToService(t *testing.T) {\n\tt.Parallel()\n\n\tuniqueID := strings.ToLower(random.UniqueId())\n\toptions := NewKubectlOptions(\"\", \"\", uniqueID)\n\tconfigData := fmt.Sprintf(ExamplePodWithServiceYAMLTemplate, uniqueID, uniqueID, uniqueID, uniqueID)\n\tt.Cleanup(func() {\n\t\tKubectlDeleteFromString(t, options, configData)\n\t})\n\tKubectlApplyFromString(t, options, configData)\n\t// t.FailNow()\n\tWaitUntilPodAvailable(t, options, \"nginx-pod\", 60, 1*time.Second)\n\n\ttestCases := []struct {\n\t\tname        string\n\t\tserviceName string\n\t}{\n\t\t{\n\t\t\t\"Pod target port by number\",\n\t\t\t\"nginx-service-number\",\n\t\t},\n\t\t{\n\t\t\t\"Pod target port by name\",\n\t\t\t\"nginx-service-name\",\n\t\t},\n\t}\n\n\tfor _, testCase := range testCases {\n\t\tt.Run(testCase.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tWaitUntilServiceAvailable(t, options, testCase.serviceName, 60, 1*time.Second)\n\n\t\t\t// Open a tunnel from any available port locally\n\t\t\ttunnel := NewTunnel(options, ResourceTypeService, testCase.serviceName, 0, 8080)\n\t\t\tt.Cleanup(func() {\n\t\t\t\ttunnel.Close()\n\t\t\t})\n\t\t\ttunnel.ForwardPort(t)\n\n\t\t\t// Setup a TLS configuration to submit with the helper, a blank struct is acceptable\n\t\t\ttlsConfig := tls.Config{}\n\n\t\t\t// Try to access the nginx service on the local port, retrying until we get a good response for up to 5 minutes\n\t\t\thttp_helper.HttpGetWithRetryWithCustomValidation(\n\t\t\t\tt,\n\t\t\t\tfmt.Sprintf(\"http://%s\", tunnel.Endpoint()),\n\t\t\t\t&tlsConfig,\n\t\t\t\t60,\n\t\t\t\t5*time.Second,\n\t\t\t\tverifyNginxWelcomePage,\n\t\t\t)\n\t\t})\n\t}\n}\n\nfunc verifyNginxWelcomePage(statusCode int, body string) bool {\n\tif statusCode != 200 {\n\t\treturn false\n\t}\n\treturn strings.Contains(body, \"Welcome to nginx\")\n}\n\nconst ExamplePodWithServiceYAMLTemplate = `---\napiVersion: v1\nkind: Namespace\nmetadata:\n  name: %s\n---\napiVersion: v1\nkind: Pod\nmetadata:\n  name: nginx-pod\n  namespace: %s\n  labels:\n    app: nginx\nspec:\n  containers:\n  - name: nginx\n    image: nginx:1.15.7\n    ports:\n    - containerPort: 80\n      name: http\n    readinessProbe:\n      httpGet:\n        path: /\n        port: 80\n---\napiVersion: v1\nkind: Service\nmetadata:\n  name: nginx-service-number\n  namespace: %s\nspec:\n  selector:\n    app: nginx\n  ports:\n  - protocol: TCP\n    targetPort: 80\n    port: 8080\n---\napiVersion: v1\nkind: Service\nmetadata:\n  name: nginx-service-name\n  namespace: %s\nspec:\n  selector:\n    app: nginx\n  ports:\n  - protocol: TCP\n    targetPort: http\n    port: 8080\n`\n"
  },
  {
    "path": "modules/k8s/version.go",
    "content": "package k8s\n\nimport \"github.com/gruntwork-io/terratest/modules/testing\"\n\n// GetKubernetesClusterVersion returns the Kubernetes cluster version.\nfunc GetKubernetesClusterVersionE(t testing.TestingT) (string, error) {\n\tkubeConfigPath, err := GetKubeConfigPathE(t)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\toptions := NewKubectlOptions(\"\", kubeConfigPath, \"default\")\n\n\treturn GetKubernetesClusterVersionWithOptionsE(t, options)\n}\n\n// GetKubernetesClusterVersion returns the Kubernetes cluster version given a configured KubectlOptions object.\nfunc GetKubernetesClusterVersionWithOptionsE(t testing.TestingT, kubectlOptions *KubectlOptions) (string, error) {\n\tclientset, err := GetKubernetesClientFromOptionsE(t, kubectlOptions)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tversionInfo, err := clientset.DiscoveryClient.ServerVersion()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn versionInfo.String(), nil\n}\n"
  },
  {
    "path": "modules/k8s/version_test.go",
    "content": "//go:build kubeall || kubernetes\n// +build kubeall kubernetes\n\n// NOTE: we have build tags to differentiate kubernetes tests from non-kubernetes tests. This is done because minikube\n// is heavy and can interfere with docker related tests in terratest. Specifically, many of the tests start to fail with\n// `connection refused` errors from `minikube`. To avoid overloading the system, we run the kubernetes tests and helm\n// tests separately from the others. This may not be necessary if you have a sufficiently powerful machine.  We\n// recommend at least 4 cores and 16GB of RAM if you want to run all the tests together.\n\npackage k8s\n\nimport (\n\t\"encoding/json\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\ntype KubectlVersion struct {\n\tServerVersion struct {\n\t\tGitVersion string `json:\"gitVersion\"`\n\t} `json:\"serverVersion\"`\n}\n\nfunc TestGetKubernetesClusterVersionE(t *testing.T) {\n\tt.Parallel()\n\n\tkubernetesClusterVersion, err := GetKubernetesClusterVersionE(t)\n\trequire.NoError(t, err)\n\n\toptions := NewKubectlOptions(\"\", \"\", \"\")\n\tkubernetesClusterVersionFromKubectl, err := RunKubectlAndGetOutputE(t, options, \"version\", \"-o\", \"json\")\n\trequire.NoError(t, err)\n\n\tvar kctlClusterVersion KubectlVersion\n\trequire.NoError(\n\t\tt,\n\t\tjson.Unmarshal([]byte(kubernetesClusterVersionFromKubectl), &kctlClusterVersion),\n\t)\n\n\tassert.EqualValues(t, kubernetesClusterVersion, kctlClusterVersion.ServerVersion.GitVersion)\n}\n\nfunc TestGetKubernetesClusterVersionWithOptionsE(t *testing.T) {\n\tt.Parallel()\n\n\toptions := NewKubectlOptions(\"\", \"\", \"\")\n\tkubernetesClusterVersion, err := GetKubernetesClusterVersionWithOptionsE(t, options)\n\trequire.NoError(t, err)\n\n\tkubernetesClusterVersionFromKubectl, err := RunKubectlAndGetOutputE(t, options, \"version\", \"-o\", \"json\")\n\trequire.NoError(t, err)\n\n\tvar kctlClusterVersion KubectlVersion\n\trequire.NoError(\n\t\tt,\n\t\tjson.Unmarshal([]byte(kubernetesClusterVersionFromKubectl), &kctlClusterVersion),\n\t)\n\n\tassert.EqualValues(t, kubernetesClusterVersion, kctlClusterVersion.ServerVersion.GitVersion)\n}\n"
  },
  {
    "path": "modules/logger/logger.go",
    "content": "// Package logger contains different methods to log.\npackage logger\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"runtime\"\n\t\"strings\"\n\t\"sync\"\n\tgotesting \"testing\"\n\t\"time\"\n\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n)\n\nvar (\n\t// Default is the default logger that is used for the Logf function, if no one is provided. It uses the\n\t// TerratestLogger to log messages. This can be overwritten to change the logging globally.\n\tDefault = New(terratestLogger{})\n\t// Discard discards all logging.\n\tDiscard = New(discardLogger{})\n\t// Terratest logs the given format and arguments, formatted using fmt.Sprintf, to stdout, along with a timestamp and\n\t// information about what test and file is doing the logging. Before Go 1.14, this is an alternative to t.Logf as it\n\t// logs to stdout immediately, rather than buffering all log output and only displaying it at the very end of the test.\n\t// This is useful because:\n\t//\n\t// 1. It allows you to iterate faster locally, as you get feedback on whether your code changes are working as expected\n\t//    right away, rather than at the very end of the test run.\n\t//\n\t// 2. If you have a bug in your code that causes a test to never complete or if the test code crashes, t.Logf would\n\t//    show you no log output whatsoever, making debugging very hard, where as this method will show you all the log\n\t//    output available.\n\t//\n\t// 3. If you have a test that takes a long time to complete, some CI systems will kill the test suite prematurely\n\t//    because there is no log output with t.Logf (e.g., CircleCI kills tests after 10 minutes of no log output). With\n\t//    this log method, you get log output continuously.\n\t//\n\tTerratest = New(terratestLogger{})\n\t// TestingT can be used to use Go's testing.T to log. If this is used, but no testing.T is provided, it will fallback\n\t// to Default.\n\tTestingT = New(testingT{})\n)\n\ntype TestLogger interface {\n\tLogf(t testing.TestingT, format string, args ...interface{})\n}\n\ntype Logger struct {\n\tl TestLogger\n}\n\nfunc New(l TestLogger) *Logger {\n\treturn &Logger{\n\t\tl,\n\t}\n}\n\nfunc (l *Logger) Logf(t testing.TestingT, format string, args ...interface{}) {\n\tif tt, ok := t.(helper); ok {\n\t\ttt.Helper()\n\t}\n\n\t// methods can be called on (typed) nil pointers. In this case, use the Default function to log. This enables the\n\t// caller to do `var l *Logger` and then use the logger already.\n\tif l == nil || l.l == nil {\n\t\tDefault.Logf(t, format, args...)\n\t\treturn\n\t}\n\n\tl.l.Logf(t, format, args...)\n}\n\n// helper is used to mark this library as a \"helper\", and thus not appearing in the line numbers. testing.T implements\n// this interface, for example.\ntype helper interface {\n\tHelper()\n}\n\ntype discardLogger struct{}\n\nfunc (_ discardLogger) Logf(_ testing.TestingT, format string, args ...interface{}) {}\n\ntype testingT struct{}\n\nfunc (_ testingT) Logf(t testing.TestingT, format string, args ...interface{}) {\n\t// this should never fail\n\ttt, ok := t.(*gotesting.T)\n\tif !ok {\n\t\t// fallback\n\t\tDoLog(t, 2, os.Stdout, fmt.Sprintf(format, args...))\n\t\treturn\n\t}\n\n\ttt.Helper()\n\ttt.Logf(format, args...)\n\treturn\n}\n\ntype terratestLogger struct{}\n\nfunc (_ terratestLogger) Logf(t testing.TestingT, format string, args ...interface{}) {\n\tDoLog(t, 3, os.Stdout, fmt.Sprintf(format, args...))\n}\n\n// Deprecated: use Logger instead, as it provides more flexibility on logging.\n// Logf logs the given format and arguments, formatted using fmt.Sprintf, to stdout, along with a timestamp and information\n// about what test and file is doing the logging. Before Go 1.14, this is an alternative to t.Logf as it logs to stdout\n// immediately, rather than buffering all log output and only displaying it at the very end of the test. This is useful\n// because:\n//\n//  1. It allows you to iterate faster locally, as you get feedback on whether your code changes are working as expected\n//     right away, rather than at the very end of the test run.\n//\n//  2. If you have a bug in your code that causes a test to never complete or if the test code crashes, t.Logf would\n//     show you no log output whatsoever, making debugging very hard, where as this method will show you all the log\n//     output available.\n//\n//  3. If you have a test that takes a long time to complete, some CI systems will kill the test suite prematurely\n//     because there is no log output with t.Logf (e.g., CircleCI kills tests after 10 minutes of no log output). With\n//     this log method, you get log output continuously.\n//\n// Although t.Logf now supports streaming output since Go 1.14, this is kept for compatibility purposes.\nfunc Logf(t testing.TestingT, format string, args ...interface{}) {\n\tif tt, ok := t.(helper); ok {\n\t\ttt.Helper()\n\t}\n\n\tmutexStdout.Lock()\n\tdefer mutexStdout.Unlock()\n\tDoLog(t, 2, os.Stdout, fmt.Sprintf(format, args...))\n}\n\n// Log logs the given arguments to stdout, along with a timestamp and information about what test and file is doing the\n// logging. This is an alternative to t.Logf that logs to stdout immediately, rather than buffering all log output and\n// only displaying it at the very end of the test. See the Logf method for more info.\nfunc Log(t testing.TestingT, args ...interface{}) {\n\tif tt, ok := t.(helper); ok {\n\t\ttt.Helper()\n\t}\n\n\tmutexStdout.Lock()\n\tdefer mutexStdout.Unlock()\n\tDoLog(t, 2, os.Stdout, args...)\n}\n\nvar mutexStdout sync.Mutex\n\n// DoLog logs the given arguments to the given writer, along with a timestamp and information about what test and file is\n// doing the logging.\nfunc DoLog(t testing.TestingT, callDepth int, writer io.Writer, args ...interface{}) {\n\tdate := time.Now()\n\tprefix := fmt.Sprintf(\"%s %s %s:\", t.Name(), date.Format(time.RFC3339), CallerPrefix(callDepth+1))\n\tallArgs := append([]interface{}{prefix}, args...)\n\tfmt.Fprintln(writer, allArgs...)\n}\n\n// CallerPrefix returns the file and line number information about the methods that called this method, based on the current\n// goroutine's stack. The argument callDepth is the number of stack frames to ascend, with 0 identifying the method\n// that called CallerPrefix, 1 identifying the method that called that method, and so on.\n//\n// This code is adapted from testing.go, where it is in a private method called decorate.\nfunc CallerPrefix(callDepth int) string {\n\t_, file, line, ok := runtime.Caller(callDepth)\n\tif ok {\n\t\t// Truncate file name at last file name separator.\n\t\tif index := strings.LastIndex(file, \"/\"); index >= 0 {\n\t\t\tfile = file[index+1:]\n\t\t} else if index = strings.LastIndex(file, \"\\\\\"); index >= 0 {\n\t\t\tfile = file[index+1:]\n\t\t}\n\t} else {\n\t\tfile = \"???\"\n\t\tline = 1\n\t}\n\n\treturn fmt.Sprintf(\"%s:%d\", file, line)\n}\n"
  },
  {
    "path": "modules/logger/logger_test.go",
    "content": "package logger\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\ttftesting \"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestDoLog(t *testing.T) {\n\tt.Parallel()\n\n\ttext := \"test-do-log\"\n\tvar buffer bytes.Buffer\n\n\tDoLog(t, 1, &buffer, text)\n\n\tassert.Regexp(t, fmt.Sprintf(\"^%s .+? [[:word:]]+.go:[0-9]+: %s$\", t.Name(), text), strings.TrimSpace(buffer.String()))\n}\n\ntype customLogger struct {\n\tlogs []string\n}\n\nfunc (c *customLogger) Logf(t tftesting.TestingT, format string, args ...interface{}) {\n\tc.logs = append(c.logs, fmt.Sprintf(format, args...))\n}\n\nfunc TestCustomLogger(t *testing.T) {\n\tLogf(t, \"this should be logged with the default logger\")\n\n\tvar l *Logger\n\tl.Logf(t, \"this should be logged with the default logger too\")\n\n\tl = New(nil)\n\tl.Logf(t, \"this should be logged with the default logger too!\")\n\n\tc := &customLogger{}\n\tl = New(c)\n\tl.Logf(t, \"log output 1\")\n\tl.Logf(t, \"log output 2\")\n\n\tt.Run(\"logger-subtest\", func(t *testing.T) {\n\t\tl.Logf(t, \"subtest log\")\n\t})\n\n\tassert.Len(t, c.logs, 3)\n\tassert.Equal(t, \"log output 1\", c.logs[0])\n\tassert.Equal(t, \"log output 2\", c.logs[1])\n\tassert.Equal(t, \"subtest log\", c.logs[2])\n}\n\n// TestLockedLog make sure that Log and Logf which use stdout are thread-safe\nfunc TestLockedLog(t *testing.T) {\n\t// should not call t.Parallel() since we are modifying os.Stdout\n\tstdout := os.Stdout\n\tt.Cleanup(func() {\n\t\tos.Stdout = stdout\n\t})\n\n\tdata := []struct {\n\t\tname string\n\t\tfn   func(*testing.T, string)\n\t}{\n\t\t{\n\t\t\tname: \"Log\",\n\t\t\tfn: func(t *testing.T, s string) {\n\t\t\t\tLog(t, s)\n\t\t\t}},\n\t\t{\n\t\t\tname: \"Logf\",\n\t\t\tfn: func(t *testing.T, s string) {\n\t\t\t\tLogf(t, \"%s\", s)\n\t\t\t}},\n\t}\n\n\tfor _, d := range data {\n\t\tmutexStdout.Lock()\n\t\tstr := \"Logging something\" + t.Name()\n\n\t\tr, w, _ := os.Pipe()\n\t\tos.Stdout = w\n\t\tch := make(chan struct{})\n\t\tgo func() {\n\t\t\td.fn(t, str)\n\t\t\tw.Close()\n\t\t\tclose(ch)\n\t\t}()\n\n\t\tselect {\n\t\tcase <-ch:\n\t\t\tt.Error(\"Log should be locked\")\n\t\tdefault:\n\t\t}\n\n\t\tmutexStdout.Unlock()\n\t\tb, err := io.ReadAll(r)\n\t\trequire.NoError(t, err, \"log should be unlocked\")\n\t\tassert.Contains(t, string(b), str, \"should contains logged string\")\n\t}\n\n}\n"
  },
  {
    "path": "modules/logger/parser/failed_test_marker.go",
    "content": "// Package logger/parser contains methods to parse and restructure log output from go testing and terratest\npackage parser\n\n// TestResultMarker tracks the indentation level of a test result line in go test output.\n// Example:\n// --- FAIL: TestSnafu\n//\n//\t--- PASS: TestSnafu/Situation\n//\t--- FAIL: TestSnafu/Normal\n//\n// The three markers for the above in order are:\n// TestResultMarker{TestName: \"TestSnafu\", IndentLevel: 0}\n// TestResultMarker{TestName: \"TestSnafu/Situation\", IndentLevel: 4}\n// TestResultMarker{TestName: \"TestSnafu/Normal\", IndentLevel: 4}\ntype TestResultMarker struct {\n\tTestName    string\n\tIndentLevel int\n}\n\n// TestResultMarkerStack is a stack data structure to store TestResultMarkers\ntype TestResultMarkerStack []TestResultMarker\n\n// A blank TestResultMarker is considered null. Used when peeking or popping an empty stack.\nvar NULL_TEST_RESULT_MARKER = TestResultMarker{}\n\n// TestResultMarker.push will push a TestResultMarker object onto the stack, returning the new one.\nfunc (s TestResultMarkerStack) push(v TestResultMarker) TestResultMarkerStack {\n\treturn append(s, v)\n}\n\n// TestResultMarker.pop will pop a TestResultMarker object off of the stack, returning the new one with the popped\n// marker.\n// When stack is empty, will return an empty object.\nfunc (s TestResultMarkerStack) pop() (TestResultMarkerStack, TestResultMarker) {\n\tl := len(s)\n\tif l == 0 {\n\t\treturn s, NULL_TEST_RESULT_MARKER\n\t}\n\treturn s[:l-1], s[l-1]\n}\n\n// TestResultMarker.peek will return the top TestResultMarker from the stack, but will not remove it.\nfunc (s TestResultMarkerStack) peek() TestResultMarker {\n\tl := len(s)\n\tif l == 0 {\n\t\treturn NULL_TEST_RESULT_MARKER\n\t}\n\treturn s[l-1]\n}\n\n// TestResultMarker.isEmpty will return whether or not the stack is empty.\nfunc (s TestResultMarkerStack) isEmpty() bool {\n\treturn len(s) == 0\n}\n\n// removeDedentedTestResultMarkers will pop items off of the stack of TestResultMarker objects until the top most item\n// has an indent level less than the current indent level.\n// Assumes that the stack is ordered, in that recently pushed items in the stack have higher indent levels.\nfunc (s TestResultMarkerStack) removeDedentedTestResultMarkers(currentIndentLevel int) TestResultMarkerStack {\n\t// This loop is a garbage collection of the stack, where it removes entries every time we dedent out of a fail\n\t// block.\n\tfor !s.isEmpty() && s.peek().IndentLevel >= currentIndentLevel {\n\t\ts, _ = s.pop()\n\t}\n\treturn s\n}\n"
  },
  {
    "path": "modules/logger/parser/failed_test_marker_test.go",
    "content": "package parser\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc createTestStack() TestResultMarkerStack {\n\treturn TestResultMarkerStack{\n\t\tTestResultMarker{\n\t\t\tTestName:    \"TestSnafu\",\n\t\t\tIndentLevel: 0,\n\t\t},\n\t\tTestResultMarker{\n\t\t\tTestName:    \"TestSnafu/Situation\",\n\t\t\tIndentLevel: 4,\n\t\t},\n\t\tTestResultMarker{\n\t\t\tTestName:    \"TestSnafu/Normal\",\n\t\t\tIndentLevel: 4,\n\t\t},\n\t}\n}\n\n// Test that pushing items to the stack appends to the list\nfunc TestStackPush(t *testing.T) {\n\tt.Parallel()\n\n\tmarkers := createTestStack()\n\tnewMarker := TestResultMarker{\n\t\tTestName:    \"TestThatEverythingWorks\",\n\t\tIndentLevel: 0,\n\t}\n\tmarkers = markers.push(newMarker)\n\tassert.Equal(t, len(markers), 4)\n\tassert.Equal(t, markers[3], newMarker)\n}\n\n// Test that popping items off the stack will remove it from the stack and return the LAST item in list\nfunc TestStackPop(t *testing.T) {\n\tt.Parallel()\n\n\toriginalMarkers := createTestStack()\n\tmarkers := createTestStack()\n\tmarkers, poppedMarker := markers.pop()\n\tassert.Equal(t, poppedMarker, originalMarkers[2])\n\tassert.Equal(t, len(markers), 2)\n\tmarkers, poppedMarker = markers.pop()\n\tassert.Equal(t, poppedMarker, originalMarkers[1])\n\tassert.Equal(t, len(markers), 1)\n\tmarkers, poppedMarker = markers.pop()\n\tassert.Equal(t, poppedMarker, originalMarkers[0])\n\tassert.Equal(t, len(markers), 0)\n}\n\n// Test that popping item off an empty stack will return an empty TestResultMarker\nfunc TestStackPopEmpty(t *testing.T) {\n\tt.Parallel()\n\n\tmarkers := TestResultMarkerStack{}\n\tmarkers, poppedMarker := markers.pop()\n\tassert.Equal(t, len(markers), 0)\n\tassert.Equal(t, poppedMarker, NULL_TEST_RESULT_MARKER)\n}\n\n// Test that peek will return the LAST item in the list WITHOUT removing it.\nfunc TestPeek(t *testing.T) {\n\tt.Parallel()\n\n\toriginalMarkers := createTestStack()\n\tmarkers := createTestStack()\n\tpeekedMarker := markers.peek()\n\tassert.Equal(t, peekedMarker, originalMarkers[2])\n\tassert.Equal(t, originalMarkers, markers)\n}\n\n// Test that peeking an empty stack will return an empty TestResultMarker\nfunc TestPeekEmpty(t *testing.T) {\n\tt.Parallel()\n\n\tmarkers := TestResultMarkerStack{}\n\tpeekedMarker := markers.peek()\n\tassert.Equal(t, len(markers), 0)\n\tassert.Equal(t, peekedMarker, NULL_TEST_RESULT_MARKER)\n}\n\n// Test isEmpty only returns True on empty stack\nfunc TestIsEmpty(t *testing.T) {\n\tt.Parallel()\n\n\temptyMarkerStack := TestResultMarkerStack{}\n\tfullMarkerStack := createTestStack()\n\tassert.True(t, emptyMarkerStack.isEmpty())\n\tassert.False(t, fullMarkerStack.isEmpty())\n}\n\n// Test that removeDedentedTestResultMarkers remove items that are dedented from the current level, assuming the stack\n// is ordered by indent level.\nfunc TestRemoveDedentedTestResultMarkers(t *testing.T) {\n\tt.Parallel()\n\n\toriginalMarkers := createTestStack()\n\tnewMarkers := originalMarkers.removeDedentedTestResultMarkers(2)\n\tassert.Equal(t, len(newMarkers), 1)\n\tassert.Equal(t, newMarkers, originalMarkers[:1])\n}\n\n// Test that removeDedentedTestResultMarkers handles empty stack.\nfunc TestRemoveDedentedTestResultMarkersEmpty(t *testing.T) {\n\tt.Parallel()\n\n\toriginalMarkers := TestResultMarkerStack{}\n\tnewMarkers := originalMarkers.removeDedentedTestResultMarkers(2)\n\tassert.Equal(t, len(newMarkers), 0)\n}\n\n// Test that removeDedentedTestResultMarkers handles removing everything\nfunc TestRemoveDedentedTestResultMarkersAll(t *testing.T) {\n\tt.Parallel()\n\n\toriginalMarkers := TestResultMarkerStack{}\n\tnewMarkers := originalMarkers.removeDedentedTestResultMarkers(-1)\n\tassert.Equal(t, len(newMarkers), 0)\n}\n"
  },
  {
    "path": "modules/logger/parser/fixtures/basic_example.log",
    "content": "=== RUN   TestStackPush\n=== PAUSE TestStackPush\n=== RUN   TestStackPop\n=== PAUSE TestStackPop\n=== RUN   TestStackPopEmpty\n=== PAUSE TestStackPopEmpty\n=== RUN   TestPeek\n=== PAUSE TestPeek\n=== RUN   TestPeekEmpty\n=== PAUSE TestPeekEmpty\n=== RUN   TestIsEmpty\n=== PAUSE TestIsEmpty\n=== RUN   TestRemoveDedentedTestResultMarkers\n--- PASS: TestRemoveDedentedTestResultMarkers (0.00s)\n=== RUN   TestRemoveDedentedTestResultMarkersEmpty\n--- PASS: TestRemoveDedentedTestResultMarkersEmpty (0.00s)\n=== RUN   TestRemoveDedentedTestResultMarkersAll\n--- PASS: TestRemoveDedentedTestResultMarkersAll (0.00s)\n=== RUN   TestGetIndent\n=== PAUSE TestGetIndent\n=== RUN   TestGetTestNameFromResultLine\n=== PAUSE TestGetTestNameFromResultLine\n=== RUN   TestIsResultLine\n=== PAUSE TestIsResultLine\n=== RUN   TestGetTestNameFromStatusLine\n=== PAUSE TestGetTestNameFromStatusLine\n=== RUN   TestIsStatusLine\n=== PAUSE TestIsStatusLine\n=== RUN   TestIsSummaryLine\n=== PAUSE TestIsSummaryLine\n=== RUN   TestIsPanicLine\n=== PAUSE TestIsPanicLine\n=== RUN   TestEnsureDirectoryExistsCreatesDirectory\n=== PAUSE TestEnsureDirectoryExistsCreatesDirectory\n=== RUN   TestEnsureDirectoryExistsHandlesExistingDirectory\n=== PAUSE TestEnsureDirectoryExistsHandlesExistingDirectory\n=== RUN   TestGetOrCreateChannelCreatesNewChannel\n=== PAUSE TestGetOrCreateChannelCreatesNewChannel\n=== RUN   TestGetOrCreateChannelReturnsExistingChannel\n=== PAUSE TestGetOrCreateChannelReturnsExistingChannel\n=== RUN   TestLogCollectorCreatesAndWritesToFile\n=== PAUSE TestLogCollectorCreatesAndWritesToFile\n=== RUN   TestGetOrCreateChannelSpawnsLogCollectorOnCreate\n=== PAUSE TestGetOrCreateChannelSpawnsLogCollectorOnCreate\n=== RUN   TestCloseChannelsClosesAll\n=== PAUSE TestCloseChannelsClosesAll\n=== CONT  TestStackPush\n=== CONT  TestIsSummaryLine\n=== CONT  TestGetOrCreateChannelReturnsExistingChannel\n=== RUN   TestIsSummaryLine/BaseCase\n=== CONT  TestCloseChannelsClosesAll\n--- PASS: TestStackPush (0.00s)\n=== CONT  TestGetOrCreateChannelSpawnsLogCollectorOnCreate\n=== RUN   TestIsSummaryLine/NotSummary\n--- PASS: TestIsSummaryLine (0.00s)\n    --- PASS: TestIsSummaryLine/BaseCase (0.00s)\n    --- PASS: TestIsSummaryLine/NotSummary (0.00s)\n=== CONT  TestLogCollectorCreatesAndWritesToFile\nTestCloseChannelsClosesAll INFO 2018-10-20T13:03:33-07:00 Closing all the channels in log writer\nTestGetOrCreateChannelSpawnsLogCollectorOnCreate INFO 2018-10-20T13:03:33-07:00 Spawned log writer for test TestGetOrCreateChannelSpawnsLogCollectorOnCreate\nTestGetOrCreateChannelSpawnsLogCollectorOnCreate INFO 2018-10-20T13:03:33-07:00 Storing logs for test TestGetOrCreateChannelSpawnsLogCollectorOnCreate to /var/folders/n2/pljz6dq52bd1ksmw23qyr3sr0000gn/T/TestEnsureDirectoryCreatesDirectory894837527/TestGetOrCreateChannelSpawnsLogCollectorOnCreate.log\n--- PASS: TestCloseChannelsClosesAll (0.00s)\n=== CONT  TestGetIndent\n=== RUN   TestGetIndent/BaseCase\n=== CONT  TestPeek\nTestLogCollectorCreatesAndWritesToFile INFO 2018-10-20T13:03:33-07:00 Spawned log writer for test TestLogCollectorCreatesAndWritesToFile\n--- PASS: TestGetOrCreateChannelReturnsExistingChannel (0.00s)\n=== CONT  TestIsStatusLine\n=== RUN   TestIsStatusLine/BaseCase\n--- PASS: TestPeek (0.00s)\nTestGetOrCreateChannelSpawnsLogCollectorOnCreate INFO 2018-10-20T13:03:33-07:00 Directory /var/folders/n2/pljz6dq52bd1ksmw23qyr3sr0000gn/T/TestEnsureDirectoryCreatesDirectory894837527 already exists\n=== RUN   TestGetIndent/NoIndent\n=== RUN   TestIsStatusLine/Indented\n=== RUN   TestGetIndent/EmptyString\n=== RUN   TestIsStatusLine/SpecialChars\n=== RUN   TestIsStatusLine/WhenPaused\n=== RUN   TestGetIndent/Tabs\n=== RUN   TestIsStatusLine/WhenCont\n=== RUN   TestGetIndent/MixTabSpace\n=== RUN   TestIsStatusLine/NonStatusLine\n--- PASS: TestIsStatusLine (0.00s)\n    --- PASS: TestIsStatusLine/BaseCase (0.00s)\n    --- PASS: TestIsStatusLine/Indented (0.00s)\n    --- PASS: TestIsStatusLine/SpecialChars (0.00s)\n    --- PASS: TestIsStatusLine/WhenPaused (0.00s)\n    --- PASS: TestIsStatusLine/WhenCont (0.00s)\n    --- PASS: TestIsStatusLine/NonStatusLine (0.00s)\n=== CONT  TestIsEmpty\n--- PASS: TestGetIndent (0.00s)\n    --- PASS: TestGetIndent/BaseCase (0.00s)\n    --- PASS: TestGetIndent/NoIndent (0.00s)\n    --- PASS: TestGetIndent/EmptyString (0.00s)\n    --- PASS: TestGetIndent/Tabs (0.00s)\n    --- PASS: TestGetIndent/MixTabSpace (0.00s)\n=== CONT  TestGetTestNameFromStatusLine\n--- PASS: TestIsEmpty (0.00s)\n=== CONT  TestPeekEmpty\n=== RUN   TestGetTestNameFromStatusLine/BaseCase\n--- PASS: TestPeekEmpty (0.00s)\n=== CONT  TestIsResultLine\n=== RUN   TestIsResultLine/BaseCase\n=== RUN   TestGetTestNameFromStatusLine/Indented\n=== RUN   TestGetTestNameFromStatusLine/SpecialChars\nTestLogCollectorCreatesAndWritesToFile INFO 2018-10-20T13:03:33-07:00 Storing logs for test TestLogCollectorCreatesAndWritesToFile to /var/folders/n2/pljz6dq52bd1ksmw23qyr3sr0000gn/T/TestLogCollectorCreatesAndWritesToFile509683594/TestLogCollectorCreatesAndWritesToFile.log\n=== RUN   TestIsResultLine/Indented\n=== RUN   TestGetTestNameFromStatusLine/WhenPaused\n=== RUN   TestIsResultLine/SpecialChars\nTestLogCollectorCreatesAndWritesToFile INFO 2018-10-20T13:03:33-07:00 Directory /var/folders/n2/pljz6dq52bd1ksmw23qyr3sr0000gn/T/TestLogCollectorCreatesAndWritesToFile509683594 already exists\n=== RUN   TestGetTestNameFromStatusLine/WhenCont\n=== RUN   TestIsResultLine/WhenFailed\n--- PASS: TestGetTestNameFromStatusLine (0.00s)\n    --- PASS: TestGetTestNameFromStatusLine/BaseCase (0.00s)\n    --- PASS: TestGetTestNameFromStatusLine/Indented (0.00s)\n    --- PASS: TestGetTestNameFromStatusLine/SpecialChars (0.00s)\n    --- PASS: TestGetTestNameFromStatusLine/WhenPaused (0.00s)\n    --- PASS: TestGetTestNameFromStatusLine/WhenCont (0.00s)\n=== CONT  TestGetTestNameFromResultLine\n=== RUN   TestGetTestNameFromResultLine/BaseCase\nTestGetOrCreateChannelSpawnsLogCollectorOnCreate INFO 2018-10-20T13:03:33-07:00 Channel closed for log writer of test TestGetOrCreateChannelSpawnsLogCollectorOnCreate\n=== RUN   TestGetTestNameFromResultLine/Indented\n=== RUN   TestGetTestNameFromResultLine/SpecialChars\n=== RUN   TestGetTestNameFromResultLine/WhenFailed\n=== RUN   TestIsResultLine/NonResultLine\n--- PASS: TestGetTestNameFromResultLine (0.00s)\n    --- PASS: TestGetTestNameFromResultLine/BaseCase (0.00s)\n    --- PASS: TestGetTestNameFromResultLine/Indented (0.00s)\n    --- PASS: TestGetTestNameFromResultLine/SpecialChars (0.00s)\n    --- PASS: TestGetTestNameFromResultLine/WhenFailed (0.00s)\n=== CONT  TestEnsureDirectoryExistsHandlesExistingDirectory\n--- PASS: TestIsResultLine (0.00s)\n    --- PASS: TestIsResultLine/BaseCase (0.00s)\n    --- PASS: TestIsResultLine/Indented (0.00s)\n    --- PASS: TestIsResultLine/SpecialChars (0.00s)\n    --- PASS: TestIsResultLine/WhenFailed (0.00s)\n    --- PASS: TestIsResultLine/NonResultLine (0.00s)\n=== CONT  TestGetOrCreateChannelCreatesNewChannel\nTestLogCollectorCreatesAndWritesToFile INFO 2018-10-20T13:03:33-07:00 Channel closed for log writer of test TestLogCollectorCreatesAndWritesToFile\nTestEnsureDirectoryExistsHandlesExistingDirectory INFO 2018-10-20T13:03:33-07:00 Directory /var/folders/n2/pljz6dq52bd1ksmw23qyr3sr0000gn/T/TestEnsureDirectoryCreatesDirectory503195489 already exists\nTestGetOrCreateChannelCreatesNewChannel INFO 2018-10-20T13:03:33-07:00 Spawned log writer for test TestGetOrCreateChannelCreatesNewChannel\nTestGetOrCreateChannelCreatesNewChannel INFO 2018-10-20T13:03:33-07:00 Storing logs for test TestGetOrCreateChannelCreatesNewChannel to /var/folders/n2/pljz6dq52bd1ksmw23qyr3sr0000gn/T/TestEnsureDirectoryCreatesDirectory272503116/TestGetOrCreateChannelCreatesNewChannel.log\n--- PASS: TestGetOrCreateChannelCreatesNewChannel (0.00s)\n=== CONT  TestStackPopEmpty\n--- PASS: TestEnsureDirectoryExistsHandlesExistingDirectory (0.00s)\nTestGetOrCreateChannelCreatesNewChannel INFO 2018-10-20T13:03:33-07:00 Creating directory /var/folders/n2/pljz6dq52bd1ksmw23qyr3sr0000gn/T/TestEnsureDirectoryCreatesDirectory272503116\n=== CONT  TestIsPanicLine\n=== CONT  TestEnsureDirectoryExistsCreatesDirectory\n=== RUN   TestIsPanicLine/BaseCase\n=== RUN   TestIsPanicLine/NotPanic\n--- PASS: TestIsPanicLine (0.00s)\n    --- PASS: TestIsPanicLine/BaseCase (0.00s)\n    --- PASS: TestIsPanicLine/NotPanic (0.00s)\n=== CONT  TestStackPop\n--- PASS: TestStackPop (0.00s)\n--- PASS: TestStackPopEmpty (0.00s)\nTestEnsureDirectoryExistsCreatesDirectory INFO 2018-10-20T13:03:33-07:00 Creating directory /var/folders/n2/pljz6dq52bd1ksmw23qyr3sr0000gn/T/TestEnsureDirectoryCreatesDirectory896401467/tmpdir\nTestGetOrCreateChannelCreatesNewChannel INFO 2018-10-20T13:03:33-07:00 Channel closed for log writer of test TestGetOrCreateChannelCreatesNewChannel\n--- PASS: TestEnsureDirectoryExistsCreatesDirectory (0.00s)\n--- PASS: TestGetOrCreateChannelSpawnsLogCollectorOnCreate (1.01s)\n--- PASS: TestLogCollectorCreatesAndWritesToFile (1.01s)\nPASS\nok  \tgithub.com/gruntwork-io/terratest/modules/logger/parser\t1.019s\n"
  },
  {
    "path": "modules/logger/parser/fixtures/basic_example_expected/TestCloseChannelsClosesAll.log",
    "content": "=== RUN   TestCloseChannelsClosesAll\n=== PAUSE TestCloseChannelsClosesAll\n=== CONT  TestCloseChannelsClosesAll\nTestCloseChannelsClosesAll INFO 2018-10-20T13:03:33-07:00 Closing all the channels in log writer\n--- PASS: TestCloseChannelsClosesAll (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/basic_example_expected/TestEnsureDirectoryExistsCreatesDirectory.log",
    "content": "=== RUN   TestEnsureDirectoryExistsCreatesDirectory\n=== PAUSE TestEnsureDirectoryExistsCreatesDirectory\n=== CONT  TestEnsureDirectoryExistsCreatesDirectory\nTestEnsureDirectoryExistsCreatesDirectory INFO 2018-10-20T13:03:33-07:00 Creating directory /var/folders/n2/pljz6dq52bd1ksmw23qyr3sr0000gn/T/TestEnsureDirectoryCreatesDirectory896401467/tmpdir\n--- PASS: TestEnsureDirectoryExistsCreatesDirectory (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/basic_example_expected/TestEnsureDirectoryExistsHandlesExistingDirectory.log",
    "content": "=== RUN   TestEnsureDirectoryExistsHandlesExistingDirectory\n=== PAUSE TestEnsureDirectoryExistsHandlesExistingDirectory\n=== CONT  TestEnsureDirectoryExistsHandlesExistingDirectory\nTestEnsureDirectoryExistsHandlesExistingDirectory INFO 2018-10-20T13:03:33-07:00 Directory /var/folders/n2/pljz6dq52bd1ksmw23qyr3sr0000gn/T/TestEnsureDirectoryCreatesDirectory503195489 already exists\n--- PASS: TestEnsureDirectoryExistsHandlesExistingDirectory (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/basic_example_expected/TestGetIndent/BaseCase.log",
    "content": "=== RUN   TestGetIndent/BaseCase\n    --- PASS: TestGetIndent/BaseCase (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/basic_example_expected/TestGetIndent/EmptyString.log",
    "content": "=== RUN   TestGetIndent/EmptyString\n    --- PASS: TestGetIndent/EmptyString (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/basic_example_expected/TestGetIndent/MixTabSpace.log",
    "content": "=== RUN   TestGetIndent/MixTabSpace\n    --- PASS: TestGetIndent/MixTabSpace (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/basic_example_expected/TestGetIndent/NoIndent.log",
    "content": "=== RUN   TestGetIndent/NoIndent\n    --- PASS: TestGetIndent/NoIndent (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/basic_example_expected/TestGetIndent/Tabs.log",
    "content": "=== RUN   TestGetIndent/Tabs\n    --- PASS: TestGetIndent/Tabs (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/basic_example_expected/TestGetIndent.log",
    "content": "=== RUN   TestGetIndent\n=== PAUSE TestGetIndent\n=== CONT  TestGetIndent\n--- PASS: TestGetIndent (0.00s)\n    --- PASS: TestGetIndent/BaseCase (0.00s)\n    --- PASS: TestGetIndent/NoIndent (0.00s)\n    --- PASS: TestGetIndent/EmptyString (0.00s)\n    --- PASS: TestGetIndent/Tabs (0.00s)\n    --- PASS: TestGetIndent/MixTabSpace (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/basic_example_expected/TestGetOrCreateChannelCreatesNewChannel.log",
    "content": "=== RUN   TestGetOrCreateChannelCreatesNewChannel\n=== PAUSE TestGetOrCreateChannelCreatesNewChannel\n=== CONT  TestGetOrCreateChannelCreatesNewChannel\nTestGetOrCreateChannelCreatesNewChannel INFO 2018-10-20T13:03:33-07:00 Spawned log writer for test TestGetOrCreateChannelCreatesNewChannel\nTestGetOrCreateChannelCreatesNewChannel INFO 2018-10-20T13:03:33-07:00 Storing logs for test TestGetOrCreateChannelCreatesNewChannel to /var/folders/n2/pljz6dq52bd1ksmw23qyr3sr0000gn/T/TestEnsureDirectoryCreatesDirectory272503116/TestGetOrCreateChannelCreatesNewChannel.log\n--- PASS: TestGetOrCreateChannelCreatesNewChannel (0.00s)\nTestGetOrCreateChannelCreatesNewChannel INFO 2018-10-20T13:03:33-07:00 Creating directory /var/folders/n2/pljz6dq52bd1ksmw23qyr3sr0000gn/T/TestEnsureDirectoryCreatesDirectory272503116\nTestGetOrCreateChannelCreatesNewChannel INFO 2018-10-20T13:03:33-07:00 Channel closed for log writer of test TestGetOrCreateChannelCreatesNewChannel\nPASS\n"
  },
  {
    "path": "modules/logger/parser/fixtures/basic_example_expected/TestGetOrCreateChannelReturnsExistingChannel.log",
    "content": "=== RUN   TestGetOrCreateChannelReturnsExistingChannel\n=== PAUSE TestGetOrCreateChannelReturnsExistingChannel\n=== CONT  TestGetOrCreateChannelReturnsExistingChannel\n--- PASS: TestGetOrCreateChannelReturnsExistingChannel (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/basic_example_expected/TestGetOrCreateChannelSpawnsLogCollectorOnCreate.log",
    "content": "=== RUN   TestGetOrCreateChannelSpawnsLogCollectorOnCreate\n=== PAUSE TestGetOrCreateChannelSpawnsLogCollectorOnCreate\n=== CONT  TestGetOrCreateChannelSpawnsLogCollectorOnCreate\nTestGetOrCreateChannelSpawnsLogCollectorOnCreate INFO 2018-10-20T13:03:33-07:00 Spawned log writer for test TestGetOrCreateChannelSpawnsLogCollectorOnCreate\nTestGetOrCreateChannelSpawnsLogCollectorOnCreate INFO 2018-10-20T13:03:33-07:00 Storing logs for test TestGetOrCreateChannelSpawnsLogCollectorOnCreate to /var/folders/n2/pljz6dq52bd1ksmw23qyr3sr0000gn/T/TestEnsureDirectoryCreatesDirectory894837527/TestGetOrCreateChannelSpawnsLogCollectorOnCreate.log\nTestGetOrCreateChannelSpawnsLogCollectorOnCreate INFO 2018-10-20T13:03:33-07:00 Directory /var/folders/n2/pljz6dq52bd1ksmw23qyr3sr0000gn/T/TestEnsureDirectoryCreatesDirectory894837527 already exists\nTestGetOrCreateChannelSpawnsLogCollectorOnCreate INFO 2018-10-20T13:03:33-07:00 Channel closed for log writer of test TestGetOrCreateChannelSpawnsLogCollectorOnCreate\n--- PASS: TestGetOrCreateChannelSpawnsLogCollectorOnCreate (1.01s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/basic_example_expected/TestGetTestNameFromResultLine/BaseCase.log",
    "content": "=== RUN   TestGetTestNameFromResultLine/BaseCase\n    --- PASS: TestGetTestNameFromResultLine/BaseCase (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/basic_example_expected/TestGetTestNameFromResultLine/Indented.log",
    "content": "=== RUN   TestGetTestNameFromResultLine/Indented\n    --- PASS: TestGetTestNameFromResultLine/Indented (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/basic_example_expected/TestGetTestNameFromResultLine/SpecialChars.log",
    "content": "=== RUN   TestGetTestNameFromResultLine/SpecialChars\n    --- PASS: TestGetTestNameFromResultLine/SpecialChars (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/basic_example_expected/TestGetTestNameFromResultLine/WhenFailed.log",
    "content": "=== RUN   TestGetTestNameFromResultLine/WhenFailed\n    --- PASS: TestGetTestNameFromResultLine/WhenFailed (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/basic_example_expected/TestGetTestNameFromResultLine.log",
    "content": "=== RUN   TestGetTestNameFromResultLine\n=== PAUSE TestGetTestNameFromResultLine\n=== CONT  TestGetTestNameFromResultLine\n--- PASS: TestGetTestNameFromResultLine (0.00s)\n    --- PASS: TestGetTestNameFromResultLine/BaseCase (0.00s)\n    --- PASS: TestGetTestNameFromResultLine/Indented (0.00s)\n    --- PASS: TestGetTestNameFromResultLine/SpecialChars (0.00s)\n    --- PASS: TestGetTestNameFromResultLine/WhenFailed (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/basic_example_expected/TestGetTestNameFromStatusLine/BaseCase.log",
    "content": "=== RUN   TestGetTestNameFromStatusLine/BaseCase\n    --- PASS: TestGetTestNameFromStatusLine/BaseCase (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/basic_example_expected/TestGetTestNameFromStatusLine/Indented.log",
    "content": "=== RUN   TestGetTestNameFromStatusLine/Indented\n    --- PASS: TestGetTestNameFromStatusLine/Indented (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/basic_example_expected/TestGetTestNameFromStatusLine/SpecialChars.log",
    "content": "=== RUN   TestGetTestNameFromStatusLine/SpecialChars\n    --- PASS: TestGetTestNameFromStatusLine/SpecialChars (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/basic_example_expected/TestGetTestNameFromStatusLine/WhenCont.log",
    "content": "=== RUN   TestGetTestNameFromStatusLine/WhenCont\n    --- PASS: TestGetTestNameFromStatusLine/WhenCont (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/basic_example_expected/TestGetTestNameFromStatusLine/WhenPaused.log",
    "content": "=== RUN   TestGetTestNameFromStatusLine/WhenPaused\n    --- PASS: TestGetTestNameFromStatusLine/WhenPaused (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/basic_example_expected/TestGetTestNameFromStatusLine.log",
    "content": "=== RUN   TestGetTestNameFromStatusLine\n=== PAUSE TestGetTestNameFromStatusLine\n=== CONT  TestGetTestNameFromStatusLine\n--- PASS: TestGetTestNameFromStatusLine (0.00s)\n    --- PASS: TestGetTestNameFromStatusLine/BaseCase (0.00s)\n    --- PASS: TestGetTestNameFromStatusLine/Indented (0.00s)\n    --- PASS: TestGetTestNameFromStatusLine/SpecialChars (0.00s)\n    --- PASS: TestGetTestNameFromStatusLine/WhenPaused (0.00s)\n    --- PASS: TestGetTestNameFromStatusLine/WhenCont (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/basic_example_expected/TestIsEmpty.log",
    "content": "=== RUN   TestIsEmpty\n=== PAUSE TestIsEmpty\n=== CONT  TestIsEmpty\n--- PASS: TestIsEmpty (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/basic_example_expected/TestIsPanicLine/BaseCase.log",
    "content": "=== RUN   TestIsPanicLine/BaseCase\n    --- PASS: TestIsPanicLine/BaseCase (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/basic_example_expected/TestIsPanicLine/NotPanic.log",
    "content": "=== RUN   TestIsPanicLine/NotPanic\n    --- PASS: TestIsPanicLine/NotPanic (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/basic_example_expected/TestIsPanicLine.log",
    "content": "=== RUN   TestIsPanicLine\n=== PAUSE TestIsPanicLine\n=== CONT  TestIsPanicLine\n--- PASS: TestIsPanicLine (0.00s)\n    --- PASS: TestIsPanicLine/BaseCase (0.00s)\n    --- PASS: TestIsPanicLine/NotPanic (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/basic_example_expected/TestIsResultLine/BaseCase.log",
    "content": "=== RUN   TestIsResultLine/BaseCase\n    --- PASS: TestIsResultLine/BaseCase (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/basic_example_expected/TestIsResultLine/Indented.log",
    "content": "=== RUN   TestIsResultLine/Indented\n    --- PASS: TestIsResultLine/Indented (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/basic_example_expected/TestIsResultLine/NonResultLine.log",
    "content": "=== RUN   TestIsResultLine/NonResultLine\n    --- PASS: TestIsResultLine/NonResultLine (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/basic_example_expected/TestIsResultLine/SpecialChars.log",
    "content": "=== RUN   TestIsResultLine/SpecialChars\n    --- PASS: TestIsResultLine/SpecialChars (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/basic_example_expected/TestIsResultLine/WhenFailed.log",
    "content": "=== RUN   TestIsResultLine/WhenFailed\n    --- PASS: TestIsResultLine/WhenFailed (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/basic_example_expected/TestIsResultLine.log",
    "content": "=== RUN   TestIsResultLine\n=== PAUSE TestIsResultLine\n=== CONT  TestIsResultLine\n--- PASS: TestIsResultLine (0.00s)\n    --- PASS: TestIsResultLine/BaseCase (0.00s)\n    --- PASS: TestIsResultLine/Indented (0.00s)\n    --- PASS: TestIsResultLine/SpecialChars (0.00s)\n    --- PASS: TestIsResultLine/WhenFailed (0.00s)\n    --- PASS: TestIsResultLine/NonResultLine (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/basic_example_expected/TestIsStatusLine/BaseCase.log",
    "content": "=== RUN   TestIsStatusLine/BaseCase\n    --- PASS: TestIsStatusLine/BaseCase (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/basic_example_expected/TestIsStatusLine/Indented.log",
    "content": "=== RUN   TestIsStatusLine/Indented\n    --- PASS: TestIsStatusLine/Indented (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/basic_example_expected/TestIsStatusLine/NonStatusLine.log",
    "content": "=== RUN   TestIsStatusLine/NonStatusLine\n    --- PASS: TestIsStatusLine/NonStatusLine (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/basic_example_expected/TestIsStatusLine/SpecialChars.log",
    "content": "=== RUN   TestIsStatusLine/SpecialChars\n    --- PASS: TestIsStatusLine/SpecialChars (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/basic_example_expected/TestIsStatusLine/WhenCont.log",
    "content": "=== RUN   TestIsStatusLine/WhenCont\n    --- PASS: TestIsStatusLine/WhenCont (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/basic_example_expected/TestIsStatusLine/WhenPaused.log",
    "content": "=== RUN   TestIsStatusLine/WhenPaused\n    --- PASS: TestIsStatusLine/WhenPaused (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/basic_example_expected/TestIsStatusLine.log",
    "content": "=== RUN   TestIsStatusLine\n=== PAUSE TestIsStatusLine\n=== CONT  TestIsStatusLine\n--- PASS: TestIsStatusLine (0.00s)\n    --- PASS: TestIsStatusLine/BaseCase (0.00s)\n    --- PASS: TestIsStatusLine/Indented (0.00s)\n    --- PASS: TestIsStatusLine/SpecialChars (0.00s)\n    --- PASS: TestIsStatusLine/WhenPaused (0.00s)\n    --- PASS: TestIsStatusLine/WhenCont (0.00s)\n    --- PASS: TestIsStatusLine/NonStatusLine (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/basic_example_expected/TestIsSummaryLine/BaseCase.log",
    "content": "=== RUN   TestIsSummaryLine/BaseCase\n    --- PASS: TestIsSummaryLine/BaseCase (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/basic_example_expected/TestIsSummaryLine/NotSummary.log",
    "content": "=== RUN   TestIsSummaryLine/NotSummary\n    --- PASS: TestIsSummaryLine/NotSummary (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/basic_example_expected/TestIsSummaryLine.log",
    "content": "=== RUN   TestIsSummaryLine\n=== PAUSE TestIsSummaryLine\n=== CONT  TestIsSummaryLine\n--- PASS: TestIsSummaryLine (0.00s)\n    --- PASS: TestIsSummaryLine/BaseCase (0.00s)\n    --- PASS: TestIsSummaryLine/NotSummary (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/basic_example_expected/TestLogCollectorCreatesAndWritesToFile.log",
    "content": "=== RUN   TestLogCollectorCreatesAndWritesToFile\n=== PAUSE TestLogCollectorCreatesAndWritesToFile\n=== CONT  TestLogCollectorCreatesAndWritesToFile\nTestLogCollectorCreatesAndWritesToFile INFO 2018-10-20T13:03:33-07:00 Spawned log writer for test TestLogCollectorCreatesAndWritesToFile\nTestLogCollectorCreatesAndWritesToFile INFO 2018-10-20T13:03:33-07:00 Storing logs for test TestLogCollectorCreatesAndWritesToFile to /var/folders/n2/pljz6dq52bd1ksmw23qyr3sr0000gn/T/TestLogCollectorCreatesAndWritesToFile509683594/TestLogCollectorCreatesAndWritesToFile.log\nTestLogCollectorCreatesAndWritesToFile INFO 2018-10-20T13:03:33-07:00 Directory /var/folders/n2/pljz6dq52bd1ksmw23qyr3sr0000gn/T/TestLogCollectorCreatesAndWritesToFile509683594 already exists\nTestLogCollectorCreatesAndWritesToFile INFO 2018-10-20T13:03:33-07:00 Channel closed for log writer of test TestLogCollectorCreatesAndWritesToFile\n--- PASS: TestLogCollectorCreatesAndWritesToFile (1.01s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/basic_example_expected/TestPeek.log",
    "content": "=== RUN   TestPeek\n=== PAUSE TestPeek\n=== CONT  TestPeek\n--- PASS: TestPeek (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/basic_example_expected/TestPeekEmpty.log",
    "content": "=== RUN   TestPeekEmpty\n=== PAUSE TestPeekEmpty\n=== CONT  TestPeekEmpty\n--- PASS: TestPeekEmpty (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/basic_example_expected/TestRemoveDedentedTestResultMarkers.log",
    "content": "=== RUN   TestRemoveDedentedTestResultMarkers\n--- PASS: TestRemoveDedentedTestResultMarkers (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/basic_example_expected/TestRemoveDedentedTestResultMarkersAll.log",
    "content": "=== RUN   TestRemoveDedentedTestResultMarkersAll\n--- PASS: TestRemoveDedentedTestResultMarkersAll (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/basic_example_expected/TestRemoveDedentedTestResultMarkersEmpty.log",
    "content": "=== RUN   TestRemoveDedentedTestResultMarkersEmpty\n--- PASS: TestRemoveDedentedTestResultMarkersEmpty (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/basic_example_expected/TestStackPop.log",
    "content": "=== RUN   TestStackPop\n=== PAUSE TestStackPop\n=== CONT  TestStackPop\n--- PASS: TestStackPop (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/basic_example_expected/TestStackPopEmpty.log",
    "content": "=== RUN   TestStackPopEmpty\n=== PAUSE TestStackPopEmpty\n=== CONT  TestStackPopEmpty\n--- PASS: TestStackPopEmpty (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/basic_example_expected/TestStackPush.log",
    "content": "=== RUN   TestStackPush\n=== PAUSE TestStackPush\n=== CONT  TestStackPush\n--- PASS: TestStackPush (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/basic_example_expected/report.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<testsuites>\n\t<testsuite tests=\"52\" failures=\"0\" time=\"1.019\" name=\"github.com/gruntwork-io/terratest/modules/logger/parser\">\n\t\t<properties>\n\t\t\t<property name=\"go.version\" value=\"go1.21.1\"></property>\n\t\t</properties>\n\t\t<testcase classname=\"parser\" name=\"TestStackPush\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestStackPop\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestStackPopEmpty\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestPeek\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestPeekEmpty\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestIsEmpty\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestRemoveDedentedTestResultMarkers\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestRemoveDedentedTestResultMarkersEmpty\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestRemoveDedentedTestResultMarkersAll\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestGetIndent\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestGetTestNameFromResultLine\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestIsResultLine\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestGetTestNameFromStatusLine\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestIsStatusLine\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestIsSummaryLine\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestIsPanicLine\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestEnsureDirectoryExistsCreatesDirectory\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestEnsureDirectoryExistsHandlesExistingDirectory\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestGetOrCreateChannelCreatesNewChannel\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestGetOrCreateChannelReturnsExistingChannel\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestLogCollectorCreatesAndWritesToFile\" time=\"1.010\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestGetOrCreateChannelSpawnsLogCollectorOnCreate\" time=\"1.010\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestCloseChannelsClosesAll\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestIsSummaryLine/BaseCase\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestIsSummaryLine/NotSummary\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestGetIndent/BaseCase\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestIsStatusLine/BaseCase\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestGetIndent/NoIndent\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestIsStatusLine/Indented\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestGetIndent/EmptyString\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestIsStatusLine/SpecialChars\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestIsStatusLine/WhenPaused\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestGetIndent/Tabs\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestIsStatusLine/WhenCont\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestGetIndent/MixTabSpace\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestIsStatusLine/NonStatusLine\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestGetTestNameFromStatusLine/BaseCase\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestIsResultLine/BaseCase\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestGetTestNameFromStatusLine/Indented\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestGetTestNameFromStatusLine/SpecialChars\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestIsResultLine/Indented\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestGetTestNameFromStatusLine/WhenPaused\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestIsResultLine/SpecialChars\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestGetTestNameFromStatusLine/WhenCont\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestIsResultLine/WhenFailed\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestGetTestNameFromResultLine/BaseCase\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestGetTestNameFromResultLine/Indented\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestGetTestNameFromResultLine/SpecialChars\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestGetTestNameFromResultLine/WhenFailed\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestIsResultLine/NonResultLine\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestIsPanicLine/BaseCase\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestIsPanicLine/NotPanic\" time=\"0.000\"></testcase>\n\t</testsuite>\n</testsuites>\n"
  },
  {
    "path": "modules/logger/parser/fixtures/basic_example_expected/summary.log",
    "content": "--- PASS: TestRemoveDedentedTestResultMarkers (0.00s)\n--- PASS: TestRemoveDedentedTestResultMarkersEmpty (0.00s)\n--- PASS: TestRemoveDedentedTestResultMarkersAll (0.00s)\n--- PASS: TestStackPush (0.00s)\n--- PASS: TestIsSummaryLine (0.00s)\n    --- PASS: TestIsSummaryLine/BaseCase (0.00s)\n    --- PASS: TestIsSummaryLine/NotSummary (0.00s)\n--- PASS: TestCloseChannelsClosesAll (0.00s)\n--- PASS: TestGetOrCreateChannelReturnsExistingChannel (0.00s)\n--- PASS: TestPeek (0.00s)\n--- PASS: TestIsStatusLine (0.00s)\n    --- PASS: TestIsStatusLine/BaseCase (0.00s)\n    --- PASS: TestIsStatusLine/Indented (0.00s)\n    --- PASS: TestIsStatusLine/SpecialChars (0.00s)\n    --- PASS: TestIsStatusLine/WhenPaused (0.00s)\n    --- PASS: TestIsStatusLine/WhenCont (0.00s)\n    --- PASS: TestIsStatusLine/NonStatusLine (0.00s)\n--- PASS: TestGetIndent (0.00s)\n    --- PASS: TestGetIndent/BaseCase (0.00s)\n    --- PASS: TestGetIndent/NoIndent (0.00s)\n    --- PASS: TestGetIndent/EmptyString (0.00s)\n    --- PASS: TestGetIndent/Tabs (0.00s)\n    --- PASS: TestGetIndent/MixTabSpace (0.00s)\n--- PASS: TestIsEmpty (0.00s)\n--- PASS: TestPeekEmpty (0.00s)\n--- PASS: TestGetTestNameFromStatusLine (0.00s)\n    --- PASS: TestGetTestNameFromStatusLine/BaseCase (0.00s)\n    --- PASS: TestGetTestNameFromStatusLine/Indented (0.00s)\n    --- PASS: TestGetTestNameFromStatusLine/SpecialChars (0.00s)\n    --- PASS: TestGetTestNameFromStatusLine/WhenPaused (0.00s)\n    --- PASS: TestGetTestNameFromStatusLine/WhenCont (0.00s)\n--- PASS: TestGetTestNameFromResultLine (0.00s)\n    --- PASS: TestGetTestNameFromResultLine/BaseCase (0.00s)\n    --- PASS: TestGetTestNameFromResultLine/Indented (0.00s)\n    --- PASS: TestGetTestNameFromResultLine/SpecialChars (0.00s)\n    --- PASS: TestGetTestNameFromResultLine/WhenFailed (0.00s)\n--- PASS: TestIsResultLine (0.00s)\n    --- PASS: TestIsResultLine/BaseCase (0.00s)\n    --- PASS: TestIsResultLine/Indented (0.00s)\n    --- PASS: TestIsResultLine/SpecialChars (0.00s)\n    --- PASS: TestIsResultLine/WhenFailed (0.00s)\n    --- PASS: TestIsResultLine/NonResultLine (0.00s)\n--- PASS: TestGetOrCreateChannelCreatesNewChannel (0.00s)\n--- PASS: TestEnsureDirectoryExistsHandlesExistingDirectory (0.00s)\n--- PASS: TestIsPanicLine (0.00s)\n    --- PASS: TestIsPanicLine/BaseCase (0.00s)\n    --- PASS: TestIsPanicLine/NotPanic (0.00s)\n--- PASS: TestStackPop (0.00s)\n--- PASS: TestStackPopEmpty (0.00s)\n--- PASS: TestEnsureDirectoryExistsCreatesDirectory (0.00s)\n--- PASS: TestGetOrCreateChannelSpawnsLogCollectorOnCreate (1.01s)\n--- PASS: TestLogCollectorCreatesAndWritesToFile (1.01s)\nok  \tgithub.com/gruntwork-io/terratest/modules/logger/parser\t1.019s\n"
  },
  {
    "path": "modules/logger/parser/fixtures/failing_example.log",
    "content": "=== RUN   TestStackPush\n=== PAUSE TestStackPush\n=== RUN   TestStackPop\n=== PAUSE TestStackPop\n=== RUN   TestStackPopEmpty\n=== PAUSE TestStackPopEmpty\n=== RUN   TestPeek\n=== PAUSE TestPeek\n=== RUN   TestPeekEmpty\n=== PAUSE TestPeekEmpty\n=== RUN   TestIsEmpty\n=== PAUSE TestIsEmpty\n=== RUN   TestRemoveDedentedTestResultMarkers\n--- PASS: TestRemoveDedentedTestResultMarkers (0.00s)\n=== RUN   TestRemoveDedentedTestResultMarkersEmpty\n--- PASS: TestRemoveDedentedTestResultMarkersEmpty (0.00s)\n=== RUN   TestRemoveDedentedTestResultMarkersAll\n--- PASS: TestRemoveDedentedTestResultMarkersAll (0.00s)\n=== RUN   TestBasicExample\n--- FAIL: TestBasicExample (0.00s)\n    integration_test.go:10:\n        \tError Trace:\tintegration_test.go:10\n        \tError:      \tExpected value not to be nil.\n        \tTest:       \tTestBasicExample\n=== RUN   TestPanicExample\n--- FAIL: TestPanicExample (0.00s)\n    integration_test.go:14:\n        \tError Trace:\tintegration_test.go:14\n        \tError:      \tExpected value not to be nil.\n        \tTest:       \tTestPanicExample\n=== RUN   TestRealWorldExample\n--- FAIL: TestRealWorldExample (0.00s)\n    integration_test.go:18:\n        \tError Trace:\tintegration_test.go:18\n        \tError:      \tExpected value not to be nil.\n        \tTest:       \tTestRealWorldExample\n=== RUN   TestGetIndent\n=== PAUSE TestGetIndent\n=== RUN   TestGetTestNameFromResultLine\n=== PAUSE TestGetTestNameFromResultLine\n=== RUN   TestIsResultLine\n=== PAUSE TestIsResultLine\n=== RUN   TestGetTestNameFromStatusLine\n=== PAUSE TestGetTestNameFromStatusLine\n=== RUN   TestIsStatusLine\n=== PAUSE TestIsStatusLine\n=== RUN   TestIsSummaryLine\n=== PAUSE TestIsSummaryLine\n=== RUN   TestIsPanicLine\n=== PAUSE TestIsPanicLine\n=== RUN   TestEnsureDirectoryExistsCreatesDirectory\n=== PAUSE TestEnsureDirectoryExistsCreatesDirectory\n=== RUN   TestEnsureDirectoryExistsHandlesExistingDirectory\n=== PAUSE TestEnsureDirectoryExistsHandlesExistingDirectory\n=== RUN   TestGetOrCreateChannelCreatesNewChannel\n=== PAUSE TestGetOrCreateChannelCreatesNewChannel\n=== RUN   TestGetOrCreateChannelReturnsExistingChannel\n=== PAUSE TestGetOrCreateChannelReturnsExistingChannel\n=== RUN   TestLogCollectorCreatesAndWritesToFile\n=== PAUSE TestLogCollectorCreatesAndWritesToFile\n=== RUN   TestGetOrCreateChannelSpawnsLogCollectorOnCreate\n=== PAUSE TestGetOrCreateChannelSpawnsLogCollectorOnCreate\n=== RUN   TestCloseChannelsClosesAll\n=== PAUSE TestCloseChannelsClosesAll\n=== CONT  TestStackPush\n=== CONT  TestIsSummaryLine\n--- PASS: TestStackPush (0.00s)\n=== CONT  TestIsStatusLine\n=== RUN   TestIsStatusLine/BaseCase\n=== CONT  TestGetTestNameFromStatusLine\n=== RUN   TestGetTestNameFromStatusLine/BaseCase\n=== CONT  TestIsResultLine\n=== RUN   TestIsResultLine/BaseCase\n=== RUN   TestIsSummaryLine/BaseCase\n=== RUN   TestGetTestNameFromStatusLine/Indented\n=== RUN   TestIsResultLine/Indented\n=== RUN   TestIsStatusLine/Indented\n=== RUN   TestGetTestNameFromStatusLine/SpecialChars\n=== RUN   TestIsResultLine/SpecialChars\n=== RUN   TestIsStatusLine/SpecialChars\n=== RUN   TestGetTestNameFromStatusLine/WhenPaused\n=== RUN   TestIsResultLine/WhenFailed\n=== RUN   TestIsStatusLine/WhenPaused\n=== RUN   TestGetTestNameFromStatusLine/WhenCont\n=== RUN   TestIsStatusLine/WhenCont\n=== RUN   TestIsResultLine/NonResultLine\n--- PASS: TestGetTestNameFromStatusLine (0.00s)\n    --- PASS: TestGetTestNameFromStatusLine/BaseCase (0.00s)\n    --- PASS: TestGetTestNameFromStatusLine/Indented (0.00s)\n    --- PASS: TestGetTestNameFromStatusLine/SpecialChars (0.00s)\n    --- PASS: TestGetTestNameFromStatusLine/WhenPaused (0.00s)\n    --- PASS: TestGetTestNameFromStatusLine/WhenCont (0.00s)\n=== RUN   TestIsStatusLine/NonStatusLine\n=== CONT  TestGetTestNameFromResultLine\n=== RUN   TestGetTestNameFromResultLine/BaseCase\n=== CONT  TestGetIndent\n--- PASS: TestIsStatusLine (0.00s)\n    --- PASS: TestIsStatusLine/BaseCase (0.00s)\n    --- PASS: TestIsStatusLine/Indented (0.00s)\n    --- PASS: TestIsStatusLine/SpecialChars (0.00s)\n    --- PASS: TestIsStatusLine/WhenPaused (0.00s)\n    --- PASS: TestIsStatusLine/WhenCont (0.00s)\n    --- PASS: TestIsStatusLine/NonStatusLine (0.00s)\n=== RUN   TestGetIndent/BaseCase\n=== RUN   TestGetTestNameFromResultLine/Indented\n=== CONT  TestIsEmpty\n--- PASS: TestIsEmpty (0.00s)\n=== RUN   TestGetTestNameFromResultLine/SpecialChars\n=== CONT  TestPeekEmpty\n--- PASS: TestPeekEmpty (0.00s)\n=== CONT  TestPeek\n=== RUN   TestGetTestNameFromResultLine/WhenFailed\n=== RUN   TestGetIndent/NoIndent\n--- PASS: TestPeek (0.00s)\n=== CONT  TestStackPop\n=== RUN   TestGetIndent/EmptyString\n--- PASS: TestGetTestNameFromResultLine (0.00s)\n    --- PASS: TestGetTestNameFromResultLine/BaseCase (0.00s)\n    --- PASS: TestGetTestNameFromResultLine/Indented (0.00s)\n    --- PASS: TestGetTestNameFromResultLine/SpecialChars (0.00s)\n    --- PASS: TestGetTestNameFromResultLine/WhenFailed (0.00s)\n=== CONT  TestGetOrCreateChannelReturnsExistingChannel\n=== CONT  TestStackPopEmpty\n--- PASS: TestStackPopEmpty (0.00s)\n=== RUN   TestGetIndent/Tabs\n=== CONT  TestCloseChannelsClosesAll\n=== RUN   TestGetIndent/MixTabSpace\n--- PASS: TestGetIndent (0.00s)\n    --- PASS: TestGetIndent/BaseCase (0.00s)\n    --- PASS: TestGetIndent/NoIndent (0.00s)\n    --- PASS: TestGetIndent/EmptyString (0.00s)\n    --- PASS: TestGetIndent/Tabs (0.00s)\n    --- PASS: TestGetIndent/MixTabSpace (0.00s)\n=== CONT  TestGetOrCreateChannelSpawnsLogCollectorOnCreate\n--- PASS: TestGetOrCreateChannelReturnsExistingChannel (0.00s)\n=== CONT  TestLogCollectorCreatesAndWritesToFile\nTestCloseChannelsClosesAll INFO 2018-10-20T13:15:09-07:00 Closing all the channels in log writer\n--- PASS: TestStackPop (0.00s)\nTestGetOrCreateChannelSpawnsLogCollectorOnCreate INFO 2018-10-20T13:15:09-07:00 Spawned log writer for test TestGetOrCreateChannelSpawnsLogCollectorOnCreate\nTestGetOrCreateChannelSpawnsLogCollectorOnCreate INFO 2018-10-20T13:15:09-07:00 Storing logs for test TestGetOrCreateChannelSpawnsLogCollectorOnCreate to /var/folders/n2/pljz6dq52bd1ksmw23qyr3sr0000gn/T/TestEnsureDirectoryCreatesDirectory945346773/TestGetOrCreateChannelSpawnsLogCollectorOnCreate.log\nTestLogCollectorCreatesAndWritesToFile INFO 2018-10-20T13:15:09-07:00 Spawned log writer for test TestLogCollectorCreatesAndWritesToFile\nTestGetOrCreateChannelSpawnsLogCollectorOnCreate INFO 2018-10-20T13:15:09-07:00 Directory /var/folders/n2/pljz6dq52bd1ksmw23qyr3sr0000gn/T/TestEnsureDirectoryCreatesDirectory945346773 already exists\nTestLogCollectorCreatesAndWritesToFile INFO 2018-10-20T13:15:09-07:00 Storing logs for test TestLogCollectorCreatesAndWritesToFile to /var/folders/n2/pljz6dq52bd1ksmw23qyr3sr0000gn/T/TestLogCollectorCreatesAndWritesToFile262063152/TestLogCollectorCreatesAndWritesToFile.log\n--- PASS: TestCloseChannelsClosesAll (0.00s)\nTestLogCollectorCreatesAndWritesToFile INFO 2018-10-20T13:15:09-07:00 Directory /var/folders/n2/pljz6dq52bd1ksmw23qyr3sr0000gn/T/TestLogCollectorCreatesAndWritesToFile262063152 already exists\n=== CONT  TestEnsureDirectoryExistsHandlesExistingDirectory\n=== RUN   TestIsSummaryLine/NotSummary\n--- PASS: TestIsSummaryLine (0.00s)\n    --- PASS: TestIsSummaryLine/BaseCase (0.00s)\n    --- PASS: TestIsSummaryLine/NotSummary (0.00s)\n=== CONT  TestGetOrCreateChannelCreatesNewChannel\nTestEnsureDirectoryExistsHandlesExistingDirectory INFO 2018-10-20T13:15:09-07:00 Directory /var/folders/n2/pljz6dq52bd1ksmw23qyr3sr0000gn/T/TestEnsureDirectoryCreatesDirectory292537295 already exists\nTestLogCollectorCreatesAndWritesToFile INFO 2018-10-20T13:15:09-07:00 Channel closed for log writer of test TestLogCollectorCreatesAndWritesToFile\nTestGetOrCreateChannelSpawnsLogCollectorOnCreate INFO 2018-10-20T13:15:09-07:00 Channel closed for log writer of test TestGetOrCreateChannelSpawnsLogCollectorOnCreate\n--- PASS: TestGetOrCreateChannelCreatesNewChannel (0.00s)\n=== CONT  TestEnsureDirectoryExistsCreatesDirectory\n--- PASS: TestIsResultLine (0.00s)\nTestGetOrCreateChannelCreatesNewChannel INFO 2018-10-20T13:15:09-07:00 Spawned log writer for test TestGetOrCreateChannelCreatesNewChannel\n    --- PASS: TestIsResultLine/BaseCase (0.00s)\n    --- PASS: TestIsResultLine/Indented (0.00s)\n    --- PASS: TestIsResultLine/SpecialChars (0.00s)\n    --- PASS: TestIsResultLine/WhenFailed (0.00s)\n    --- PASS: TestIsResultLine/NonResultLine (0.00s)\nTestGetOrCreateChannelCreatesNewChannel INFO 2018-10-20T13:15:09-07:00 Storing logs for test TestGetOrCreateChannelCreatesNewChannel to /var/folders/n2/pljz6dq52bd1ksmw23qyr3sr0000gn/T/TestEnsureDirectoryCreatesDirectory867148002/TestGetOrCreateChannelCreatesNewChannel.log\n--- PASS: TestEnsureDirectoryExistsHandlesExistingDirectory (0.00s)\n=== CONT  TestIsPanicLine\nTestGetOrCreateChannelCreatesNewChannel INFO 2018-10-20T13:15:09-07:00 Creating directory /var/folders/n2/pljz6dq52bd1ksmw23qyr3sr0000gn/T/TestEnsureDirectoryCreatesDirectory867148002\n=== RUN   TestIsPanicLine/BaseCase\n=== RUN   TestIsPanicLine/NotPanic\n--- PASS: TestIsPanicLine (0.00s)\n    --- PASS: TestIsPanicLine/BaseCase (0.00s)\n    --- PASS: TestIsPanicLine/NotPanic (0.00s)\nTestEnsureDirectoryExistsCreatesDirectory INFO 2018-10-20T13:15:09-07:00 Creating directory /var/folders/n2/pljz6dq52bd1ksmw23qyr3sr0000gn/T/TestEnsureDirectoryCreatesDirectory357603033/tmpdir\nTestGetOrCreateChannelCreatesNewChannel INFO 2018-10-20T13:15:09-07:00 Channel closed for log writer of test TestGetOrCreateChannelCreatesNewChannel\n--- PASS: TestEnsureDirectoryExistsCreatesDirectory (0.00s)\n--- PASS: TestLogCollectorCreatesAndWritesToFile (1.01s)\n--- PASS: TestGetOrCreateChannelSpawnsLogCollectorOnCreate (1.01s)\nFAIL\nexit status 1\nFAIL\tgithub.com/gruntwork-io/terratest/modules/logger/parser\t1.020s\n"
  },
  {
    "path": "modules/logger/parser/fixtures/failing_example_expected/TestBasicExample.log",
    "content": "=== RUN   TestBasicExample\n--- FAIL: TestBasicExample (0.00s)\n    integration_test.go:10:\n        \tError Trace:\tintegration_test.go:10\n        \tError:      \tExpected value not to be nil.\n        \tTest:       \tTestBasicExample\n"
  },
  {
    "path": "modules/logger/parser/fixtures/failing_example_expected/TestCloseChannelsClosesAll.log",
    "content": "=== RUN   TestCloseChannelsClosesAll\n=== PAUSE TestCloseChannelsClosesAll\n=== CONT  TestCloseChannelsClosesAll\nTestCloseChannelsClosesAll INFO 2018-10-20T13:15:09-07:00 Closing all the channels in log writer\n--- PASS: TestCloseChannelsClosesAll (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/failing_example_expected/TestEnsureDirectoryExistsCreatesDirectory.log",
    "content": "=== RUN   TestEnsureDirectoryExistsCreatesDirectory\n=== PAUSE TestEnsureDirectoryExistsCreatesDirectory\n=== CONT  TestEnsureDirectoryExistsCreatesDirectory\nTestEnsureDirectoryExistsCreatesDirectory INFO 2018-10-20T13:15:09-07:00 Creating directory /var/folders/n2/pljz6dq52bd1ksmw23qyr3sr0000gn/T/TestEnsureDirectoryCreatesDirectory357603033/tmpdir\n--- PASS: TestEnsureDirectoryExistsCreatesDirectory (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/failing_example_expected/TestEnsureDirectoryExistsHandlesExistingDirectory.log",
    "content": "=== RUN   TestEnsureDirectoryExistsHandlesExistingDirectory\n=== PAUSE TestEnsureDirectoryExistsHandlesExistingDirectory\n=== CONT  TestEnsureDirectoryExistsHandlesExistingDirectory\nTestEnsureDirectoryExistsHandlesExistingDirectory INFO 2018-10-20T13:15:09-07:00 Directory /var/folders/n2/pljz6dq52bd1ksmw23qyr3sr0000gn/T/TestEnsureDirectoryCreatesDirectory292537295 already exists\n--- PASS: TestEnsureDirectoryExistsHandlesExistingDirectory (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/failing_example_expected/TestGetIndent/BaseCase.log",
    "content": "=== RUN   TestGetIndent/BaseCase\n    --- PASS: TestGetIndent/BaseCase (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/failing_example_expected/TestGetIndent/EmptyString.log",
    "content": "=== RUN   TestGetIndent/EmptyString\n    --- PASS: TestGetIndent/EmptyString (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/failing_example_expected/TestGetIndent/MixTabSpace.log",
    "content": "=== RUN   TestGetIndent/MixTabSpace\n    --- PASS: TestGetIndent/MixTabSpace (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/failing_example_expected/TestGetIndent/NoIndent.log",
    "content": "=== RUN   TestGetIndent/NoIndent\n    --- PASS: TestGetIndent/NoIndent (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/failing_example_expected/TestGetIndent/Tabs.log",
    "content": "=== RUN   TestGetIndent/Tabs\n    --- PASS: TestGetIndent/Tabs (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/failing_example_expected/TestGetIndent.log",
    "content": "=== RUN   TestGetIndent\n=== PAUSE TestGetIndent\n=== CONT  TestGetIndent\n--- PASS: TestGetIndent (0.00s)\n    --- PASS: TestGetIndent/BaseCase (0.00s)\n    --- PASS: TestGetIndent/NoIndent (0.00s)\n    --- PASS: TestGetIndent/EmptyString (0.00s)\n    --- PASS: TestGetIndent/Tabs (0.00s)\n    --- PASS: TestGetIndent/MixTabSpace (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/failing_example_expected/TestGetOrCreateChannelCreatesNewChannel.log",
    "content": "=== RUN   TestGetOrCreateChannelCreatesNewChannel\n=== PAUSE TestGetOrCreateChannelCreatesNewChannel\n=== CONT  TestGetOrCreateChannelCreatesNewChannel\n--- PASS: TestGetOrCreateChannelCreatesNewChannel (0.00s)\nTestGetOrCreateChannelCreatesNewChannel INFO 2018-10-20T13:15:09-07:00 Spawned log writer for test TestGetOrCreateChannelCreatesNewChannel\nTestGetOrCreateChannelCreatesNewChannel INFO 2018-10-20T13:15:09-07:00 Storing logs for test TestGetOrCreateChannelCreatesNewChannel to /var/folders/n2/pljz6dq52bd1ksmw23qyr3sr0000gn/T/TestEnsureDirectoryCreatesDirectory867148002/TestGetOrCreateChannelCreatesNewChannel.log\nTestGetOrCreateChannelCreatesNewChannel INFO 2018-10-20T13:15:09-07:00 Creating directory /var/folders/n2/pljz6dq52bd1ksmw23qyr3sr0000gn/T/TestEnsureDirectoryCreatesDirectory867148002\nTestGetOrCreateChannelCreatesNewChannel INFO 2018-10-20T13:15:09-07:00 Channel closed for log writer of test TestGetOrCreateChannelCreatesNewChannel\nexit status 1\n"
  },
  {
    "path": "modules/logger/parser/fixtures/failing_example_expected/TestGetOrCreateChannelReturnsExistingChannel.log",
    "content": "=== RUN   TestGetOrCreateChannelReturnsExistingChannel\n=== PAUSE TestGetOrCreateChannelReturnsExistingChannel\n=== CONT  TestGetOrCreateChannelReturnsExistingChannel\n--- PASS: TestGetOrCreateChannelReturnsExistingChannel (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/failing_example_expected/TestGetOrCreateChannelSpawnsLogCollectorOnCreate.log",
    "content": "=== RUN   TestGetOrCreateChannelSpawnsLogCollectorOnCreate\n=== PAUSE TestGetOrCreateChannelSpawnsLogCollectorOnCreate\n=== CONT  TestGetOrCreateChannelSpawnsLogCollectorOnCreate\nTestGetOrCreateChannelSpawnsLogCollectorOnCreate INFO 2018-10-20T13:15:09-07:00 Spawned log writer for test TestGetOrCreateChannelSpawnsLogCollectorOnCreate\nTestGetOrCreateChannelSpawnsLogCollectorOnCreate INFO 2018-10-20T13:15:09-07:00 Storing logs for test TestGetOrCreateChannelSpawnsLogCollectorOnCreate to /var/folders/n2/pljz6dq52bd1ksmw23qyr3sr0000gn/T/TestEnsureDirectoryCreatesDirectory945346773/TestGetOrCreateChannelSpawnsLogCollectorOnCreate.log\nTestGetOrCreateChannelSpawnsLogCollectorOnCreate INFO 2018-10-20T13:15:09-07:00 Directory /var/folders/n2/pljz6dq52bd1ksmw23qyr3sr0000gn/T/TestEnsureDirectoryCreatesDirectory945346773 already exists\nTestGetOrCreateChannelSpawnsLogCollectorOnCreate INFO 2018-10-20T13:15:09-07:00 Channel closed for log writer of test TestGetOrCreateChannelSpawnsLogCollectorOnCreate\n--- PASS: TestGetOrCreateChannelSpawnsLogCollectorOnCreate (1.01s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/failing_example_expected/TestGetTestNameFromResultLine/BaseCase.log",
    "content": "=== RUN   TestGetTestNameFromResultLine/BaseCase\n    --- PASS: TestGetTestNameFromResultLine/BaseCase (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/failing_example_expected/TestGetTestNameFromResultLine/Indented.log",
    "content": "=== RUN   TestGetTestNameFromResultLine/Indented\n    --- PASS: TestGetTestNameFromResultLine/Indented (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/failing_example_expected/TestGetTestNameFromResultLine/SpecialChars.log",
    "content": "=== RUN   TestGetTestNameFromResultLine/SpecialChars\n    --- PASS: TestGetTestNameFromResultLine/SpecialChars (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/failing_example_expected/TestGetTestNameFromResultLine/WhenFailed.log",
    "content": "=== RUN   TestGetTestNameFromResultLine/WhenFailed\n    --- PASS: TestGetTestNameFromResultLine/WhenFailed (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/failing_example_expected/TestGetTestNameFromResultLine.log",
    "content": "=== RUN   TestGetTestNameFromResultLine\n=== PAUSE TestGetTestNameFromResultLine\n=== CONT  TestGetTestNameFromResultLine\n--- PASS: TestGetTestNameFromResultLine (0.00s)\n    --- PASS: TestGetTestNameFromResultLine/BaseCase (0.00s)\n    --- PASS: TestGetTestNameFromResultLine/Indented (0.00s)\n    --- PASS: TestGetTestNameFromResultLine/SpecialChars (0.00s)\n    --- PASS: TestGetTestNameFromResultLine/WhenFailed (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/failing_example_expected/TestGetTestNameFromStatusLine/BaseCase.log",
    "content": "=== RUN   TestGetTestNameFromStatusLine/BaseCase\n    --- PASS: TestGetTestNameFromStatusLine/BaseCase (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/failing_example_expected/TestGetTestNameFromStatusLine/Indented.log",
    "content": "=== RUN   TestGetTestNameFromStatusLine/Indented\n    --- PASS: TestGetTestNameFromStatusLine/Indented (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/failing_example_expected/TestGetTestNameFromStatusLine/SpecialChars.log",
    "content": "=== RUN   TestGetTestNameFromStatusLine/SpecialChars\n    --- PASS: TestGetTestNameFromStatusLine/SpecialChars (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/failing_example_expected/TestGetTestNameFromStatusLine/WhenCont.log",
    "content": "=== RUN   TestGetTestNameFromStatusLine/WhenCont\n    --- PASS: TestGetTestNameFromStatusLine/WhenCont (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/failing_example_expected/TestGetTestNameFromStatusLine/WhenPaused.log",
    "content": "=== RUN   TestGetTestNameFromStatusLine/WhenPaused\n    --- PASS: TestGetTestNameFromStatusLine/WhenPaused (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/failing_example_expected/TestGetTestNameFromStatusLine.log",
    "content": "=== RUN   TestGetTestNameFromStatusLine\n=== PAUSE TestGetTestNameFromStatusLine\n=== CONT  TestGetTestNameFromStatusLine\n--- PASS: TestGetTestNameFromStatusLine (0.00s)\n    --- PASS: TestGetTestNameFromStatusLine/BaseCase (0.00s)\n    --- PASS: TestGetTestNameFromStatusLine/Indented (0.00s)\n    --- PASS: TestGetTestNameFromStatusLine/SpecialChars (0.00s)\n    --- PASS: TestGetTestNameFromStatusLine/WhenPaused (0.00s)\n    --- PASS: TestGetTestNameFromStatusLine/WhenCont (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/failing_example_expected/TestIsEmpty.log",
    "content": "=== RUN   TestIsEmpty\n=== PAUSE TestIsEmpty\n=== CONT  TestIsEmpty\n--- PASS: TestIsEmpty (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/failing_example_expected/TestIsPanicLine/BaseCase.log",
    "content": "=== RUN   TestIsPanicLine/BaseCase\n    --- PASS: TestIsPanicLine/BaseCase (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/failing_example_expected/TestIsPanicLine/NotPanic.log",
    "content": "=== RUN   TestIsPanicLine/NotPanic\n    --- PASS: TestIsPanicLine/NotPanic (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/failing_example_expected/TestIsPanicLine.log",
    "content": "=== RUN   TestIsPanicLine\n=== PAUSE TestIsPanicLine\n=== CONT  TestIsPanicLine\n--- PASS: TestIsPanicLine (0.00s)\n    --- PASS: TestIsPanicLine/BaseCase (0.00s)\n    --- PASS: TestIsPanicLine/NotPanic (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/failing_example_expected/TestIsResultLine/BaseCase.log",
    "content": "=== RUN   TestIsResultLine/BaseCase\n    --- PASS: TestIsResultLine/BaseCase (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/failing_example_expected/TestIsResultLine/Indented.log",
    "content": "=== RUN   TestIsResultLine/Indented\n    --- PASS: TestIsResultLine/Indented (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/failing_example_expected/TestIsResultLine/NonResultLine.log",
    "content": "=== RUN   TestIsResultLine/NonResultLine\n    --- PASS: TestIsResultLine/NonResultLine (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/failing_example_expected/TestIsResultLine/SpecialChars.log",
    "content": "=== RUN   TestIsResultLine/SpecialChars\n    --- PASS: TestIsResultLine/SpecialChars (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/failing_example_expected/TestIsResultLine/WhenFailed.log",
    "content": "=== RUN   TestIsResultLine/WhenFailed\n    --- PASS: TestIsResultLine/WhenFailed (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/failing_example_expected/TestIsResultLine.log",
    "content": "=== RUN   TestIsResultLine\n=== PAUSE TestIsResultLine\n=== CONT  TestIsResultLine\n--- PASS: TestIsResultLine (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/failing_example_expected/TestIsStatusLine/BaseCase.log",
    "content": "=== RUN   TestIsStatusLine/BaseCase\n    --- PASS: TestIsStatusLine/BaseCase (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/failing_example_expected/TestIsStatusLine/Indented.log",
    "content": "=== RUN   TestIsStatusLine/Indented\n    --- PASS: TestIsStatusLine/Indented (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/failing_example_expected/TestIsStatusLine/NonStatusLine.log",
    "content": "=== RUN   TestIsStatusLine/NonStatusLine\n    --- PASS: TestIsStatusLine/NonStatusLine (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/failing_example_expected/TestIsStatusLine/SpecialChars.log",
    "content": "=== RUN   TestIsStatusLine/SpecialChars\n    --- PASS: TestIsStatusLine/SpecialChars (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/failing_example_expected/TestIsStatusLine/WhenCont.log",
    "content": "=== RUN   TestIsStatusLine/WhenCont\n    --- PASS: TestIsStatusLine/WhenCont (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/failing_example_expected/TestIsStatusLine/WhenPaused.log",
    "content": "=== RUN   TestIsStatusLine/WhenPaused\n    --- PASS: TestIsStatusLine/WhenPaused (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/failing_example_expected/TestIsStatusLine.log",
    "content": "=== RUN   TestIsStatusLine\n=== PAUSE TestIsStatusLine\n=== CONT  TestIsStatusLine\n--- PASS: TestIsStatusLine (0.00s)\n    --- PASS: TestIsStatusLine/BaseCase (0.00s)\n    --- PASS: TestIsStatusLine/Indented (0.00s)\n    --- PASS: TestIsStatusLine/SpecialChars (0.00s)\n    --- PASS: TestIsStatusLine/WhenPaused (0.00s)\n    --- PASS: TestIsStatusLine/WhenCont (0.00s)\n    --- PASS: TestIsStatusLine/NonStatusLine (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/failing_example_expected/TestIsSummaryLine/BaseCase.log",
    "content": "=== RUN   TestIsSummaryLine/BaseCase\n    --- PASS: TestIsSummaryLine/BaseCase (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/failing_example_expected/TestIsSummaryLine/NotSummary.log",
    "content": "=== RUN   TestIsSummaryLine/NotSummary\n    --- PASS: TestIsSummaryLine/NotSummary (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/failing_example_expected/TestIsSummaryLine.log",
    "content": "=== RUN   TestIsSummaryLine\n=== PAUSE TestIsSummaryLine\n=== CONT  TestIsSummaryLine\n--- PASS: TestIsSummaryLine (0.00s)\n    --- PASS: TestIsSummaryLine/BaseCase (0.00s)\n    --- PASS: TestIsSummaryLine/NotSummary (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/failing_example_expected/TestLogCollectorCreatesAndWritesToFile.log",
    "content": "=== RUN   TestLogCollectorCreatesAndWritesToFile\n=== PAUSE TestLogCollectorCreatesAndWritesToFile\n=== CONT  TestLogCollectorCreatesAndWritesToFile\nTestLogCollectorCreatesAndWritesToFile INFO 2018-10-20T13:15:09-07:00 Spawned log writer for test TestLogCollectorCreatesAndWritesToFile\nTestLogCollectorCreatesAndWritesToFile INFO 2018-10-20T13:15:09-07:00 Storing logs for test TestLogCollectorCreatesAndWritesToFile to /var/folders/n2/pljz6dq52bd1ksmw23qyr3sr0000gn/T/TestLogCollectorCreatesAndWritesToFile262063152/TestLogCollectorCreatesAndWritesToFile.log\nTestLogCollectorCreatesAndWritesToFile INFO 2018-10-20T13:15:09-07:00 Directory /var/folders/n2/pljz6dq52bd1ksmw23qyr3sr0000gn/T/TestLogCollectorCreatesAndWritesToFile262063152 already exists\nTestLogCollectorCreatesAndWritesToFile INFO 2018-10-20T13:15:09-07:00 Channel closed for log writer of test TestLogCollectorCreatesAndWritesToFile\n--- PASS: TestLogCollectorCreatesAndWritesToFile (1.01s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/failing_example_expected/TestPanicExample.log",
    "content": "=== RUN   TestPanicExample\n--- FAIL: TestPanicExample (0.00s)\n    integration_test.go:14:\n        \tError Trace:\tintegration_test.go:14\n        \tError:      \tExpected value not to be nil.\n        \tTest:       \tTestPanicExample\n"
  },
  {
    "path": "modules/logger/parser/fixtures/failing_example_expected/TestPeek.log",
    "content": "=== RUN   TestPeek\n=== PAUSE TestPeek\n=== CONT  TestPeek\n--- PASS: TestPeek (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/failing_example_expected/TestPeekEmpty.log",
    "content": "=== RUN   TestPeekEmpty\n=== PAUSE TestPeekEmpty\n=== CONT  TestPeekEmpty\n--- PASS: TestPeekEmpty (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/failing_example_expected/TestRealWorldExample.log",
    "content": "=== RUN   TestRealWorldExample\n--- FAIL: TestRealWorldExample (0.00s)\n    integration_test.go:18:\n        \tError Trace:\tintegration_test.go:18\n        \tError:      \tExpected value not to be nil.\n        \tTest:       \tTestRealWorldExample\n"
  },
  {
    "path": "modules/logger/parser/fixtures/failing_example_expected/TestRemoveDedentedTestResultMarkers.log",
    "content": "=== RUN   TestRemoveDedentedTestResultMarkers\n--- PASS: TestRemoveDedentedTestResultMarkers (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/failing_example_expected/TestRemoveDedentedTestResultMarkersAll.log",
    "content": "=== RUN   TestRemoveDedentedTestResultMarkersAll\n--- PASS: TestRemoveDedentedTestResultMarkersAll (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/failing_example_expected/TestRemoveDedentedTestResultMarkersEmpty.log",
    "content": "=== RUN   TestRemoveDedentedTestResultMarkersEmpty\n--- PASS: TestRemoveDedentedTestResultMarkersEmpty (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/failing_example_expected/TestStackPop.log",
    "content": "=== RUN   TestStackPop\n=== PAUSE TestStackPop\n=== CONT  TestStackPop\n--- PASS: TestStackPop (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/failing_example_expected/TestStackPopEmpty.log",
    "content": "=== RUN   TestStackPopEmpty\n=== PAUSE TestStackPopEmpty\n=== CONT  TestStackPopEmpty\n--- PASS: TestStackPopEmpty (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/failing_example_expected/TestStackPush.log",
    "content": "=== RUN   TestStackPush\n=== PAUSE TestStackPush\n=== CONT  TestStackPush\n--- PASS: TestStackPush (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/failing_example_expected/report.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<testsuites>\n\t<testsuite tests=\"55\" failures=\"3\" time=\"1.020\" name=\"github.com/gruntwork-io/terratest/modules/logger/parser\">\n\t\t<properties>\n\t\t\t<property name=\"go.version\" value=\"go1.21.1\"></property>\n\t\t</properties>\n\t\t<testcase classname=\"parser\" name=\"TestStackPush\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestStackPop\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestStackPopEmpty\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestPeek\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestPeekEmpty\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestIsEmpty\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestRemoveDedentedTestResultMarkers\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestRemoveDedentedTestResultMarkersEmpty\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestRemoveDedentedTestResultMarkersAll\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestBasicExample\" time=\"0.000\">\n\t\t\t<failure message=\"Failed\" type=\"\">integration_test.go:10:&#xA;Error Trace:&#x9;integration_test.go:10&#xA;Error:      &#x9;Expected value not to be nil.&#xA;Test:       &#x9;TestBasicExample</failure>\n\t\t</testcase>\n\t\t<testcase classname=\"parser\" name=\"TestPanicExample\" time=\"0.000\">\n\t\t\t<failure message=\"Failed\" type=\"\">integration_test.go:14:&#xA;Error Trace:&#x9;integration_test.go:14&#xA;Error:      &#x9;Expected value not to be nil.&#xA;Test:       &#x9;TestPanicExample</failure>\n\t\t</testcase>\n\t\t<testcase classname=\"parser\" name=\"TestRealWorldExample\" time=\"0.000\">\n\t\t\t<failure message=\"Failed\" type=\"\">integration_test.go:18:&#xA;Error Trace:&#x9;integration_test.go:18&#xA;Error:      &#x9;Expected value not to be nil.&#xA;Test:       &#x9;TestRealWorldExample</failure>\n\t\t</testcase>\n\t\t<testcase classname=\"parser\" name=\"TestGetIndent\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestGetTestNameFromResultLine\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestIsResultLine\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestGetTestNameFromStatusLine\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestIsStatusLine\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestIsSummaryLine\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestIsPanicLine\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestEnsureDirectoryExistsCreatesDirectory\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestEnsureDirectoryExistsHandlesExistingDirectory\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestGetOrCreateChannelCreatesNewChannel\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestGetOrCreateChannelReturnsExistingChannel\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestLogCollectorCreatesAndWritesToFile\" time=\"1.010\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestGetOrCreateChannelSpawnsLogCollectorOnCreate\" time=\"1.010\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestCloseChannelsClosesAll\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestIsStatusLine/BaseCase\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestGetTestNameFromStatusLine/BaseCase\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestIsResultLine/BaseCase\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestIsSummaryLine/BaseCase\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestGetTestNameFromStatusLine/Indented\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestIsResultLine/Indented\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestIsStatusLine/Indented\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestGetTestNameFromStatusLine/SpecialChars\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestIsResultLine/SpecialChars\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestIsStatusLine/SpecialChars\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestGetTestNameFromStatusLine/WhenPaused\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestIsResultLine/WhenFailed\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestIsStatusLine/WhenPaused\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestGetTestNameFromStatusLine/WhenCont\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestIsStatusLine/WhenCont\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestIsResultLine/NonResultLine\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestIsStatusLine/NonStatusLine\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestGetTestNameFromResultLine/BaseCase\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestGetIndent/BaseCase\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestGetTestNameFromResultLine/Indented\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestGetTestNameFromResultLine/SpecialChars\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestGetTestNameFromResultLine/WhenFailed\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestGetIndent/NoIndent\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestGetIndent/EmptyString\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestGetIndent/Tabs\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestGetIndent/MixTabSpace\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestIsSummaryLine/NotSummary\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestIsPanicLine/BaseCase\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestIsPanicLine/NotPanic\" time=\"0.000\"></testcase>\n\t</testsuite>\n</testsuites>\n"
  },
  {
    "path": "modules/logger/parser/fixtures/failing_example_expected/summary.log",
    "content": "--- PASS: TestRemoveDedentedTestResultMarkers (0.00s)\n--- PASS: TestRemoveDedentedTestResultMarkersEmpty (0.00s)\n--- PASS: TestRemoveDedentedTestResultMarkersAll (0.00s)\n--- FAIL: TestBasicExample (0.00s)\n--- FAIL: TestPanicExample (0.00s)\n--- FAIL: TestRealWorldExample (0.00s)\n--- PASS: TestStackPush (0.00s)\n--- PASS: TestGetTestNameFromStatusLine (0.00s)\n    --- PASS: TestGetTestNameFromStatusLine/BaseCase (0.00s)\n    --- PASS: TestGetTestNameFromStatusLine/Indented (0.00s)\n    --- PASS: TestGetTestNameFromStatusLine/SpecialChars (0.00s)\n    --- PASS: TestGetTestNameFromStatusLine/WhenPaused (0.00s)\n    --- PASS: TestGetTestNameFromStatusLine/WhenCont (0.00s)\n--- PASS: TestIsStatusLine (0.00s)\n    --- PASS: TestIsStatusLine/BaseCase (0.00s)\n    --- PASS: TestIsStatusLine/Indented (0.00s)\n    --- PASS: TestIsStatusLine/SpecialChars (0.00s)\n    --- PASS: TestIsStatusLine/WhenPaused (0.00s)\n    --- PASS: TestIsStatusLine/WhenCont (0.00s)\n    --- PASS: TestIsStatusLine/NonStatusLine (0.00s)\n--- PASS: TestIsEmpty (0.00s)\n--- PASS: TestPeekEmpty (0.00s)\n--- PASS: TestPeek (0.00s)\n--- PASS: TestGetTestNameFromResultLine (0.00s)\n    --- PASS: TestGetTestNameFromResultLine/BaseCase (0.00s)\n    --- PASS: TestGetTestNameFromResultLine/Indented (0.00s)\n    --- PASS: TestGetTestNameFromResultLine/SpecialChars (0.00s)\n    --- PASS: TestGetTestNameFromResultLine/WhenFailed (0.00s)\n--- PASS: TestStackPopEmpty (0.00s)\n--- PASS: TestGetIndent (0.00s)\n    --- PASS: TestGetIndent/BaseCase (0.00s)\n    --- PASS: TestGetIndent/NoIndent (0.00s)\n    --- PASS: TestGetIndent/EmptyString (0.00s)\n    --- PASS: TestGetIndent/Tabs (0.00s)\n    --- PASS: TestGetIndent/MixTabSpace (0.00s)\n--- PASS: TestGetOrCreateChannelReturnsExistingChannel (0.00s)\n--- PASS: TestStackPop (0.00s)\n--- PASS: TestCloseChannelsClosesAll (0.00s)\n--- PASS: TestIsSummaryLine (0.00s)\n    --- PASS: TestIsSummaryLine/BaseCase (0.00s)\n    --- PASS: TestIsSummaryLine/NotSummary (0.00s)\n--- PASS: TestGetOrCreateChannelCreatesNewChannel (0.00s)\n--- PASS: TestIsResultLine (0.00s)\n    --- PASS: TestIsResultLine/BaseCase (0.00s)\n    --- PASS: TestIsResultLine/Indented (0.00s)\n    --- PASS: TestIsResultLine/SpecialChars (0.00s)\n    --- PASS: TestIsResultLine/WhenFailed (0.00s)\n    --- PASS: TestIsResultLine/NonResultLine (0.00s)\n--- PASS: TestEnsureDirectoryExistsHandlesExistingDirectory (0.00s)\n--- PASS: TestIsPanicLine (0.00s)\n    --- PASS: TestIsPanicLine/BaseCase (0.00s)\n    --- PASS: TestIsPanicLine/NotPanic (0.00s)\n--- PASS: TestEnsureDirectoryExistsCreatesDirectory (0.00s)\n--- PASS: TestLogCollectorCreatesAndWritesToFile (1.01s)\n--- PASS: TestGetOrCreateChannelSpawnsLogCollectorOnCreate (1.01s)\nFAIL\nFAIL\tgithub.com/gruntwork-io/terratest/modules/logger/parser\t1.020s\n"
  },
  {
    "path": "modules/logger/parser/fixtures/new_go_failing_example.log",
    "content": "=== RUN   TestIntegrationBasicExample\n=== PAUSE TestIntegrationBasicExample\n=== RUN   TestIntegrationFailingExample\n=== PAUSE TestIntegrationFailingExample\n=== RUN   TestIntegrationPanicExample\n=== PAUSE TestIntegrationPanicExample\n=== CONT  TestIntegrationBasicExample\n=== CONT  TestIntegrationPanicExample\n=== CONT  TestIntegrationFailingExample\n=== CONT  TestIntegrationBasicExample\n    integration_test.go:57: \n        \tError Trace:\tintegration_test.go:57\n        \tError:      \tShould be true\n        \tTest:       \tTestIntegrationBasicExample\n--- PASS: TestIntegrationPanicExample (0.00s)\n--- PASS: TestIntegrationFailingExample (0.00s)\n--- FAIL: TestIntegrationBasicExample (0.00s)\nFAIL\nFAIL\tgithub.com/gruntwork-io/terratest/modules/logger/parser\t1.589s\nFAIL\n"
  },
  {
    "path": "modules/logger/parser/fixtures/new_go_failing_example_expected/TestIntegrationBasicExample.log",
    "content": "=== RUN   TestIntegrationBasicExample\n=== PAUSE TestIntegrationBasicExample\n=== CONT  TestIntegrationBasicExample\n=== CONT  TestIntegrationBasicExample\n    integration_test.go:57: \n        \tError Trace:\tintegration_test.go:57\n        \tError:      \tShould be true\n        \tTest:       \tTestIntegrationBasicExample\n--- FAIL: TestIntegrationBasicExample (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/new_go_failing_example_expected/TestIntegrationFailingExample.log",
    "content": "=== RUN   TestIntegrationFailingExample\n=== PAUSE TestIntegrationFailingExample\n=== CONT  TestIntegrationFailingExample\n--- PASS: TestIntegrationFailingExample (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/new_go_failing_example_expected/TestIntegrationPanicExample.log",
    "content": "=== RUN   TestIntegrationPanicExample\n=== PAUSE TestIntegrationPanicExample\n=== CONT  TestIntegrationPanicExample\n--- PASS: TestIntegrationPanicExample (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/new_go_failing_example_expected/report.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<testsuites>\n\t<testsuite tests=\"3\" failures=\"1\" time=\"1.589\" name=\"github.com/gruntwork-io/terratest/modules/logger/parser\">\n\t\t<properties>\n\t\t\t<property name=\"go.version\" value=\"go1.21.1\"></property>\n\t\t</properties>\n\t\t<testcase classname=\"parser\" name=\"TestIntegrationBasicExample\" time=\"0.000\">\n\t\t\t<failure message=\"Failed\" type=\"\">    integration_test.go:57: </failure>\n\t\t</testcase>\n\t\t<testcase classname=\"parser\" name=\"TestIntegrationFailingExample\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestIntegrationPanicExample\" time=\"0.000\"></testcase>\n\t</testsuite>\n</testsuites>\n"
  },
  {
    "path": "modules/logger/parser/fixtures/new_go_failing_example_expected/summary.log",
    "content": "--- PASS: TestIntegrationPanicExample (0.00s)\n--- PASS: TestIntegrationFailingExample (0.00s)\n--- FAIL: TestIntegrationBasicExample (0.00s)\nFAIL\nFAIL\tgithub.com/gruntwork-io/terratest/modules/logger/parser\t1.589s\nFAIL\n"
  },
  {
    "path": "modules/logger/parser/fixtures/panic_example.log",
    "content": "=== RUN   TestStackPush\n=== PAUSE TestStackPush\n=== RUN   TestStackPop\n=== PAUSE TestStackPop\n=== RUN   TestStackPopEmpty\n=== PAUSE TestStackPopEmpty\n=== RUN   TestPeek\n=== PAUSE TestPeek\n=== RUN   TestPeekEmpty\n=== PAUSE TestPeekEmpty\n=== RUN   TestIsEmpty\n=== PAUSE TestIsEmpty\n=== RUN   TestRemoveDedentedTestResultMarkers\n--- PASS: TestRemoveDedentedTestResultMarkers (0.00s)\n=== RUN   TestRemoveDedentedTestResultMarkersEmpty\n--- PASS: TestRemoveDedentedTestResultMarkersEmpty (0.00s)\n=== RUN   TestRemoveDedentedTestResultMarkersAll\n--- PASS: TestRemoveDedentedTestResultMarkersAll (0.00s)\n=== RUN   TestGetIndent\n=== PAUSE TestGetIndent\n=== RUN   TestGetTestNameFromResultLine\n=== PAUSE TestGetTestNameFromResultLine\n=== RUN   TestIsResultLine\n=== PAUSE TestIsResultLine\n=== RUN   TestGetTestNameFromStatusLine\n=== PAUSE TestGetTestNameFromStatusLine\n=== RUN   TestIsStatusLine\n=== PAUSE TestIsStatusLine\n=== RUN   TestIsSummaryLine\n=== PAUSE TestIsSummaryLine\n=== RUN   TestIsPanicLine\n=== PAUSE TestIsPanicLine\n=== RUN   TestEnsureDirectoryExistsCreatesDirectory\n=== PAUSE TestEnsureDirectoryExistsCreatesDirectory\n=== RUN   TestEnsureDirectoryExistsHandlesExistingDirectory\n=== PAUSE TestEnsureDirectoryExistsHandlesExistingDirectory\n=== RUN   TestGetOrCreateChannelCreatesNewChannel\n=== PAUSE TestGetOrCreateChannelCreatesNewChannel\n=== RUN   TestGetOrCreateChannelReturnsExistingChannel\n=== PAUSE TestGetOrCreateChannelReturnsExistingChannel\n=== RUN   TestLogCollectorCreatesAndWritesToFile\n=== PAUSE TestLogCollectorCreatesAndWritesToFile\n=== RUN   TestGetOrCreateChannelSpawnsLogCollectorOnCreate\n=== PAUSE TestGetOrCreateChannelSpawnsLogCollectorOnCreate\n=== RUN   TestCloseChannelsClosesAll\n=== PAUSE TestCloseChannelsClosesAll\n=== CONT  TestStackPush\n=== CONT  TestIsSummaryLine\n=== CONT  TestGetIndent\n=== RUN   TestIsSummaryLine/BaseCase\n=== RUN   TestGetIndent/BaseCase\n=== CONT  TestPeek\n--- PASS: TestStackPush (0.00s)\n=== CONT  TestIsStatusLine\n=== RUN   TestIsStatusLine/BaseCase\n=== RUN   TestIsSummaryLine/NotSummary\n=== RUN   TestIsStatusLine/Indented\n=== RUN   TestGetIndent/NoIndent\n--- PASS: TestIsSummaryLine (0.00s)\n    --- PASS: TestIsSummaryLine/BaseCase (0.00s)\n    --- PASS: TestIsSummaryLine/NotSummary (0.00s)\n=== CONT  TestGetTestNameFromStatusLine\n=== RUN   TestIsStatusLine/SpecialChars\n=== RUN   TestGetTestNameFromStatusLine/BaseCase\n=== RUN   TestGetIndent/EmptyString\n=== RUN   TestIsStatusLine/WhenPaused\n=== RUN   TestGetTestNameFromStatusLine/Indented\n=== RUN   TestIsStatusLine/WhenCont\n=== RUN   TestGetIndent/Tabs\n=== RUN   TestGetTestNameFromStatusLine/SpecialChars\n=== CONT  TestIsResultLine\n--- PASS: TestPeek (0.00s)\n=== RUN   TestIsResultLine/BaseCase\n=== RUN   TestIsStatusLine/NonStatusLine\n=== RUN   TestGetIndent/MixTabSpace\n--- PASS: TestIsStatusLine (0.00s)\n    --- PASS: TestIsStatusLine/BaseCase (0.00s)\n    --- PASS: TestIsStatusLine/Indented (0.00s)\n    --- PASS: TestIsStatusLine/SpecialChars (0.00s)\n    --- PASS: TestIsStatusLine/WhenPaused (0.00s)\n    --- PASS: TestIsStatusLine/WhenCont (0.00s)\n    --- PASS: TestIsStatusLine/NonStatusLine (0.00s)\n=== CONT  TestGetTestNameFromResultLine\n=== RUN   TestGetTestNameFromStatusLine/WhenPaused\n=== RUN   TestGetTestNameFromResultLine/BaseCase\n=== RUN   TestGetTestNameFromStatusLine/WhenCont\n--- PASS: TestGetIndent (0.00s)\n    --- PASS: TestGetIndent/BaseCase (0.00s)\n    --- PASS: TestGetIndent/NoIndent (0.00s)\n    --- PASS: TestGetIndent/EmptyString (0.00s)\n    --- PASS: TestGetIndent/Tabs (0.00s)\n    --- PASS: TestGetIndent/MixTabSpace (0.00s)\n=== CONT  TestGetOrCreateChannelReturnsExistingChannel\n--- PASS: TestGetTestNameFromStatusLine (0.00s)\n    --- PASS: TestGetTestNameFromStatusLine/BaseCase (0.00s)\n    --- PASS: TestGetTestNameFromStatusLine/Indented (0.00s)\n    --- PASS: TestGetTestNameFromStatusLine/SpecialChars (0.00s)\n    --- PASS: TestGetTestNameFromStatusLine/WhenPaused (0.00s)\n    --- PASS: TestGetTestNameFromStatusLine/WhenCont (0.00s)\n=== CONT  TestIsEmpty\n=== RUN   TestGetTestNameFromResultLine/Indented\n=== RUN   TestGetTestNameFromResultLine/SpecialChars\n--- PASS: TestIsEmpty (0.00s)\n=== CONT  TestPeekEmpty\n--- PASS: TestPeekEmpty (0.00s)\n=== RUN   TestGetTestNameFromResultLine/WhenFailed\n=== CONT  TestStackPopEmpty\n--- PASS: TestStackPopEmpty (0.00s)\n=== CONT  TestGetOrCreateChannelSpawnsLogCollectorOnCreate\n--- PASS: TestGetTestNameFromResultLine (0.00s)\n    --- PASS: TestGetTestNameFromResultLine/BaseCase (0.00s)\n    --- PASS: TestGetTestNameFromResultLine/Indented (0.00s)\n    --- PASS: TestGetTestNameFromResultLine/SpecialChars (0.00s)\n    --- PASS: TestGetTestNameFromResultLine/WhenFailed (0.00s)\n=== CONT  TestCloseChannelsClosesAll\n=== RUN   TestIsResultLine/Indented\n=== RUN   TestIsResultLine/SpecialChars\n=== RUN   TestIsResultLine/WhenFailed\n=== RUN   TestIsResultLine/NonResultLine\n--- PASS: TestIsResultLine (0.00s)\n    --- PASS: TestIsResultLine/BaseCase (0.00s)\n    --- PASS: TestIsResultLine/Indented (0.00s)\n    --- PASS: TestIsResultLine/SpecialChars (0.00s)\n    --- PASS: TestIsResultLine/WhenFailed (0.00s)\n    --- PASS: TestIsResultLine/NonResultLine (0.00s)\n=== CONT  TestStackPop\n--- PASS: TestStackPop (0.00s)\n=== CONT  TestLogCollectorCreatesAndWritesToFile\n=== CONT  TestEnsureDirectoryExistsHandlesExistingDirectory\n--- PASS: TestGetOrCreateChannelReturnsExistingChannel (0.00s)\nTestGetOrCreateChannelSpawnsLogCollectorOnCreate INFO 2018-10-20T13:03:19-07:00 Spawned log writer for test TestGetOrCreateChannelSpawnsLogCollectorOnCreate\nTestCloseChannelsClosesAll INFO 2018-10-20T13:03:19-07:00 Closing all the channels in log writer\nTestGetOrCreateChannelSpawnsLogCollectorOnCreate INFO 2018-10-20T13:03:19-07:00 Storing logs for test TestGetOrCreateChannelSpawnsLogCollectorOnCreate to /var/folders/n2/pljz6dq52bd1ksmw23qyr3sr0000gn/T/TestEnsureDirectoryCreatesDirectory724282597/TestGetOrCreateChannelSpawnsLogCollectorOnCreate.log\nTestEnsureDirectoryExistsHandlesExistingDirectory INFO 2018-10-20T13:03:19-07:00 Directory /var/folders/n2/pljz6dq52bd1ksmw23qyr3sr0000gn/T/TestEnsureDirectoryCreatesDirectory135329330 already exists\nTestGetOrCreateChannelSpawnsLogCollectorOnCreate INFO 2018-10-20T13:03:19-07:00 Directory /var/folders/n2/pljz6dq52bd1ksmw23qyr3sr0000gn/T/TestEnsureDirectoryCreatesDirectory724282597 already exists\n--- PASS: TestCloseChannelsClosesAll (0.00s)\n=== CONT  TestGetOrCreateChannelCreatesNewChannel\n--- PASS: TestEnsureDirectoryExistsHandlesExistingDirectory (0.00s)\n=== CONT  TestEnsureDirectoryExistsCreatesDirectory\nTestLogCollectorCreatesAndWritesToFile INFO 2018-10-20T13:03:19-07:00 Spawned log writer for test TestLogCollectorCreatesAndWritesToFile\nTestGetOrCreateChannelSpawnsLogCollectorOnCreate INFO 2018-10-20T13:03:19-07:00 Channel closed for log writer of test TestGetOrCreateChannelSpawnsLogCollectorOnCreate\nTestEnsureDirectoryExistsCreatesDirectory INFO 2018-10-20T13:03:19-07:00 Creating directory /var/folders/n2/pljz6dq52bd1ksmw23qyr3sr0000gn/T/TestEnsureDirectoryCreatesDirectory601920052/tmpdir\n--- PASS: TestGetOrCreateChannelCreatesNewChannel (0.00s)\n=== CONT  TestIsPanicLine\n=== RUN   TestIsPanicLine/BaseCase\n=== RUN   TestIsPanicLine/NotPanic\n--- FAIL: TestIsPanicLine (0.00s)\n    --- PASS: TestIsPanicLine/BaseCase (0.00s)\n    --- PASS: TestIsPanicLine/NotPanic (0.00s)\npanic: error [recovered]\n\tpanic: error\n\ngoroutine 36 [running]:\ntesting.tRunner.func1(0xc0000c5300)\n\t/usr/local/Cellar/go/1.11/libexec/src/testing/testing.go:792 +0x387\npanic(0x1329720, 0x13fd400)\n\t/usr/local/Cellar/go/1.11/libexec/src/runtime/panic.go:513 +0x1b9\ngithub.com/gruntwork-io/terratest/modules/logger/parser.TestIsPanicLine(0xc0000c5300)\n\t/Users/yoriy/go/src/github.com/gruntwork-io/terratest/modules/logger/parser/parser_test.go:306 +0x1c4\ntesting.tRunner(0xc0000c5300, 0x13bb160)\n\t/usr/local/Cellar/go/1.11/libexec/src/testing/testing.go:827 +0xbf\ncreated by testing.(*T).Run\n\t/usr/local/Cellar/go/1.11/libexec/src/testing/testing.go:878 +0x353\nexit status 2\nFAIL\tgithub.com/gruntwork-io/terratest/modules/logger/parser\t0.020s\n"
  },
  {
    "path": "modules/logger/parser/fixtures/panic_example_expected/TestCloseChannelsClosesAll.log",
    "content": "=== RUN   TestCloseChannelsClosesAll\n=== PAUSE TestCloseChannelsClosesAll\n=== CONT  TestCloseChannelsClosesAll\nTestCloseChannelsClosesAll INFO 2018-10-20T13:03:19-07:00 Closing all the channels in log writer\n--- PASS: TestCloseChannelsClosesAll (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/panic_example_expected/TestEnsureDirectoryExistsCreatesDirectory.log",
    "content": "=== RUN   TestEnsureDirectoryExistsCreatesDirectory\n=== PAUSE TestEnsureDirectoryExistsCreatesDirectory\n=== CONT  TestEnsureDirectoryExistsCreatesDirectory\nTestEnsureDirectoryExistsCreatesDirectory INFO 2018-10-20T13:03:19-07:00 Creating directory /var/folders/n2/pljz6dq52bd1ksmw23qyr3sr0000gn/T/TestEnsureDirectoryCreatesDirectory601920052/tmpdir\n"
  },
  {
    "path": "modules/logger/parser/fixtures/panic_example_expected/TestEnsureDirectoryExistsHandlesExistingDirectory.log",
    "content": "=== RUN   TestEnsureDirectoryExistsHandlesExistingDirectory\n=== PAUSE TestEnsureDirectoryExistsHandlesExistingDirectory\n=== CONT  TestEnsureDirectoryExistsHandlesExistingDirectory\nTestEnsureDirectoryExistsHandlesExistingDirectory INFO 2018-10-20T13:03:19-07:00 Directory /var/folders/n2/pljz6dq52bd1ksmw23qyr3sr0000gn/T/TestEnsureDirectoryCreatesDirectory135329330 already exists\n--- PASS: TestEnsureDirectoryExistsHandlesExistingDirectory (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/panic_example_expected/TestGetIndent/BaseCase.log",
    "content": "=== RUN   TestGetIndent/BaseCase\n    --- PASS: TestGetIndent/BaseCase (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/panic_example_expected/TestGetIndent/EmptyString.log",
    "content": "=== RUN   TestGetIndent/EmptyString\n    --- PASS: TestGetIndent/EmptyString (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/panic_example_expected/TestGetIndent/MixTabSpace.log",
    "content": "=== RUN   TestGetIndent/MixTabSpace\n    --- PASS: TestGetIndent/MixTabSpace (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/panic_example_expected/TestGetIndent/NoIndent.log",
    "content": "=== RUN   TestGetIndent/NoIndent\n    --- PASS: TestGetIndent/NoIndent (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/panic_example_expected/TestGetIndent/Tabs.log",
    "content": "=== RUN   TestGetIndent/Tabs\n    --- PASS: TestGetIndent/Tabs (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/panic_example_expected/TestGetIndent.log",
    "content": "=== RUN   TestGetIndent\n=== PAUSE TestGetIndent\n=== CONT  TestGetIndent\n--- PASS: TestGetIndent (0.00s)\n    --- PASS: TestGetIndent/BaseCase (0.00s)\n    --- PASS: TestGetIndent/NoIndent (0.00s)\n    --- PASS: TestGetIndent/EmptyString (0.00s)\n    --- PASS: TestGetIndent/Tabs (0.00s)\n    --- PASS: TestGetIndent/MixTabSpace (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/panic_example_expected/TestGetOrCreateChannelCreatesNewChannel.log",
    "content": "=== RUN   TestGetOrCreateChannelCreatesNewChannel\n=== PAUSE TestGetOrCreateChannelCreatesNewChannel\n=== CONT  TestGetOrCreateChannelCreatesNewChannel\n--- PASS: TestGetOrCreateChannelCreatesNewChannel (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/panic_example_expected/TestGetOrCreateChannelReturnsExistingChannel.log",
    "content": "=== RUN   TestGetOrCreateChannelReturnsExistingChannel\n=== PAUSE TestGetOrCreateChannelReturnsExistingChannel\n=== CONT  TestGetOrCreateChannelReturnsExistingChannel\n--- PASS: TestGetOrCreateChannelReturnsExistingChannel (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/panic_example_expected/TestGetOrCreateChannelSpawnsLogCollectorOnCreate.log",
    "content": "=== RUN   TestGetOrCreateChannelSpawnsLogCollectorOnCreate\n=== PAUSE TestGetOrCreateChannelSpawnsLogCollectorOnCreate\n=== CONT  TestGetOrCreateChannelSpawnsLogCollectorOnCreate\nTestGetOrCreateChannelSpawnsLogCollectorOnCreate INFO 2018-10-20T13:03:19-07:00 Spawned log writer for test TestGetOrCreateChannelSpawnsLogCollectorOnCreate\nTestGetOrCreateChannelSpawnsLogCollectorOnCreate INFO 2018-10-20T13:03:19-07:00 Storing logs for test TestGetOrCreateChannelSpawnsLogCollectorOnCreate to /var/folders/n2/pljz6dq52bd1ksmw23qyr3sr0000gn/T/TestEnsureDirectoryCreatesDirectory724282597/TestGetOrCreateChannelSpawnsLogCollectorOnCreate.log\nTestGetOrCreateChannelSpawnsLogCollectorOnCreate INFO 2018-10-20T13:03:19-07:00 Directory /var/folders/n2/pljz6dq52bd1ksmw23qyr3sr0000gn/T/TestEnsureDirectoryCreatesDirectory724282597 already exists\nTestGetOrCreateChannelSpawnsLogCollectorOnCreate INFO 2018-10-20T13:03:19-07:00 Channel closed for log writer of test TestGetOrCreateChannelSpawnsLogCollectorOnCreate\n"
  },
  {
    "path": "modules/logger/parser/fixtures/panic_example_expected/TestGetTestNameFromResultLine/BaseCase.log",
    "content": "=== RUN   TestGetTestNameFromResultLine/BaseCase\n    --- PASS: TestGetTestNameFromResultLine/BaseCase (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/panic_example_expected/TestGetTestNameFromResultLine/Indented.log",
    "content": "=== RUN   TestGetTestNameFromResultLine/Indented\n    --- PASS: TestGetTestNameFromResultLine/Indented (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/panic_example_expected/TestGetTestNameFromResultLine/SpecialChars.log",
    "content": "=== RUN   TestGetTestNameFromResultLine/SpecialChars\n    --- PASS: TestGetTestNameFromResultLine/SpecialChars (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/panic_example_expected/TestGetTestNameFromResultLine/WhenFailed.log",
    "content": "=== RUN   TestGetTestNameFromResultLine/WhenFailed\n    --- PASS: TestGetTestNameFromResultLine/WhenFailed (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/panic_example_expected/TestGetTestNameFromResultLine.log",
    "content": "=== RUN   TestGetTestNameFromResultLine\n=== PAUSE TestGetTestNameFromResultLine\n=== CONT  TestGetTestNameFromResultLine\n--- PASS: TestGetTestNameFromResultLine (0.00s)\n    --- PASS: TestGetTestNameFromResultLine/BaseCase (0.00s)\n    --- PASS: TestGetTestNameFromResultLine/Indented (0.00s)\n    --- PASS: TestGetTestNameFromResultLine/SpecialChars (0.00s)\n    --- PASS: TestGetTestNameFromResultLine/WhenFailed (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/panic_example_expected/TestGetTestNameFromStatusLine/BaseCase.log",
    "content": "=== RUN   TestGetTestNameFromStatusLine/BaseCase\n    --- PASS: TestGetTestNameFromStatusLine/BaseCase (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/panic_example_expected/TestGetTestNameFromStatusLine/Indented.log",
    "content": "=== RUN   TestGetTestNameFromStatusLine/Indented\n    --- PASS: TestGetTestNameFromStatusLine/Indented (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/panic_example_expected/TestGetTestNameFromStatusLine/SpecialChars.log",
    "content": "=== RUN   TestGetTestNameFromStatusLine/SpecialChars\n    --- PASS: TestGetTestNameFromStatusLine/SpecialChars (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/panic_example_expected/TestGetTestNameFromStatusLine/WhenCont.log",
    "content": "=== RUN   TestGetTestNameFromStatusLine/WhenCont\n    --- PASS: TestGetTestNameFromStatusLine/WhenCont (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/panic_example_expected/TestGetTestNameFromStatusLine/WhenPaused.log",
    "content": "=== RUN   TestGetTestNameFromStatusLine/WhenPaused\n    --- PASS: TestGetTestNameFromStatusLine/WhenPaused (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/panic_example_expected/TestGetTestNameFromStatusLine.log",
    "content": "=== RUN   TestGetTestNameFromStatusLine\n=== PAUSE TestGetTestNameFromStatusLine\n=== CONT  TestGetTestNameFromStatusLine\n--- PASS: TestGetTestNameFromStatusLine (0.00s)\n    --- PASS: TestGetTestNameFromStatusLine/BaseCase (0.00s)\n    --- PASS: TestGetTestNameFromStatusLine/Indented (0.00s)\n    --- PASS: TestGetTestNameFromStatusLine/SpecialChars (0.00s)\n    --- PASS: TestGetTestNameFromStatusLine/WhenPaused (0.00s)\n    --- PASS: TestGetTestNameFromStatusLine/WhenCont (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/panic_example_expected/TestIsEmpty.log",
    "content": "=== RUN   TestIsEmpty\n=== PAUSE TestIsEmpty\n=== CONT  TestIsEmpty\n--- PASS: TestIsEmpty (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/panic_example_expected/TestIsPanicLine/BaseCase.log",
    "content": "=== RUN   TestIsPanicLine/BaseCase\n    --- PASS: TestIsPanicLine/BaseCase (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/panic_example_expected/TestIsPanicLine/NotPanic.log",
    "content": "=== RUN   TestIsPanicLine/NotPanic\n    --- PASS: TestIsPanicLine/NotPanic (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/panic_example_expected/TestIsPanicLine.log",
    "content": "=== RUN   TestIsPanicLine\n=== PAUSE TestIsPanicLine\n=== CONT  TestIsPanicLine\n--- FAIL: TestIsPanicLine (0.00s)\n    --- PASS: TestIsPanicLine/BaseCase (0.00s)\n    --- PASS: TestIsPanicLine/NotPanic (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/panic_example_expected/TestIsResultLine/BaseCase.log",
    "content": "=== RUN   TestIsResultLine/BaseCase\n    --- PASS: TestIsResultLine/BaseCase (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/panic_example_expected/TestIsResultLine/Indented.log",
    "content": "=== RUN   TestIsResultLine/Indented\n    --- PASS: TestIsResultLine/Indented (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/panic_example_expected/TestIsResultLine/NonResultLine.log",
    "content": "=== RUN   TestIsResultLine/NonResultLine\n    --- PASS: TestIsResultLine/NonResultLine (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/panic_example_expected/TestIsResultLine/SpecialChars.log",
    "content": "=== RUN   TestIsResultLine/SpecialChars\n    --- PASS: TestIsResultLine/SpecialChars (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/panic_example_expected/TestIsResultLine/WhenFailed.log",
    "content": "=== RUN   TestIsResultLine/WhenFailed\n    --- PASS: TestIsResultLine/WhenFailed (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/panic_example_expected/TestIsResultLine.log",
    "content": "=== RUN   TestIsResultLine\n=== PAUSE TestIsResultLine\n=== CONT  TestIsResultLine\n--- PASS: TestIsResultLine (0.00s)\n    --- PASS: TestIsResultLine/BaseCase (0.00s)\n    --- PASS: TestIsResultLine/Indented (0.00s)\n    --- PASS: TestIsResultLine/SpecialChars (0.00s)\n    --- PASS: TestIsResultLine/WhenFailed (0.00s)\n    --- PASS: TestIsResultLine/NonResultLine (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/panic_example_expected/TestIsStatusLine/BaseCase.log",
    "content": "=== RUN   TestIsStatusLine/BaseCase\n    --- PASS: TestIsStatusLine/BaseCase (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/panic_example_expected/TestIsStatusLine/Indented.log",
    "content": "=== RUN   TestIsStatusLine/Indented\n    --- PASS: TestIsStatusLine/Indented (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/panic_example_expected/TestIsStatusLine/NonStatusLine.log",
    "content": "=== RUN   TestIsStatusLine/NonStatusLine\n    --- PASS: TestIsStatusLine/NonStatusLine (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/panic_example_expected/TestIsStatusLine/SpecialChars.log",
    "content": "=== RUN   TestIsStatusLine/SpecialChars\n    --- PASS: TestIsStatusLine/SpecialChars (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/panic_example_expected/TestIsStatusLine/WhenCont.log",
    "content": "=== RUN   TestIsStatusLine/WhenCont\n    --- PASS: TestIsStatusLine/WhenCont (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/panic_example_expected/TestIsStatusLine/WhenPaused.log",
    "content": "=== RUN   TestIsStatusLine/WhenPaused\n    --- PASS: TestIsStatusLine/WhenPaused (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/panic_example_expected/TestIsStatusLine.log",
    "content": "=== RUN   TestIsStatusLine\n=== PAUSE TestIsStatusLine\n=== CONT  TestIsStatusLine\n--- PASS: TestIsStatusLine (0.00s)\n    --- PASS: TestIsStatusLine/BaseCase (0.00s)\n    --- PASS: TestIsStatusLine/Indented (0.00s)\n    --- PASS: TestIsStatusLine/SpecialChars (0.00s)\n    --- PASS: TestIsStatusLine/WhenPaused (0.00s)\n    --- PASS: TestIsStatusLine/WhenCont (0.00s)\n    --- PASS: TestIsStatusLine/NonStatusLine (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/panic_example_expected/TestIsSummaryLine/BaseCase.log",
    "content": "=== RUN   TestIsSummaryLine/BaseCase\n    --- PASS: TestIsSummaryLine/BaseCase (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/panic_example_expected/TestIsSummaryLine/NotSummary.log",
    "content": "=== RUN   TestIsSummaryLine/NotSummary\n    --- PASS: TestIsSummaryLine/NotSummary (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/panic_example_expected/TestIsSummaryLine.log",
    "content": "=== RUN   TestIsSummaryLine\n=== PAUSE TestIsSummaryLine\n=== CONT  TestIsSummaryLine\n--- PASS: TestIsSummaryLine (0.00s)\n    --- PASS: TestIsSummaryLine/BaseCase (0.00s)\n    --- PASS: TestIsSummaryLine/NotSummary (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/panic_example_expected/TestLogCollectorCreatesAndWritesToFile.log",
    "content": "=== RUN   TestLogCollectorCreatesAndWritesToFile\n=== PAUSE TestLogCollectorCreatesAndWritesToFile\n=== CONT  TestLogCollectorCreatesAndWritesToFile\nTestLogCollectorCreatesAndWritesToFile INFO 2018-10-20T13:03:19-07:00 Spawned log writer for test TestLogCollectorCreatesAndWritesToFile\n"
  },
  {
    "path": "modules/logger/parser/fixtures/panic_example_expected/TestPeek.log",
    "content": "=== RUN   TestPeek\n=== PAUSE TestPeek\n=== CONT  TestPeek\n--- PASS: TestPeek (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/panic_example_expected/TestPeekEmpty.log",
    "content": "=== RUN   TestPeekEmpty\n=== PAUSE TestPeekEmpty\n=== CONT  TestPeekEmpty\n--- PASS: TestPeekEmpty (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/panic_example_expected/TestRemoveDedentedTestResultMarkers.log",
    "content": "=== RUN   TestRemoveDedentedTestResultMarkers\n--- PASS: TestRemoveDedentedTestResultMarkers (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/panic_example_expected/TestRemoveDedentedTestResultMarkersAll.log",
    "content": "=== RUN   TestRemoveDedentedTestResultMarkersAll\n--- PASS: TestRemoveDedentedTestResultMarkersAll (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/panic_example_expected/TestRemoveDedentedTestResultMarkersEmpty.log",
    "content": "=== RUN   TestRemoveDedentedTestResultMarkersEmpty\n--- PASS: TestRemoveDedentedTestResultMarkersEmpty (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/panic_example_expected/TestStackPop.log",
    "content": "=== RUN   TestStackPop\n=== PAUSE TestStackPop\n=== CONT  TestStackPop\n--- PASS: TestStackPop (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/panic_example_expected/TestStackPopEmpty.log",
    "content": "=== RUN   TestStackPopEmpty\n=== PAUSE TestStackPopEmpty\n=== CONT  TestStackPopEmpty\n--- PASS: TestStackPopEmpty (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/panic_example_expected/TestStackPush.log",
    "content": "=== RUN   TestStackPush\n=== PAUSE TestStackPush\n=== CONT  TestStackPush\n--- PASS: TestStackPush (0.00s)\n"
  },
  {
    "path": "modules/logger/parser/fixtures/panic_example_expected/report.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<testsuites>\n\t<testsuite tests=\"52\" failures=\"4\" time=\"0.020\" name=\"github.com/gruntwork-io/terratest/modules/logger/parser\">\n\t\t<properties>\n\t\t\t<property name=\"go.version\" value=\"go1.21.1\"></property>\n\t\t</properties>\n\t\t<testcase classname=\"parser\" name=\"TestStackPush\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestStackPop\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestStackPopEmpty\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestPeek\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestPeekEmpty\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestIsEmpty\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestRemoveDedentedTestResultMarkers\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestRemoveDedentedTestResultMarkersEmpty\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestRemoveDedentedTestResultMarkersAll\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestGetIndent\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestGetTestNameFromResultLine\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestIsResultLine\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestGetTestNameFromStatusLine\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestIsStatusLine\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestIsSummaryLine\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestIsPanicLine\" time=\"0.000\">\n\t\t\t<failure message=\"Failed\" type=\"\"></failure>\n\t\t</testcase>\n\t\t<testcase classname=\"parser\" name=\"TestEnsureDirectoryExistsCreatesDirectory\" time=\"0.000\">\n\t\t\t<failure message=\"Failed\" type=\"\"></failure>\n\t\t</testcase>\n\t\t<testcase classname=\"parser\" name=\"TestEnsureDirectoryExistsHandlesExistingDirectory\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestGetOrCreateChannelCreatesNewChannel\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestGetOrCreateChannelReturnsExistingChannel\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestLogCollectorCreatesAndWritesToFile\" time=\"0.000\">\n\t\t\t<failure message=\"Failed\" type=\"\"></failure>\n\t\t</testcase>\n\t\t<testcase classname=\"parser\" name=\"TestGetOrCreateChannelSpawnsLogCollectorOnCreate\" time=\"0.000\">\n\t\t\t<failure message=\"Failed\" type=\"\"></failure>\n\t\t</testcase>\n\t\t<testcase classname=\"parser\" name=\"TestCloseChannelsClosesAll\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestIsSummaryLine/BaseCase\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestGetIndent/BaseCase\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestIsStatusLine/BaseCase\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestIsSummaryLine/NotSummary\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestIsStatusLine/Indented\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestGetIndent/NoIndent\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestIsStatusLine/SpecialChars\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestGetTestNameFromStatusLine/BaseCase\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestGetIndent/EmptyString\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestIsStatusLine/WhenPaused\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestGetTestNameFromStatusLine/Indented\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestIsStatusLine/WhenCont\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestGetIndent/Tabs\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestGetTestNameFromStatusLine/SpecialChars\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestIsResultLine/BaseCase\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestIsStatusLine/NonStatusLine\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestGetIndent/MixTabSpace\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestGetTestNameFromStatusLine/WhenPaused\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestGetTestNameFromResultLine/BaseCase\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestGetTestNameFromStatusLine/WhenCont\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestGetTestNameFromResultLine/Indented\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestGetTestNameFromResultLine/SpecialChars\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestGetTestNameFromResultLine/WhenFailed\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestIsResultLine/Indented\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestIsResultLine/SpecialChars\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestIsResultLine/WhenFailed\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestIsResultLine/NonResultLine\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestIsPanicLine/BaseCase\" time=\"0.000\"></testcase>\n\t\t<testcase classname=\"parser\" name=\"TestIsPanicLine/NotPanic\" time=\"0.000\"></testcase>\n\t</testsuite>\n</testsuites>\n"
  },
  {
    "path": "modules/logger/parser/fixtures/panic_example_expected/summary.log",
    "content": "--- PASS: TestRemoveDedentedTestResultMarkers (0.00s)\n--- PASS: TestRemoveDedentedTestResultMarkersEmpty (0.00s)\n--- PASS: TestRemoveDedentedTestResultMarkersAll (0.00s)\n--- PASS: TestStackPush (0.00s)\n--- PASS: TestIsSummaryLine (0.00s)\n    --- PASS: TestIsSummaryLine/BaseCase (0.00s)\n    --- PASS: TestIsSummaryLine/NotSummary (0.00s)\n--- PASS: TestPeek (0.00s)\n--- PASS: TestIsStatusLine (0.00s)\n    --- PASS: TestIsStatusLine/BaseCase (0.00s)\n    --- PASS: TestIsStatusLine/Indented (0.00s)\n    --- PASS: TestIsStatusLine/SpecialChars (0.00s)\n    --- PASS: TestIsStatusLine/WhenPaused (0.00s)\n    --- PASS: TestIsStatusLine/WhenCont (0.00s)\n    --- PASS: TestIsStatusLine/NonStatusLine (0.00s)\n--- PASS: TestGetIndent (0.00s)\n    --- PASS: TestGetIndent/BaseCase (0.00s)\n    --- PASS: TestGetIndent/NoIndent (0.00s)\n    --- PASS: TestGetIndent/EmptyString (0.00s)\n    --- PASS: TestGetIndent/Tabs (0.00s)\n    --- PASS: TestGetIndent/MixTabSpace (0.00s)\n--- PASS: TestGetTestNameFromStatusLine (0.00s)\n    --- PASS: TestGetTestNameFromStatusLine/BaseCase (0.00s)\n    --- PASS: TestGetTestNameFromStatusLine/Indented (0.00s)\n    --- PASS: TestGetTestNameFromStatusLine/SpecialChars (0.00s)\n    --- PASS: TestGetTestNameFromStatusLine/WhenPaused (0.00s)\n    --- PASS: TestGetTestNameFromStatusLine/WhenCont (0.00s)\n--- PASS: TestIsEmpty (0.00s)\n--- PASS: TestPeekEmpty (0.00s)\n--- PASS: TestStackPopEmpty (0.00s)\n--- PASS: TestGetTestNameFromResultLine (0.00s)\n    --- PASS: TestGetTestNameFromResultLine/BaseCase (0.00s)\n    --- PASS: TestGetTestNameFromResultLine/Indented (0.00s)\n    --- PASS: TestGetTestNameFromResultLine/SpecialChars (0.00s)\n    --- PASS: TestGetTestNameFromResultLine/WhenFailed (0.00s)\n--- PASS: TestIsResultLine (0.00s)\n    --- PASS: TestIsResultLine/BaseCase (0.00s)\n    --- PASS: TestIsResultLine/Indented (0.00s)\n    --- PASS: TestIsResultLine/SpecialChars (0.00s)\n    --- PASS: TestIsResultLine/WhenFailed (0.00s)\n    --- PASS: TestIsResultLine/NonResultLine (0.00s)\n--- PASS: TestStackPop (0.00s)\n--- PASS: TestGetOrCreateChannelReturnsExistingChannel (0.00s)\n--- PASS: TestCloseChannelsClosesAll (0.00s)\n--- PASS: TestEnsureDirectoryExistsHandlesExistingDirectory (0.00s)\n--- PASS: TestGetOrCreateChannelCreatesNewChannel (0.00s)\n--- FAIL: TestIsPanicLine (0.00s)\n    --- PASS: TestIsPanicLine/BaseCase (0.00s)\n    --- PASS: TestIsPanicLine/NotPanic (0.00s)\npanic: error [recovered]\n\tpanic: error\n\ngoroutine 36 [running]:\ntesting.tRunner.func1(0xc0000c5300)\n\t/usr/local/Cellar/go/1.11/libexec/src/testing/testing.go:792 +0x387\npanic(0x1329720, 0x13fd400)\n\t/usr/local/Cellar/go/1.11/libexec/src/runtime/panic.go:513 +0x1b9\ngithub.com/gruntwork-io/terratest/modules/logger/parser.TestIsPanicLine(0xc0000c5300)\n\t/Users/yoriy/go/src/github.com/gruntwork-io/terratest/modules/logger/parser/parser_test.go:306 +0x1c4\ntesting.tRunner(0xc0000c5300, 0x13bb160)\n\t/usr/local/Cellar/go/1.11/libexec/src/testing/testing.go:827 +0xbf\ncreated by testing.(*T).Run\n\t/usr/local/Cellar/go/1.11/libexec/src/testing/testing.go:878 +0x353\nexit status 2\nFAIL\tgithub.com/gruntwork-io/terratest/modules/logger/parser\t0.020s\n"
  },
  {
    "path": "modules/logger/parser/helpers_for_test.go",
    "content": "package parser\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/sirupsen/logrus\"\n)\n\nfunc NewTestLogger(t *testing.T) *logrus.Logger {\n\tlogger := logrus.New()\n\tlogger.SetFormatter(&LogTestFormatter{TestName: t.Name()})\n\treturn logger\n}\n\ntype LogTestFormatter struct {\n\tTestName string\n}\n\nfunc (formatter *LogTestFormatter) Format(entry *logrus.Entry) ([]byte, error) {\n\tb := bytes.Buffer{}\n\toutStr := fmt.Sprintf(\n\t\t\"%s %s %s %s\\n\",\n\t\tformatter.TestName,\n\t\tstrings.ToUpper(entry.Level.String()),\n\t\tentry.Time.Format(time.RFC3339),\n\t\tentry.Message,\n\t)\n\tb.WriteString(outStr)\n\treturn b.Bytes(), nil\n}\n"
  },
  {
    "path": "modules/logger/parser/integration_test.go",
    "content": "package parser\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/files\"\n\t\"github.com/gruntwork-io/terratest/modules/shell\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc DirectoryEqual(t *testing.T, dirA string, dirB string) bool {\n\tdirAAbs, err := filepath.Abs(dirA)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdirBAbs, err := filepath.Abs(dirB)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\t// We use diff here instead of using something in go for simplicity of comparing directories and file contents\n\t// recursively\n\tcmd := shell.Command{\n\t\tCommand: \"diff\",\n\t\tArgs:    []string{\"-ar\", dirAAbs, dirBAbs},\n\t}\n\terr = shell.RunCommandE(t, cmd)\n\texitCode, err := shell.GetExitCodeForRunCommandError(err)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn exitCode == 0\n}\n\nfunc openFile(t *testing.T, filename string) *os.File {\n\tfile, err := os.Open(filename)\n\tif err != nil {\n\t\tt.Fatalf(\"Error opening file: %s\", err)\n\t}\n\treturn file\n}\n\nfunc testExample(t *testing.T, example string) {\n\texpected, output := path.Join(t.TempDir(), \"expected\"), path.Join(t.TempDir(), \"output\")\n\trequire.NoError(t, os.Mkdir(expected, 0755))\n\trequire.NoError(t, os.Mkdir(output, 0755))\n\n\t// prepare expected directory to diff against\n\texpectedOutputDirName := fmt.Sprintf(\"./fixtures/%s_example_expected\", example)\n\trequire.NoError(t, files.CopyFolderContents(expectedOutputDirName, expected))\n\tb, err := os.ReadFile(path.Join(expected, \"report.xml\"))\n\trequire.NoError(t, err)\n\tb = bytes.ReplaceAll(b, []byte(\"go1.21.1\"), []byte(runtime.Version())) // replace the harcoded go version of the fixture\n\trequire.NoError(t, os.WriteFile(path.Join(expected, \"report.xml\"), b, 644))\n\n\t// run the parser\n\tlogger := NewTestLogger(t)\n\tlogFileName := fmt.Sprintf(\"./fixtures/%s_example.log\", example)\n\tfile := openFile(t, logFileName)\n\tSpawnParsers(logger, file, output)\n\n\t// assert\n\tassert.True(t, DirectoryEqual(t, expected, output))\n}\n\nfunc TestIntegrationBasicExample(t *testing.T) {\n\tt.Parallel()\n\ttestExample(t, \"basic\")\n}\n\nfunc TestIntegrationFailingExample(t *testing.T) {\n\tt.Parallel()\n\ttestExample(t, \"failing\")\n}\n\nfunc TestIntegrationPanicExample(t *testing.T) {\n\tt.Parallel()\n\ttestExample(t, \"panic\")\n}\n\nfunc TestIntegrationNewGoExample(t *testing.T) {\n\tt.Parallel()\n\ttestExample(t, \"new_go_failing\")\n}\n"
  },
  {
    "path": "modules/logger/parser/parser.go",
    "content": "// Package logger/parser contains methods to parse and restructure log output from go testing and terratest\npackage parser\n\nimport (\n\t\"bufio\"\n\t\"io\"\n\t\"os\"\n\t\"regexp\"\n\t\"strings\"\n\t\"sync\"\n\n\tjunitparser \"github.com/jstemmer/go-junit-report/parser\"\n\t\"github.com/sirupsen/logrus\"\n)\n\n// SpawnParsers will spawn the log parser and junit report parsers off of a single reader.\nfunc SpawnParsers(logger *logrus.Logger, reader io.Reader, outputDir string) {\n\tforkedReader, forkedWriter := io.Pipe()\n\tteedReader := io.TeeReader(reader, forkedWriter)\n\tvar waitForParsers sync.WaitGroup\n\twaitForParsers.Add(2)\n\tgo func() {\n\t\t// close pipe writer, because this section drains the tee reader indicating reader is done draining\n\t\tdefer forkedWriter.Close()\n\t\tdefer waitForParsers.Done()\n\t\tparseAndStoreTestOutput(logger, teedReader, outputDir)\n\t}()\n\tgo func() {\n\t\tdefer waitForParsers.Done()\n\t\treport, err := junitparser.Parse(forkedReader, \"\")\n\t\tif err == nil {\n\t\t\tstoreJunitReport(logger, outputDir, report)\n\t\t} else {\n\t\t\tlogger.Errorf(\"Error parsing test output into junit report: %s\", err)\n\t\t}\n\t}()\n\twaitForParsers.Wait()\n}\n\n// RegEx for parsing test status lines. Pulled from jstemmer/go-junit-report\nvar (\n\tregexResult  = regexp.MustCompile(`--- (PASS|FAIL|SKIP): (.+) \\((\\d+\\.\\d+)(?: ?seconds|s)\\)`)\n\tregexStatus  = regexp.MustCompile(`=== (RUN|PAUSE|CONT)\\s+(.+)`)\n\tregexSummary = regexp.MustCompile(`(^FAIL$)|(^(ok|FAIL)\\s+([^ ]+)\\s+(?:(\\d+\\.\\d+)s|\\(cached\\)|(\\[\\w+ failed]))(?:\\s+coverage:\\s+(\\d+\\.\\d+)%\\sof\\sstatements(?:\\sin\\s.+)?)?$)`)\n\tregexPanic   = regexp.MustCompile(`^panic:`)\n)\n\n// getIndent takes a line and returns the indent string\n// Example:\n//\n//\tin:  \"    --- FAIL: TestSnafu\"\n//\tout: \"    \"\nfunc getIndent(data string) string {\n\tre := regexp.MustCompile(`^\\s+`)\n\tindent := re.FindString(data)\n\treturn indent\n}\n\n// getTestNameFromResultLine takes a go testing result line and extracts out the test name\n// Example:\n//\n//\tin:  --- FAIL: TestSnafu\n//\tout: TestSnafu\nfunc getTestNameFromResultLine(text string) string {\n\tm := regexResult.FindStringSubmatch(text)\n\treturn m[2]\n}\n\n// isResultLine checks if a line of text matches a test result (begins with \"--- FAIL\" or \"--- PASS\")\nfunc isResultLine(text string) bool {\n\treturn regexResult.MatchString(text)\n}\n\n// getTestNameFromStatusLine takes a go testing status line and extracts out the test name\n// Example:\n//\n//\tin:  === RUN  TestSnafu\n//\tout: TestSnafu\nfunc getTestNameFromStatusLine(text string) string {\n\tm := regexStatus.FindStringSubmatch(text)\n\treturn m[2]\n}\n\n// isStatusLine checks if a line of text matches a test status\nfunc isStatusLine(text string) bool {\n\treturn regexStatus.MatchString(text)\n}\n\n// isSummaryLine checks if a line of text matches the test summary\nfunc isSummaryLine(text string) bool {\n\treturn regexSummary.MatchString(text)\n}\n\n// isPanicLine checks if a line of text matches a panic\nfunc isPanicLine(text string) bool {\n\treturn regexPanic.MatchString(text)\n}\n\n// parseAndStoreTestOutput will take test log entries from terratest and aggregate the output by test. Takes advantage\n// of the fact that terratest logs are prefixed by the test name. This will store the broken out logs into files under\n// the outputDir, named by test name.\n// Additionally will take test result lines and collect them under a summary log file named `summary.log`.\n// See the `fixtures` directory for some examples.\nfunc parseAndStoreTestOutput(\n\tlogger *logrus.Logger,\n\tread io.Reader,\n\toutputDir string,\n) {\n\tlogWriter := LogWriter{\n\t\tlookup:    make(map[string]*os.File),\n\t\toutputDir: outputDir,\n\t}\n\tdefer logWriter.closeFiles(logger)\n\n\t// Track some state that persists across lines\n\ttestResultMarkers := TestResultMarkerStack{}\n\tpreviousTestName := \"\"\n\n\tvar err error\n\treader := bufio.NewReader(read)\n\tfor {\n\t\tvar data string\n\t\tdata, err = reader.ReadString('\\n')\n\t\tif len(data) == 0 && err == io.EOF {\n\t\t\tbreak\n\t\t}\n\n\t\tdata = strings.TrimSuffix(data, \"\\n\")\n\n\t\t// separate block so that we do not overwrite the err variable that we need afterwards to check if we're done\n\t\t{\n\t\t\tindentLevel := len(getIndent(data))\n\t\t\tisIndented := indentLevel > 0\n\n\t\t\t// Garbage collection of test result markers. Primary purpose is to detect when we dedent out, which can only be\n\t\t\t// detected when we reach a dedented line.\n\t\t\ttestResultMarkers = testResultMarkers.removeDedentedTestResultMarkers(indentLevel)\n\n\t\t\t// Handle each possible category of test lines\n\t\t\tswitch {\n\t\t\tcase isSummaryLine(data):\n\t\t\t\tlogWriter.writeLog(logger, \"summary\", data)\n\n\t\t\tcase isStatusLine(data):\n\t\t\t\ttestName := getTestNameFromStatusLine(data)\n\t\t\t\tpreviousTestName = testName\n\t\t\t\tlogWriter.writeLog(logger, testName, data)\n\n\t\t\tcase strings.HasPrefix(data, \"Test\"):\n\t\t\t\t// Heuristic: `go test` will only execute test functions named `Test.*`, so we assume any line prefixed\n\t\t\t\t// with `Test` is a test output for a named test. Also assume that test output will be space delimeted and\n\t\t\t\t// test names can't contain spaces (because they are function names).\n\t\t\t\t// This must be modified when `logger.DoLog` changes.\n\t\t\t\tvals := strings.Split(data, \" \")\n\t\t\t\ttestName := vals[0]\n\t\t\t\tpreviousTestName = testName\n\t\t\t\tlogWriter.writeLog(logger, testName, data)\n\n\t\t\tcase isIndented && isResultLine(data):\n\t\t\t\t// In a nested test result block, so collect the line into all the test results we have seen so far.\n\t\t\t\tfor _, marker := range testResultMarkers {\n\t\t\t\t\tlogWriter.writeLog(logger, marker.TestName, data)\n\t\t\t\t}\n\n\t\t\tcase isPanicLine(data):\n\t\t\t\t// When panic, we want all subsequent nonstandard test lines to roll up to the summary\n\t\t\t\tpreviousTestName = \"summary\"\n\t\t\t\tlogWriter.writeLog(logger, \"summary\", data)\n\n\t\t\tcase isResultLine(data):\n\t\t\t\t// We ignore result lines, because that is handled specially below.\n\n\t\t\tcase previousTestName != \"\":\n\t\t\t\t// Base case: roll up to the previous test line, if it exists.\n\t\t\t\t// Handles case where terratest log has entries with newlines in them.\n\t\t\t\tlogWriter.writeLog(logger, previousTestName, data)\n\n\t\t\tdefault:\n\t\t\t\tlogger.Warnf(\"Found test line that does not match known cases: %s\", data)\n\t\t\t}\n\n\t\t\t// This has to happen separately from main if block to handle the special case of nested tests (e.g table driven\n\t\t\t// tests). For those result lines, we want it to roll up to the parent test, so we need to run the handler in\n\t\t\t// the `isIndented` section. But for both root and indented result lines, we want to execute the following code,\n\t\t\t// hence this special block.\n\t\t\tif isResultLine(data) {\n\t\t\t\ttestName := getTestNameFromResultLine(data)\n\t\t\t\tlogWriter.writeLog(logger, testName, data)\n\t\t\t\tlogWriter.writeLog(logger, \"summary\", data)\n\n\t\t\t\tmarker := TestResultMarker{\n\t\t\t\t\tTestName:    testName,\n\t\t\t\t\tIndentLevel: indentLevel,\n\t\t\t\t}\n\t\t\t\ttestResultMarkers = testResultMarkers.push(marker)\n\t\t\t}\n\t\t}\n\n\t\tif err != nil {\n\t\t\tbreak\n\t\t}\n\t}\n\tif err != io.EOF {\n\t\tlogger.Fatalf(\"Error reading from Reader: %s\", err)\n\t}\n}\n"
  },
  {
    "path": "modules/logger/parser/parser_test.go",
    "content": "package parser\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestGetIndent(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tname string\n\t\tin   string\n\t\tout  string\n\t}{\n\t\t{\n\t\t\t\"BaseCase\",\n\t\t\t\"    --- FAIL: TestSnafu\",\n\t\t\t\"    \",\n\t\t},\n\t\t{\n\t\t\t\"NoIndent\",\n\t\t\t\"--- FAIL: TestSnafu\",\n\t\t\t\"\",\n\t\t},\n\t\t{\n\t\t\t\"EmptyString\",\n\t\t\t\"\",\n\t\t\t\"\",\n\t\t},\n\t\t{\n\t\t\t\"Tabs\",\n\t\t\t\"\\t\\t---FAIL: TestSnafu\",\n\t\t\t\"\\t\\t\",\n\t\t},\n\t\t{\n\t\t\t\"MixTabSpace\",\n\t\t\t\"\\t    ---FAIL: TestSnafu\",\n\t\t\t\"\\t    \",\n\t\t},\n\t}\n\n\tfor _, testCase := range testCases {\n\t\ttestCase := testCase\n\t\tt.Run(testCase.name, func(t *testing.T) {\n\t\t\tassert.Equal(\n\t\t\t\tt,\n\t\t\t\tgetIndent(testCase.in),\n\t\t\t\ttestCase.out,\n\t\t\t)\n\t\t})\n\t}\n}\n\nfunc TestGetTestNameFromResultLine(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tname string\n\t\tin   string\n\t\tout  string\n\t}{\n\t\t{\n\t\t\t\"BaseCase\",\n\t\t\t\"--- PASS: TestGetTestNameFromResultLine (0.00s)\",\n\t\t\t\"TestGetTestNameFromResultLine\",\n\t\t},\n\t\t{\n\t\t\t\"Indented\",\n\t\t\t\"    --- PASS: TestGetTestNameFromResultLine/Indented (0.00s)\",\n\t\t\t\"TestGetTestNameFromResultLine/Indented\",\n\t\t},\n\t\t{\n\t\t\t\"SpecialChars\",\n\t\t\t\"    --- PASS: TestGetTestNameFromResultLine/SpecialChars---_FAIL (0.00s)\",\n\t\t\t\"TestGetTestNameFromResultLine/SpecialChars---_FAIL\",\n\t\t},\n\t\t{\n\t\t\t\"WhenFailed\",\n\t\t\t\"--- FAIL: TestGetTestNameFromResultLine (0.00s)\",\n\t\t\t\"TestGetTestNameFromResultLine\",\n\t\t},\n\t}\n\n\tfor _, testCase := range testCases {\n\t\ttestCase := testCase\n\t\tt.Run(testCase.name, func(t *testing.T) {\n\t\t\tassert.Equal(\n\t\t\t\tt,\n\t\t\t\tgetTestNameFromResultLine(testCase.in),\n\t\t\t\ttestCase.out,\n\t\t\t)\n\t\t})\n\t}\n}\n\nfunc TestIsResultLine(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tname string\n\t\tin   string\n\t\tout  bool\n\t}{\n\t\t{\n\t\t\t\"BaseCase\",\n\t\t\t\"--- PASS: TestIsResultLine (0.00s)\",\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"Indented\",\n\t\t\t\"    --- PASS: TestIsResultLine/Indented (0.00s)\",\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"SpecialChars\",\n\t\t\t\"    --- PASS: TestIsResultLine/SpecialChars---_FAIL (0.00s)\",\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"WhenFailed\",\n\t\t\t\"--- FAIL: TestIsResultLine (0.00s)\",\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"NonResultLine\",\n\t\t\t\"=== RUN TestIsResultLine\",\n\t\t\tfalse,\n\t\t},\n\t}\n\n\tfor _, testCase := range testCases {\n\t\ttestCase := testCase\n\t\tt.Run(testCase.name, func(t *testing.T) {\n\t\t\tassert.Equal(\n\t\t\t\tt,\n\t\t\t\tisResultLine(testCase.in),\n\t\t\t\ttestCase.out,\n\t\t\t)\n\t\t})\n\t}\n}\n\nfunc TestGetTestNameFromStatusLine(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tname string\n\t\tin   string\n\t\tout  string\n\t}{\n\t\t{\n\t\t\t\"BaseCase\",\n\t\t\t\"=== RUN   TestGetTestNameFromStatusLine\",\n\t\t\t\"TestGetTestNameFromStatusLine\",\n\t\t},\n\t\t{\n\t\t\t\"Indented\",\n\t\t\t\"    === RUN   TestGetTestNameFromStatusLine/Indented\",\n\t\t\t\"TestGetTestNameFromStatusLine/Indented\",\n\t\t},\n\t\t{\n\t\t\t\"SpecialChars\",\n\t\t\t\"=== RUN   TestGetTestNameFromStatusLine/SpecialChars---_FAIL\",\n\t\t\t\"TestGetTestNameFromStatusLine/SpecialChars---_FAIL\",\n\t\t},\n\t\t{\n\t\t\t\"WhenPaused\",\n\t\t\t\"=== PAUSE TestGetTestNameFromStatusLine\",\n\t\t\t\"TestGetTestNameFromStatusLine\",\n\t\t},\n\t\t{\n\t\t\t\"WhenCont\",\n\t\t\t\"=== CONT  TestGetTestNameFromStatusLine\",\n\t\t\t\"TestGetTestNameFromStatusLine\",\n\t\t},\n\t}\n\n\tfor _, testCase := range testCases {\n\t\ttestCase := testCase\n\t\tt.Run(testCase.name, func(t *testing.T) {\n\t\t\tassert.Equal(\n\t\t\t\tt,\n\t\t\t\tgetTestNameFromStatusLine(testCase.in),\n\t\t\t\ttestCase.out,\n\t\t\t)\n\t\t})\n\t}\n}\n\nfunc TestIsStatusLine(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tname string\n\t\tin   string\n\t\tout  bool\n\t}{\n\t\t{\n\t\t\t\"BaseCase\",\n\t\t\t\"=== RUN   TestGetTestNameFromStatusLine\",\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"Indented\",\n\t\t\t\"    === RUN   TestGetTestNameFromStatusLine/Indented\",\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"SpecialChars\",\n\t\t\t\"=== RUN   TestGetTestNameFromStatusLine/SpecialChars---_FAIL\",\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"WhenPaused\",\n\t\t\t\"=== PAUSE TestGetTestNameFromStatusLine\",\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"WhenCont\",\n\t\t\t\"=== CONT  TestGetTestNameFromStatusLine\",\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"NonStatusLine\",\n\t\t\t\"--- FAIL: TestIsStatusLine\",\n\t\t\tfalse,\n\t\t},\n\t}\n\n\tfor _, testCase := range testCases {\n\t\ttestCase := testCase\n\t\tt.Run(testCase.name, func(t *testing.T) {\n\t\t\tassert.Equal(\n\t\t\t\tt,\n\t\t\t\tisStatusLine(testCase.in),\n\t\t\t\ttestCase.out,\n\t\t\t)\n\t\t})\n\t}\n}\n\nfunc TestIsSummaryLine(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tname string\n\t\tin   string\n\t\tout  bool\n\t}{\n\t\t{\n\t\t\t\"BaseCase\",\n\t\t\t\"ok  \tgithub.com/gruntwork-io/terratest/test\t812.034s\",\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"NotSummary\",\n\t\t\t\"--- FAIL: TestIsStatusLine\",\n\t\t\tfalse,\n\t\t},\n\t}\n\n\tfor _, testCase := range testCases {\n\t\ttestCase := testCase\n\t\tt.Run(testCase.name, func(t *testing.T) {\n\t\t\tassert.Equal(\n\t\t\t\tt,\n\t\t\t\tisSummaryLine(testCase.in),\n\t\t\t\ttestCase.out,\n\t\t\t)\n\t\t})\n\t}\n}\n\nfunc TestIsPanicLine(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tname string\n\t\tin   string\n\t\tout  bool\n\t}{\n\t\t{\n\t\t\t\"BaseCase\",\n\t\t\t\"panic: error [recovered]\",\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"NotPanic\",\n\t\t\t\"--- FAIL: TestIsStatusLine\",\n\t\t\tfalse,\n\t\t},\n\t}\n\n\tfor _, testCase := range testCases {\n\t\ttestCase := testCase\n\t\tt.Run(testCase.name, func(t *testing.T) {\n\t\t\tassert.Equal(\n\t\t\t\tt,\n\t\t\t\tisPanicLine(testCase.in),\n\t\t\t\ttestCase.out,\n\t\t\t)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "modules/logger/parser/store.go",
    "content": "// Package logger/parser contains methods to parse and restructure log output from go testing and terratest\npackage parser\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/gruntwork-io/go-commons/errors\"\n\t\"github.com/gruntwork-io/go-commons/files\"\n\tjunitformatter \"github.com/jstemmer/go-junit-report/formatter\"\n\tjunitparser \"github.com/jstemmer/go-junit-report/parser\"\n\t\"github.com/sirupsen/logrus\"\n)\n\ntype LogWriter struct {\n\t// Represents an open file to a log corresponding to a test (key = test name)\n\tlookup    map[string]*os.File\n\toutputDir string\n}\n\n// LogWriter.getOrCreateFile will get the corresponding file to a log for the provided test name, or create a new file.\nfunc (logWriter LogWriter) getOrCreateFile(logger *logrus.Logger, testName string) (*os.File, error) {\n\tfile, hasKey := logWriter.lookup[testName]\n\tif hasKey {\n\t\treturn file, nil\n\t}\n\n\tfilename := filepath.Join(logWriter.outputDir, testName+\".log\")\n\tfile, err := createLogFile(logger, filename)\n\tif err != nil {\n\t\treturn nil, errors.WithStackTrace(err)\n\t}\n\tlogWriter.lookup[testName] = file\n\treturn file, nil\n}\n\n// LogWriter.closeChannels closes all the channels in the lookup dictionary\nfunc (logWriter LogWriter) closeFiles(logger *logrus.Logger) {\n\tlogger.Infof(\"Closing all the files in log writer\")\n\tfor testName, file := range logWriter.lookup {\n\t\terr := file.Close()\n\t\tif err != nil {\n\t\t\tlogger.Errorf(\"Error closing log file for test %s: %s\", testName, err)\n\t\t}\n\t}\n}\n\n// writeLog will write the provided text to the corresponding log file for the provided test.\nfunc (logWriter LogWriter) writeLog(logger *logrus.Logger, testName string, text string) error {\n\tfile, err := logWriter.getOrCreateFile(logger, testName)\n\tif err != nil {\n\t\tlogger.Errorf(\"Error retrieving log for test: %s\", testName)\n\t\treturn errors.WithStackTrace(err)\n\t}\n\t_, err = file.WriteString(text + \"\\n\")\n\tif err != nil {\n\t\tlogger.Errorf(\"Error (%s) writing log entry: %s\", err, text)\n\t\treturn errors.WithStackTrace(err)\n\t}\n\tfile.Sync()\n\treturn nil\n}\n\n// createLogFile will create and return the open file handle for the file at provided filename, creating all directories\n// in the process.\nfunc createLogFile(logger *logrus.Logger, filename string) (*os.File, error) {\n\t// We extract and create the directory for interpolated filename, to handle nested tests where testname contains '/'\n\tdirName := filepath.Dir(filename)\n\terr := ensureDirectoryExists(logger, dirName)\n\tif err != nil {\n\t\treturn nil, errors.WithStackTrace(err)\n\t}\n\tfile, err := os.Create(filename)\n\tif err != nil {\n\t\treturn nil, errors.WithStackTrace(err)\n\t}\n\treturn file, nil\n}\n\n// ensureDirectoryExists will only attempt to create the directory if it does not exist\nfunc ensureDirectoryExists(logger *logrus.Logger, dirName string) error {\n\tif files.IsDir(dirName) {\n\t\tlogger.Infof(\"Directory %s already exists\", dirName)\n\t\treturn nil\n\t}\n\tlogger.Infof(\"Creating directory %s\", dirName)\n\terr := os.MkdirAll(dirName, os.ModePerm)\n\tif err != nil {\n\t\tlogger.Errorf(\"Error making directory %s: %s\", dirName, err)\n\t\treturn errors.WithStackTrace(err)\n\t}\n\treturn nil\n}\n\n// storeJunitReport takes a parsed Junit report and stores it as report.xml in the output directory\nfunc storeJunitReport(logger *logrus.Logger, outputDir string, report *junitparser.Report) {\n\tensureDirectoryExists(logger, outputDir)\n\tfilename := filepath.Join(outputDir, \"report.xml\")\n\tf, err := os.Create(filename)\n\tif err != nil {\n\t\tlogger.Errorf(\"Error making file %s for junit report\", filename)\n\t\treturn\n\t}\n\tdefer f.Close()\n\n\terr = junitformatter.JUnitReportXML(report, false, \"\", f)\n\tif err != nil {\n\t\tlogger.Errorf(\"Error formatting junit xml report: %s\", err)\n\t\treturn\n\t}\n}\n"
  },
  {
    "path": "modules/logger/parser/store_test.go",
    "content": "package parser\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/go-commons/files\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc createLogWriter(t *testing.T) LogWriter {\n\tlogWriter := LogWriter{\n\t\tlookup:    make(map[string]*os.File),\n\t\toutputDir: t.TempDir(),\n\t}\n\treturn logWriter\n}\n\nfunc TestEnsureDirectoryExistsCreatesDirectory(t *testing.T) {\n\tt.Parallel()\n\n\tdir := t.TempDir()\n\n\tlogger := NewTestLogger(t)\n\ttmpd := filepath.Join(dir, \"tmpdir\")\n\tassert.False(t, files.IsDir(tmpd))\n\tensureDirectoryExists(logger, tmpd)\n\tassert.True(t, files.IsDir(tmpd))\n}\n\nfunc TestEnsureDirectoryExistsHandlesExistingDirectory(t *testing.T) {\n\tt.Parallel()\n\n\tdir := t.TempDir()\n\n\tlogger := NewTestLogger(t)\n\tassert.True(t, files.IsDir(dir))\n\tensureDirectoryExists(logger, dir)\n\tassert.True(t, files.IsDir(dir))\n}\n\nfunc TestGetOrCreateFileCreatesNewFile(t *testing.T) {\n\tt.Parallel()\n\n\tlogWriter := createLogWriter(t)\n\n\tlogger := NewTestLogger(t)\n\ttestFileName := filepath.Join(logWriter.outputDir, t.Name()+\".log\")\n\tassert.False(t, files.FileExists(testFileName))\n\tfile, err := logWriter.getOrCreateFile(logger, t.Name())\n\tdefer file.Close()\n\tassert.Nil(t, err)\n\tassert.NotNil(t, file)\n\tassert.True(t, files.FileExists(testFileName))\n}\n\nfunc TestGetOrCreateFileCreatesNewFileIfTestNameHasDir(t *testing.T) {\n\tt.Parallel()\n\n\tlogWriter := createLogWriter(t)\n\n\tlogger := NewTestLogger(t)\n\tdirName := filepath.Join(logWriter.outputDir, \"TestMain\")\n\ttestFileName := filepath.Join(dirName, t.Name()+\".log\")\n\tassert.False(t, files.IsDir(dirName))\n\tassert.False(t, files.FileExists(testFileName))\n\tfile, err := logWriter.getOrCreateFile(logger, filepath.Join(\"TestMain\", t.Name()))\n\tdefer file.Close()\n\tassert.Nil(t, err)\n\tassert.NotNil(t, file)\n\tassert.True(t, files.IsDir(dirName))\n\tassert.True(t, files.FileExists(testFileName))\n}\n\nfunc TestGetOrCreateChannelReturnsExistingFileHandle(t *testing.T) {\n\tt.Parallel()\n\n\tlogWriter := createLogWriter(t)\n\n\ttestName := t.Name()\n\tlogger := NewTestLogger(t)\n\ttestFileName := filepath.Join(logWriter.outputDir, t.Name())\n\tfile, err := os.Create(testFileName)\n\tif err != nil {\n\t\tt.Fatalf(\"error creating test file %s\", testFileName)\n\t}\n\tdefer file.Close()\n\n\tlogWriter.lookup[testName] = file\n\tlookupFile, err := logWriter.getOrCreateFile(logger, testName)\n\tassert.Nil(t, err)\n\tassert.Equal(t, lookupFile, file)\n}\n\nfunc TestCloseFilesClosesAll(t *testing.T) {\n\tt.Parallel()\n\n\tlogWriter := createLogWriter(t)\n\n\tlogger := NewTestLogger(t)\n\ttestName := t.Name()\n\ttestFileName := filepath.Join(logWriter.outputDir, testName)\n\ttestFile, err := os.Create(testFileName)\n\tif err != nil {\n\t\tt.Fatalf(\"error creating test file %s\", testFileName)\n\t}\n\talternativeTestName := t.Name() + \"Alternative\"\n\talternativeTestFileName := filepath.Join(logWriter.outputDir, alternativeTestName)\n\talternativeTestFile, err := os.Create(alternativeTestFileName)\n\tif err != nil {\n\t\tt.Fatalf(\"error creating test file %s\", alternativeTestFileName)\n\t}\n\tlogWriter.lookup[testName] = testFile\n\tlogWriter.lookup[alternativeTestName] = alternativeTestFile\n\n\tlogWriter.closeFiles(logger)\n\terr = testFile.Close()\n\tassert.Contains(t, err.Error(), os.ErrClosed.Error())\n\terr = alternativeTestFile.Close()\n\tassert.Contains(t, err.Error(), os.ErrClosed.Error())\n}\n\nfunc TestWriteLogWritesToCorrectLogFile(t *testing.T) {\n\tt.Parallel()\n\n\tlogWriter := createLogWriter(t)\n\n\tlogger := NewTestLogger(t)\n\ttestName := t.Name()\n\ttestFileName := filepath.Join(logWriter.outputDir, testName)\n\ttestFile, err := os.Create(testFileName)\n\tif err != nil {\n\t\tt.Fatalf(\"error creating test file %s\", testFileName)\n\t}\n\tdefer testFile.Close()\n\talternativeTestName := t.Name() + \"Alternative\"\n\talternativeTestFileName := filepath.Join(logWriter.outputDir, alternativeTestName)\n\talternativeTestFile, err := os.Create(alternativeTestFileName)\n\tif err != nil {\n\t\tt.Fatalf(\"error creating test file %s\", alternativeTestFileName)\n\t}\n\tdefer alternativeTestFile.Close()\n\tlogWriter.lookup[testName] = testFile\n\tlogWriter.lookup[alternativeTestName] = alternativeTestFile\n\n\trandomString := random.UniqueId()\n\terr = logWriter.writeLog(logger, testName, randomString)\n\tassert.Nil(t, err)\n\talternativeRandomString := random.UniqueId()\n\terr = logWriter.writeLog(logger, alternativeTestName, alternativeRandomString)\n\tassert.Nil(t, err)\n\n\tbuf, err := os.ReadFile(testFileName)\n\tassert.Nil(t, err)\n\tassert.Equal(t, string(buf), randomString+\"\\n\")\n\tbuf, err = os.ReadFile(alternativeTestFileName)\n\tassert.Nil(t, err)\n\tassert.Equal(t, string(buf), alternativeRandomString+\"\\n\")\n}\n\nfunc TestWriteLogCreatesLogFileIfNotExists(t *testing.T) {\n\tt.Parallel()\n\n\tlogWriter := createLogWriter(t)\n\n\tlogger := NewTestLogger(t)\n\ttestName := t.Name()\n\ttestFileName := filepath.Join(logWriter.outputDir, testName+\".log\")\n\n\trandomString := random.UniqueId()\n\terr := logWriter.writeLog(logger, testName, randomString)\n\tassert.Nil(t, err)\n\n\tassert.True(t, files.FileExists(testFileName))\n\tbuf, err := os.ReadFile(testFileName)\n\tassert.Nil(t, err)\n\tassert.Equal(t, string(buf), randomString+\"\\n\")\n}\n"
  },
  {
    "path": "modules/oci/compute.go",
    "content": "package oci\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"sort\"\n\n\t\"github.com/gruntwork-io/terratest/modules/logger\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/oracle/oci-go-sdk/common\"\n\t\"github.com/oracle/oci-go-sdk/core\"\n)\n\n// DeleteImage deletes a custom image with given OCID.\nfunc DeleteImage(t testing.TestingT, ocid string) {\n\terr := DeleteImageE(t, ocid)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n\n// DeleteImageE deletes a custom image with given OCID.\nfunc DeleteImageE(t testing.TestingT, ocid string) error {\n\tlogger.Default.Logf(t, \"Deleting image with OCID %s\", ocid)\n\n\tconfigProvider := common.DefaultConfigProvider()\n\tclient, err := core.NewComputeClientWithConfigurationProvider(configProvider)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\trequest := core.DeleteImageRequest{ImageId: &ocid}\n\t_, err = client.DeleteImage(context.Background(), request)\n\treturn err\n}\n\n// GetMostRecentImageID gets the OCID of the most recent image in the given compartment that has the given OS name and version.\nfunc GetMostRecentImageID(t testing.TestingT, compartmentID string, osName string, osVersion string) string {\n\tocid, err := GetMostRecentImageIDE(t, compartmentID, osName, osVersion)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn ocid\n}\n\n// GetMostRecentImageIDE gets the OCID of the most recent image in the given compartment that has the given OS name and version.\nfunc GetMostRecentImageIDE(t testing.TestingT, compartmentID string, osName string, osVersion string) (string, error) {\n\tconfigProvider := common.DefaultConfigProvider()\n\tclient, err := core.NewComputeClientWithConfigurationProvider(configProvider)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\trequest := core.ListImagesRequest{\n\t\tCompartmentId:          &compartmentID,\n\t\tOperatingSystem:        &osName,\n\t\tOperatingSystemVersion: &osVersion,\n\t}\n\tresponse, err := client.ListImages(context.Background(), request)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif len(response.Items) == 0 {\n\t\treturn \"\", fmt.Errorf(\"No %s %s images found in the %s compartment\", osName, osVersion, compartmentID)\n\t}\n\n\tmostRecentImage := mostRecentImage(response.Items)\n\treturn *mostRecentImage.Id, nil\n}\n\n// Image sorting code borrowed from: https://github.com/hashicorp/packer/blob/7f4112ba229309cfc0ebaa10ded2abdfaf1b22c8/builder/amazon/common/step_source_ami_info.go\ntype imageSort []core.Image\n\nfunc (a imageSort) Len() int      { return len(a) }\nfunc (a imageSort) Swap(i, j int) { a[i], a[j] = a[j], a[i] }\nfunc (a imageSort) Less(i, j int) bool {\n\tiTime := a[i].TimeCreated.Unix()\n\tjTime := a[j].TimeCreated.Unix()\n\treturn iTime < jTime\n}\n\n// mostRecentImage returns the most recent image out of a slice of images.\nfunc mostRecentImage(images []core.Image) core.Image {\n\tsortedImages := images\n\tsort.Sort(imageSort(sortedImages))\n\treturn sortedImages[len(sortedImages)-1]\n}\n"
  },
  {
    "path": "modules/oci/identity.go",
    "content": "package oci\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/gruntwork-io/terratest/modules/logger\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/oracle/oci-go-sdk/common\"\n\t\"github.com/oracle/oci-go-sdk/identity\"\n)\n\n// GetRandomAvailabilityDomain gets a randomly chosen availability domain for given compartment.\n// The returned value can be overridden by of the environment variable TF_VAR_availability_domain.\nfunc GetRandomAvailabilityDomain(t testing.TestingT, compartmentID string) string {\n\tad, err := GetRandomAvailabilityDomainE(t, compartmentID)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn ad\n}\n\n// GetRandomAvailabilityDomainE gets a randomly chosen availability domain for given compartment.\n// The returned value can be overridden by of the environment variable TF_VAR_availability_domain.\nfunc GetRandomAvailabilityDomainE(t testing.TestingT, compartmentID string) (string, error) {\n\tadFromEnvVar := os.Getenv(availabilityDomainEnvVar)\n\tif adFromEnvVar != \"\" {\n\t\tlogger.Default.Logf(t, \"Using availability domain %s from environment variable %s\", adFromEnvVar, availabilityDomainEnvVar)\n\t\treturn adFromEnvVar, nil\n\t}\n\n\tallADs, err := GetAllAvailabilityDomainsE(t, compartmentID)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tad := random.RandomString(allADs)\n\n\tlogger.Default.Logf(t, \"Using availability domain %s\", ad)\n\treturn ad, nil\n}\n\n// GetAllAvailabilityDomains gets the list of availability domains available in the given compartment.\nfunc GetAllAvailabilityDomains(t testing.TestingT, compartmentID string) []string {\n\tads, err := GetAllAvailabilityDomainsE(t, compartmentID)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn ads\n}\n\n// GetAllAvailabilityDomainsE gets the list of availability domains available in the given compartment.\nfunc GetAllAvailabilityDomainsE(t testing.TestingT, compartmentID string) ([]string, error) {\n\tconfigProvider := common.DefaultConfigProvider()\n\tclient, err := identity.NewIdentityClientWithConfigurationProvider(configProvider)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\trequest := identity.ListAvailabilityDomainsRequest{CompartmentId: &compartmentID}\n\tresponse, err := client.ListAvailabilityDomains(context.Background(), request)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif len(response.Items) == 0 {\n\t\treturn nil, fmt.Errorf(\"No availability domains found in the %s compartment\", compartmentID)\n\t}\n\n\treturn availabilityDomainsNames(response.Items), nil\n}\n\nfunc availabilityDomainsNames(ads []identity.AvailabilityDomain) []string {\n\tnames := []string{}\n\tfor _, ad := range ads {\n\t\tnames = append(names, *ad.Name)\n\t}\n\treturn names\n}\n"
  },
  {
    "path": "modules/oci/network.go",
    "content": "package oci\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/gruntwork-io/terratest/modules/logger\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/oracle/oci-go-sdk/common\"\n\t\"github.com/oracle/oci-go-sdk/core\"\n)\n\n// GetRandomSubnetID gets a randomly chosen subnet OCID in the given availability domain.\n// The returned value can be overridden by of the environment variable TF_VAR_subnet_ocid.\nfunc GetRandomSubnetID(t testing.TestingT, compartmentID string, availabilityDomain string) string {\n\tocid, err := GetRandomSubnetIDE(t, compartmentID, availabilityDomain)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn ocid\n}\n\n// GetRandomSubnetIDE gets a randomly chosen subnet OCID in the given availability domain.\n// The returned value can be overridden by of the environment variable TF_VAR_subnet_ocid.\nfunc GetRandomSubnetIDE(t testing.TestingT, compartmentID string, availabilityDomain string) (string, error) {\n\tconfigProvider := common.DefaultConfigProvider()\n\tclient, err := core.NewVirtualNetworkClientWithConfigurationProvider(configProvider)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tvcnIDs, err := GetAllVcnIDsE(t, compartmentID)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tallSubnetIDs := map[string][]string{}\n\tfor _, vcnID := range vcnIDs {\n\t\trequest := core.ListSubnetsRequest{\n\t\t\tCompartmentId: &compartmentID,\n\t\t\tVcnId:         &vcnID,\n\t\t}\n\t\tresponse, err := client.ListSubnets(context.Background(), request)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\tmapSubnetsByAvailabilityDomain(allSubnetIDs, response.Items)\n\t}\n\n\tsubnetID := random.RandomString(allSubnetIDs[availabilityDomain])\n\n\tlogger.Default.Logf(t, \"Using subnet with OCID %s\", subnetID)\n\treturn subnetID, nil\n}\n\n// GetAllVcnIDs gets the list of VCNs available in the given compartment.\nfunc GetAllVcnIDs(t testing.TestingT, compartmentID string) []string {\n\tvcnIDS, err := GetAllVcnIDsE(t, compartmentID)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn vcnIDS\n}\n\n// GetAllVcnIDsE gets the list of VCNs available in the given compartment.\nfunc GetAllVcnIDsE(t testing.TestingT, compartmentID string) ([]string, error) {\n\tconfigProvider := common.DefaultConfigProvider()\n\tclient, err := core.NewVirtualNetworkClientWithConfigurationProvider(configProvider)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\trequest := core.ListVcnsRequest{CompartmentId: &compartmentID}\n\tresponse, err := client.ListVcns(context.Background(), request)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif len(response.Items) == 0 {\n\t\treturn nil, fmt.Errorf(\"No VCNs found in the %s compartment\", compartmentID)\n\t}\n\n\treturn vcnsIDs(response.Items), nil\n}\n\nfunc mapSubnetsByAvailabilityDomain(allSubnets map[string][]string, subnets []core.Subnet) map[string][]string {\n\tfor _, subnet := range subnets {\n\t\tallSubnets[*subnet.AvailabilityDomain] = append(allSubnets[*subnet.AvailabilityDomain], *subnet.Id)\n\t}\n\treturn allSubnets\n}\n\nfunc vcnsIDs(vcns []core.Vcn) []string {\n\tids := []string{}\n\tfor _, vcn := range vcns {\n\t\tids = append(ids, *vcn.Id)\n\t}\n\treturn ids\n}\n"
  },
  {
    "path": "modules/oci/provider.go",
    "content": "package oci\n\nimport (\n\t\"os\"\n\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/oracle/oci-go-sdk/common\"\n)\n\n// You can set this environment variable to force Terratest to use a specific compartment.\nconst compartmentIDEnvVar = \"TF_VAR_compartment_ocid\"\n\n// You can set this environment variable to force Terratest to use a specific availability domain\n// rather than a random one. This is convenient when iterating locally.\nconst availabilityDomainEnvVar = \"TF_VAR_availability_domain\"\n\n// You can set this environment variable to force Terratest to use a specific subnet.\nconst subnetIDEnvVar = \"TF_VAR_subnet_ocid\"\n\n// You can set this environment variable to force Terratest to use a pass phrase.\nconst passPhraseEnvVar = \"TF_VAR_pass_phrase\"\n\n// GetRootComparmentID gets an OCID of the root compartment (a.k.a. tenancy OCID).\nfunc GetRootCompartmentID(t testing.TestingT) string {\n\ttenancyID, err := GetRootCompartmentIDE(t)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn tenancyID\n}\n\n// GetRootComparmentIDE gets an OCID of the root compartment (a.k.a. tenancy OCID).\nfunc GetRootCompartmentIDE(t testing.TestingT) (string, error) {\n\tconfigProvider := common.DefaultConfigProvider()\n\ttenancyID, err := configProvider.TenancyOCID()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn tenancyID, nil\n}\n\n// GetCompartmentIDFromEnvVar returns the compartment OCID for use with testing.\nfunc GetCompartmentIDFromEnvVar() string {\n\treturn os.Getenv(compartmentIDEnvVar)\n}\n\n// GetSubnetIDFromEnvVar returns the subnet OCID for use with testing.\nfunc GetSubnetIDFromEnvVar() string {\n\treturn os.Getenv(subnetIDEnvVar)\n}\n\n// GetPassPhraseFromEnvVar returns the pass phrase for use with testing.\nfunc GetPassPhraseFromEnvVar() string {\n\treturn os.Getenv(passPhraseEnvVar)\n}\n"
  },
  {
    "path": "modules/opa/download_policy.go",
    "content": "package opa\n\nimport (\n\t\"context\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"sync\"\n\n\tgetter \"github.com/hashicorp/go-getter/v2\"\n\n\t\"github.com/gruntwork-io/terratest/modules/logger\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n)\n\nvar (\n\t// A map that maps the go-getter base URL to the temporary directory where it is downloaded.\n\tpolicyDirCache sync.Map\n)\n\n// DownloadPolicyE takes in a rule path written in go-getter syntax and downloads it to a temporary directory so that it\n// can be passed to opa. The temporary directory that is used is cached based on the go-getter base path, and reused\n// across calls.\n// For example, if you call DownloadPolicyE with the go-getter URL multiple times:\n//\n//\tgit::https://github.com/gruntwork-io/terratest.git//policies/foo.rego?ref=main\n//\n// The first time the gruntwork-io/terratest repo will be downloaded to a new temp directory. All subsequent calls will\n// reuse that first temporary dir where the repo was cloned. This is preserved even if a different subdir is requested\n// later, e.g.: git::https://github.com/gruntwork-io/terratest.git//examples/bar.rego?ref=main\n// Note that the query parameters are always included in the base URL. This means that if you use a different ref (e.g.,\n// git::https://github.com/gruntwork-io/terratest.git//examples/bar.rego?ref=v0.39.3), then that will be cloned to a new\n// temporary directory rather than the cached dir.\nfunc DownloadPolicyE(t testing.TestingT, rulePath string) (string, error) {\n\tcwd, err := os.Getwd()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// File getters are assumed to be a local path reference, so pass through the original path.\n\tvar fileGetter getter.FileGetter\n\tif ok, _ := fileGetter.Detect(&getter.Request{\n\t\tSrc:     rulePath,\n\t\tPwd:     cwd,\n\t\tGetMode: getter.ModeAny,\n\t}); ok {\n\t\treturn rulePath, nil\n\t}\n\n\t// At this point we assume the getter URL is a remote URL, so we start the process of downloading it to a temp dir.\n\n\t// First, check if we had already downloaded the source and it is in our cache.\n\tbaseDir, subDir := getter.SourceDirSubdir(rulePath)\n\tdownloadPath, hasDownloaded := policyDirCache.Load(baseDir)\n\tif hasDownloaded {\n\t\tlogger.Default.Logf(t, \"Previously downloaded %s: returning cached path\", baseDir)\n\t\treturn filepath.Join(downloadPath.(string), subDir), nil\n\t}\n\n\t// Not downloaded, so use go-getter to download the remote source to a temp dir.\n\ttempDir, err := os.MkdirTemp(\"\", \"terratest-opa-policy-*\")\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\t// go-getter doesn't work if you give it a directory that already exists, so we add an additional path in the\n\t// tempDir to make sure we feed a directory that doesn't exist yet.\n\ttempDir = filepath.Join(tempDir, \"getter\")\n\n\tlogger.Default.Logf(t, \"Downloading %s to temp dir %s\", rulePath, tempDir)\n\tif _, err := getter.GetAny(context.Background(), tempDir, baseDir); err != nil {\n\t\treturn \"\", err\n\t}\n\tpolicyDirCache.Store(baseDir, tempDir)\n\treturn filepath.Join(tempDir, subDir), nil\n}\n"
  },
  {
    "path": "modules/opa/download_policy_test.go",
    "content": "package opa\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/gruntwork-io/terratest/modules/files\"\n\t\"github.com/gruntwork-io/terratest/modules/git\"\n)\n\n// Test to make sure the DownloadPolicyE function returns a local path without processing it.\nfunc TestDownloadPolicyReturnsLocalPath(t *testing.T) {\n\tt.Parallel()\n\n\tlocalPath := \"../../examples/terraform-opa-example/policy/enforce_source.rego\"\n\tpath, err := DownloadPolicyE(t, localPath)\n\trequire.NoError(t, err)\n\tassert.Equal(t, localPath, path)\n}\n\n// Test to make sure the DownloadPolicyE function returns a remote path to a temporary directory.\nfunc TestDownloadPolicyDownloadsRemote(t *testing.T) {\n\tt.Parallel()\n\n\tcurRef := git.GetCurrentGitRef(t)\n\tbaseDir := fmt.Sprintf(\"git::https://github.com/gruntwork-io/terratest.git?ref=%s\", curRef)\n\tlocalPath := \"../../examples/terraform-opa-example/policy/enforce_source.rego\"\n\tremotePath := fmt.Sprintf(\"git::https://github.com/gruntwork-io/terratest.git//examples/terraform-opa-example/policy/enforce_source.rego?ref=%s\", curRef)\n\n\t// Make sure we clean up the downloaded file, while simultaneously asserting that the download dir was stored in the\n\t// cache.\n\tdefer func() {\n\t\tdownloadPathRaw, inCache := policyDirCache.Load(baseDir)\n\t\trequire.True(t, inCache)\n\t\tdownloadPath := downloadPathRaw.(string)\n\t\tif strings.HasSuffix(downloadPath, \"/getter\") {\n\t\t\tdownloadPath = filepath.Dir(downloadPath)\n\t\t}\n\t\tassert.NoError(t, os.RemoveAll(downloadPath))\n\t}()\n\n\tpath, err := DownloadPolicyE(t, remotePath)\n\trequire.NoError(t, err)\n\n\tabsPath, err := filepath.Abs(localPath)\n\trequire.NoError(t, err)\n\tassert.NotEqual(t, absPath, path)\n\n\tlocalContents, err := os.ReadFile(localPath)\n\trequire.NoError(t, err)\n\tremoteContents, err := os.ReadFile(path)\n\trequire.NoError(t, err)\n\tassert.Equal(t, localContents, remoteContents)\n}\n\n// Test to make sure the DownloadPolicyE function uses the cache if it has already downloaded an existing base path.\nfunc TestDownloadPolicyReusesCachedDir(t *testing.T) {\n\tt.Parallel()\n\n\tbaseDir := \"git::https://github.com/gruntwork-io/terratest.git?ref=main\"\n\tremotePath := \"git::https://github.com/gruntwork-io/terratest.git//examples/terraform-opa-example/policy/enforce_source.rego?ref=main\"\n\tremotePathAltSubPath := \"git::https://github.com/gruntwork-io/terratest.git//modules/opa/eval.go?ref=main\"\n\n\t// Make sure we clean up the downloaded file, while simultaneously asserting that the download dir was stored in the\n\t// cache.\n\tdefer func() {\n\t\tdownloadPathRaw, inCache := policyDirCache.Load(baseDir)\n\t\trequire.True(t, inCache)\n\t\tdownloadPath := downloadPathRaw.(string)\n\n\t\tif strings.HasSuffix(downloadPath, \"/getter\") {\n\t\t\tdownloadPath = filepath.Dir(downloadPath)\n\t\t}\n\t\tassert.NoError(t, os.RemoveAll(downloadPath))\n\t}()\n\n\tpath, err := DownloadPolicyE(t, remotePath)\n\trequire.NoError(t, err)\n\tfiles.FileExists(path)\n\n\tdownloadPathRaw, inCache := policyDirCache.Load(baseDir)\n\trequire.True(t, inCache)\n\tdownloadPath := downloadPathRaw.(string)\n\n\t// make sure the second call is exactly equal to the first call\n\tnewPath, err := DownloadPolicyE(t, remotePath)\n\trequire.NoError(t, err)\n\tassert.Equal(t, path, newPath)\n\n\t// Also make sure the cache is reused for alternative sub dirs.\n\tnewAltPath, err := DownloadPolicyE(t, remotePathAltSubPath)\n\trequire.NoError(t, err)\n\tassert.True(t, strings.HasPrefix(path, downloadPath))\n\tassert.True(t, strings.HasPrefix(newAltPath, downloadPath))\n}\n"
  },
  {
    "path": "modules/opa/eval.go",
    "content": "package opa\n\nimport (\n\t\"path/filepath\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/gruntwork-io/terratest/modules/logger\"\n\t\"github.com/gruntwork-io/terratest/modules/shell\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/hashicorp/go-multierror\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// EvalOptions defines options that can be passed to the 'opa eval' command for checking policies on arbitrary JSON data\n// via OPA.\ntype EvalOptions struct {\n\t// Whether OPA should run checks with failure.\n\tFailMode FailMode\n\n\t// Path to rego file containing the OPA rules. Can also be a remote path defined in go-getter syntax. Refer to\n\t// https://github.com/hashicorp/go-getter#url-format for supported options.\n\tRulePath string\n\n\t// Set a logger that should be used. See the logger package for more info.\n\tLogger *logger.Logger\n\n\t// Extra command line arguments to pass to opa eval. These are added after the eval subcommand\n\t// and before the standard arguments (-i, -d, query).\n\t// Example: []string{\"--v0-compatible\"} to enable OPA v0 compatibility mode.\n\t// Example: []string{\"--strict\"} to enable strict mode for the eval subcommand.\n\tExtraArgs []string\n\n\t// The following options can be used to change the behavior of the related functions for debuggability.\n\n\t// When true, keep any temp files and folders that are created for the purpose of running opa eval.\n\tDebugKeepTempFiles bool\n\n\t// When true, disable the functionality where terratest reruns the opa check on the same file and query all elements\n\t// on error. By default, terratest will rerun the opa eval call with `data` query so you can see all the contents\n\t// evaluated.\n\tDebugDisableQueryDataOnError bool\n}\n\n// FailMode signals whether `opa eval` should fail when the query returns an undefined value (FailUndefined), a\n// defined value (FailDefined), or not at all (NoFail).\ntype FailMode int\n\nconst (\n\tFailUndefined FailMode = iota\n\tFailDefined\n\tNoFail\n)\n\n// EvalE runs `opa eval` on the given JSON files using the configured policy file and result query. Translates to:\n//\n//\topa eval -i $JSONFile -d $RulePath $ResultQuery\n//\n// This will asynchronously run OPA on each file concurrently using goroutines.\n// This will fail the test if any one of the files failed.\nfunc Eval(t testing.TestingT, options *EvalOptions, jsonFilePaths []string, resultQuery string) {\n\trequire.NoError(t, EvalE(t, options, jsonFilePaths, resultQuery))\n}\n\n// EvalE runs `opa eval` on the given JSON files using the configured policy file and result query. Translates to:\n//\n//\topa eval -i $JSONFile -d $RulePath $ResultQuery\n//\n// This will asynchronously run OPA on each file concurrently using goroutines.\n// This will fail the test if any one of the files failed.\n// For each file, the output will be returned on the outputs slice.\nfunc EvalWithOutput(t testing.TestingT, options *EvalOptions, jsonFilePaths []string, resultQuery string) (outputs []string) {\n\toutputs, err := EvalWithOutputE(t, options, jsonFilePaths, resultQuery)\n\trequire.NoError(t, err)\n\treturn\n}\n\n// EvalE runs `opa eval` on the given JSON files using the configured policy file and result query. Translates to:\n//\n//\topa eval -i $JSONFile -d $RulePath $ResultQuery\n//\n// This will asynchronously run OPA on each file concurrently using goroutines.\nfunc EvalE(t testing.TestingT, options *EvalOptions, jsonFilePaths []string, resultQuery string) (err error) {\n\t_, err = evalE(t, options, jsonFilePaths, resultQuery)\n\treturn\n}\n\n// EvalWithOutputE runs `opa eval` on the given JSON files using the configured policy file and result query. Translates to:\n//\n//\topa eval -i $JSONFile -d $RulePath $ResultQuery\n//\n// This will asynchronously run OPA on each file concurrently using goroutines.\n// For each file, the output will be returned on the outputs slice.\nfunc EvalWithOutputE(t testing.TestingT, options *EvalOptions, jsonFilePaths []string, resultQuery string) (outputs []string, err error) {\n\treturn evalE(t, options, jsonFilePaths, resultQuery)\n}\n\nfunc evalE(t testing.TestingT, options *EvalOptions, jsonFilePaths []string, resultQuery string) (outputs []string, err error) {\n\tdownloadedPolicyPath, err := DownloadPolicyE(t, options.RulePath)\n\tif err != nil {\n\t\treturn\n\t}\n\n\toutputs = make([]string, len(jsonFilePaths))\n\twg := new(sync.WaitGroup)\n\twg.Add(len(jsonFilePaths))\n\terrorsOccurred := new(multierror.Error)\n\terrChans := make([]chan error, len(jsonFilePaths))\n\tfor i, jsonFilePath := range jsonFilePaths {\n\t\terrChan := make(chan error, 1)\n\t\terrChans[i] = errChan\n\n\t\tgo func(i int, jsonFilePath string) {\n\t\t\toutputs[i] = asyncEval(t, wg, errChan, options, downloadedPolicyPath, jsonFilePath, resultQuery)\n\t\t}(i, jsonFilePath)\n\t}\n\twg.Wait()\n\tfor _, errChan := range errChans {\n\t\terr := <-errChan\n\t\tif err != nil {\n\t\t\terrorsOccurred = multierror.Append(errorsOccurred, err)\n\t\t}\n\t}\n\treturn outputs, errorsOccurred.ErrorOrNil()\n}\n\n// asyncEval is a function designed to be run in a goroutine to asynchronously call `opa eval` on a single input file.\nfunc asyncEval(\n\tt testing.TestingT,\n\twg *sync.WaitGroup,\n\terrChan chan error,\n\toptions *EvalOptions,\n\tdownloadedPolicyPath string,\n\tjsonFilePath string,\n\tresultQuery string,\n) (output string) {\n\tdefer wg.Done()\n\tcmd := shell.Command{\n\t\tCommand: \"opa\",\n\t\tArgs:    formatOPAEvalArgs(options, downloadedPolicyPath, jsonFilePath, resultQuery),\n\n\t\t// Do not log output from shell package so we can log the full json without breaking it up. This is ok, because\n\t\t// opa eval is typically very quick.\n\t\tLogger: logger.Discard,\n\t}\n\toutput, err := runCommandWithFullLoggingE(t, options.Logger, cmd)\n\truleBasePath := filepath.Base(downloadedPolicyPath)\n\tif err == nil {\n\t\toptions.Logger.Logf(t, \"opa eval passed on file %s (policy %s; query %s)\", jsonFilePath, ruleBasePath, resultQuery)\n\t} else {\n\t\toptions.Logger.Logf(t, \"Failed opa eval on file %s (policy %s; query %s)\", jsonFilePath, ruleBasePath, resultQuery)\n\t\tif options.DebugDisableQueryDataOnError == false {\n\t\t\toptions.Logger.Logf(t, \"DEBUG: rerunning opa eval to query for full data.\")\n\t\t\tcmd.Args = formatOPAEvalArgs(options, downloadedPolicyPath, jsonFilePath, \"data\")\n\t\t\t// We deliberately ignore the error here as we want to only return the original error.\n\t\t\toutput, _ = runCommandWithFullLoggingE(t, options.Logger, cmd)\n\t\t}\n\t}\n\terrChan <- err\n\n\treturn\n}\n\n// formatOPAEvalArgs formats the arguments for the `opa eval` command.\nfunc formatOPAEvalArgs(options *EvalOptions, rulePath, jsonFilePath, resultQuery string) []string {\n\tvar args []string\n\n\t// Add the eval subcommand\n\targs = append(args, \"eval\")\n\n\t// Add any extra arguments provided by the user (for the eval subcommand)\n\t// These come before the fail mode flags to allow overriding behavior\n\tif len(options.ExtraArgs) > 0 {\n\t\targs = append(args, options.ExtraArgs...)\n\t}\n\n\tswitch options.FailMode {\n\tcase FailUndefined:\n\t\targs = append(args, \"--fail\")\n\tcase FailDefined:\n\t\targs = append(args, \"--fail-defined\")\n\t}\n\n\targs = append(\n\t\targs,\n\t\t[]string{\n\t\t\t\"-i\", jsonFilePath,\n\t\t\t\"-d\", rulePath,\n\t\t\tresultQuery,\n\t\t}...,\n\t)\n\treturn args\n}\n\n// runCommandWithFullLogging will log the command output in its entirety with buffering. This avoids breaking up the\n// logs when commands are run concurrently. This is a private function used in the context of opa only because opa runs\n// very quickly, and the output of opa is hard to parse if it is broken up by interleaved logs.\nfunc runCommandWithFullLoggingE(t testing.TestingT, logger *logger.Logger, cmd shell.Command) (output string, err error) {\n\toutput, err = shell.RunCommandAndGetOutputE(t, cmd)\n\tlogger.Logf(t, \"Output of command `%s %s`:\\n%s\", cmd.Command, strings.Join(cmd.Args, \" \"), output)\n\treturn\n}\n"
  },
  {
    "path": "modules/opa/eval_test.go",
    "content": "package opa\n\nimport (\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestFormatOPAEvalArgs(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname     string\n\t\toptions  *EvalOptions\n\t\trulePath string\n\t\tjsonFile string\n\t\tquery    string\n\t\texpected []string\n\t}{\n\t\t{\n\t\t\tname: \"Basic args without extras\",\n\t\t\toptions: &EvalOptions{\n\t\t\t\tFailMode: NoFail,\n\t\t\t},\n\t\t\trulePath: \"/path/to/policy.rego\",\n\t\t\tjsonFile: \"/path/to/input.json\",\n\t\t\tquery:    \"data.test.allow\",\n\t\t\texpected: []string{\"eval\", \"-i\", \"/path/to/input.json\", \"-d\", \"/path/to/policy.rego\", \"data.test.allow\"},\n\t\t},\n\t\t{\n\t\t\tname: \"With fail mode\",\n\t\t\toptions: &EvalOptions{\n\t\t\t\tFailMode: FailUndefined,\n\t\t\t},\n\t\t\trulePath: \"/path/to/policy.rego\",\n\t\t\tjsonFile: \"/path/to/input.json\",\n\t\t\tquery:    \"data.test.allow\",\n\t\t\texpected: []string{\"eval\", \"--fail\", \"-i\", \"/path/to/input.json\", \"-d\", \"/path/to/policy.rego\", \"data.test.allow\"},\n\t\t},\n\t\t{\n\t\t\tname: \"With extra args\",\n\t\t\toptions: &EvalOptions{\n\t\t\t\tFailMode:  FailUndefined,\n\t\t\t\tExtraArgs: []string{\"--format\", \"json\"},\n\t\t\t},\n\t\t\trulePath: \"/path/to/policy.rego\",\n\t\t\tjsonFile: \"/path/to/input.json\",\n\t\t\tquery:    \"data.test.allow\",\n\t\t\texpected: []string{\"eval\", \"--format\", \"json\", \"--fail\", \"-i\", \"/path/to/input.json\", \"-d\", \"/path/to/policy.rego\", \"data.test.allow\"},\n\t\t},\n\t\t{\n\t\t\tname: \"With v0-compatible flag\",\n\t\t\toptions: &EvalOptions{\n\t\t\t\tFailMode:  FailUndefined,\n\t\t\t\tExtraArgs: []string{\"--v0-compatible\"},\n\t\t\t},\n\t\t\trulePath: \"/path/to/policy.rego\",\n\t\t\tjsonFile: \"/path/to/input.json\",\n\t\t\tquery:    \"data.test.allow\",\n\t\t\texpected: []string{\"eval\", \"--v0-compatible\", \"--fail\", \"-i\", \"/path/to/input.json\", \"-d\", \"/path/to/policy.rego\", \"data.test.allow\"},\n\t\t},\n\t\t{\n\t\t\tname: \"With multiple extra args\",\n\t\t\toptions: &EvalOptions{\n\t\t\t\tFailMode:  FailUndefined,\n\t\t\t\tExtraArgs: []string{\"--v0-compatible\", \"--format\", \"json\"},\n\t\t\t},\n\t\t\trulePath: \"/path/to/policy.rego\",\n\t\t\tjsonFile: \"/path/to/input.json\",\n\t\t\tquery:    \"data.test.allow\",\n\t\t\texpected: []string{\"eval\", \"--v0-compatible\", \"--format\", \"json\", \"--fail\", \"-i\", \"/path/to/input.json\", \"-d\", \"/path/to/policy.rego\", \"data.test.allow\"},\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\ttest := test\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tactual := formatOPAEvalArgs(test.options, test.rulePath, test.jsonFile, test.query)\n\t\t\tassert.Equal(t, test.expected, actual)\n\t\t})\n\t}\n}\n\nfunc TestEvalWithOutput(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname string\n\n\t\tpolicy  string\n\t\tquery   string\n\t\tinputs  []string\n\t\toutputs []string\n\t\tisError bool\n\t}{\n\t\t{\n\t\t\tname: \"Success\",\n\t\t\tpolicy: `\n\t\t\t\tpackage test\n\t\t\t\tallow := true if {\n\t\t\t\t\tstartswith(input.user, \"admin\")\n\t\t\t\t}\n\t\t\t`,\n\t\t\tquery: \"data.test.allow\",\n\t\t\tinputs: []string{\n\t\t\t\t`{\"user\": \"admin-1\"}`,\n\t\t\t\t`{\"user\": \"admin-2\"}`,\n\t\t\t\t`{\"user\": \"admin-3\"}`,\n\t\t\t},\n\t\t\toutputs: []string{\n\t\t\t\t`{\n\t\t\t\t\t\"result\": [{\n\t\t\t\t\t\t\"expressions\": [{\n\t\t\t\t\t\t\t\"value\": true,\n\t\t\t\t\t\t\t\"text\": \"data.test.allow\",\n\t\t\t\t\t\t\t\"location\": {\n\t\t\t\t\t\t\t\t\"row\": 1,\n\t\t\t\t\t\t\t\t\"col\": 1\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}]\n\t\t\t\t\t}]\n\t\t\t\t}`,\n\t\t\t\t`{\n\t\t\t\t\t\"result\": [{\n\t\t\t\t\t\t\"expressions\": [{\n\t\t\t\t\t\t\t\"value\": true,\n\t\t\t\t\t\t\t\"text\": \"data.test.allow\",\n\t\t\t\t\t\t\t\"location\": {\n\t\t\t\t\t\t\t\t\"row\": 1,\n\t\t\t\t\t\t\t\t\"col\": 1\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}]\n\t\t\t\t\t}]\n\t\t\t\t}`,\n\t\t\t\t`{\n\t\t\t\t\t\"result\": [{\n\t\t\t\t\t\t\"expressions\": [{\n\t\t\t\t\t\t\t\"value\": true,\n\t\t\t\t\t\t\t\"text\": \"data.test.allow\",\n\t\t\t\t\t\t\t\"location\": {\n\t\t\t\t\t\t\t\t\"row\": 1,\n\t\t\t\t\t\t\t\t\"col\": 1\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}]\n\t\t\t\t\t}]\n\t\t\t\t}`,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"ContainsError\",\n\t\t\tpolicy: `\n\t\t\t\tpackage test\n\t\t\t\tallow := true if {\n\t\t\t\t\tinput.user == \"admin\"\n\t\t\t\t}\n\t\t\t`,\n\t\t\tquery:   \"data.test.allow\",\n\t\t\tisError: true,\n\t\t\tinputs: []string{\n\t\t\t\t`{\"user\": \"admin\"}`,\n\t\t\t\t`{\"user\": \"nobody\"}`,\n\t\t\t},\n\t\t\toutputs: []string{\n\t\t\t\t`{\n\t\t\t\t\t\"result\": [{\n\t\t\t\t\t\t\"expressions\": [{\n\t\t\t\t\t\t\t\"value\": true,\n\t\t\t\t\t\t\t\"text\": \"data.test.allow\",\n\t\t\t\t\t\t\t\"location\": {\n\t\t\t\t\t\t\t\t\"row\": 1,\n\t\t\t\t\t\t\t\t\"col\": 1\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}]\n\t\t\t\t\t}]\n\t\t\t\t}`,\n\t\t\t\t`{\n\t\t\t\t\t\"result\": [{\n\t\t\t\t\t\t\"expressions\": [{\n\t\t\t\t\t\t\t\"value\": {\n\t\t\t\t\t\t\t\t\"test\": {}\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"text\": \"data\",\n\t\t\t\t\t\t\t\"location\": {\n\t\t\t\t\t\t\t\t\"row\": 1,\n\t\t\t\t\t\t\t\t\"col\": 1\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}]\n\t\t\t\t\t}]\n\t\t\t\t}`,\n\t\t\t},\n\t\t},\n\t}\n\n\tcreateTempFile := func(t *testing.T, name string, content string) string {\n\t\tf, err := os.CreateTemp(t.TempDir(), name)\n\t\trequire.NoError(t, err)\n\t\tt.Cleanup(func() { os.Remove(f.Name()) })\n\t\t_, err = f.WriteString(content)\n\t\trequire.NoError(t, err)\n\t\treturn f.Name()\n\t}\n\n\tfor _, test := range tests {\n\t\ttest := test\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tpolicy := createTempFile(t, \"policy-*.rego\", test.policy)\n\t\t\tinputs := make([]string, len(test.inputs))\n\t\t\tfor i, input := range test.inputs {\n\t\t\t\tf := createTempFile(t, \"inputs-*.json\", input)\n\t\t\t\tinputs[i] = f\n\t\t\t}\n\n\t\t\toptions := &EvalOptions{\n\t\t\t\tRulePath: policy,\n\t\t\t}\n\n\t\t\toutputs, err := EvalWithOutputE(t, options, inputs, test.query)\n\t\t\tif test.isError {\n\t\t\t\tassert.Error(t, err)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t\tfor i, output := range test.outputs {\n\t\t\t\trequire.JSONEq(t, output, outputs[i], \"output for input: %d\", i)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "modules/packer/packer.go",
    "content": "// Package packer allows to interact with Packer.\npackage packer\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/gruntwork-io/terratest/modules/retry\"\n\t\"github.com/hashicorp/go-multierror\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/gruntwork-io/terratest/modules/logger\"\n\t\"github.com/gruntwork-io/terratest/modules/shell\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/hashicorp/go-version\"\n)\n\n// Options are the options for Packer.\ntype Options struct {\n\tTemplate                   string            // The path to the Packer template\n\tVars                       map[string]string // The custom vars to pass when running the build command\n\tVarFiles                   []string          // Var file paths to pass Packer using -var-file option\n\tOnly                       string            // If specified, only run the build of this name\n\tExcept                     string            // Runs the build excluding the specified builds and post-processors\n\tEnv                        map[string]string // Custom environment variables to set when running Packer\n\tRetryableErrors            map[string]string // If packer build fails with one of these (transient) errors, retry. The keys are a regexp to match against the error and the message is what to display to a user if that error is matched.\n\tMaxRetries                 int               // Maximum number of times to retry errors matching RetryableErrors\n\tTimeBetweenRetries         time.Duration     // The amount of time to wait between retries\n\tWorkingDir                 string            // The directory to run packer in\n\tLogger                     *logger.Logger    // If set, use a non-default logger\n\tDisableTemporaryPluginPath bool              // If set, do not use a temporary directory for Packer plugins.\n}\n\n// BuildArtifacts can take a map of identifierName <-> Options and then parallelize\n// the packer builds. Once all the packer builds have completed a map of identifierName <-> generated identifier\n// is returned. The identifierName can be anything you want, it is only used so that you can\n// know which generated artifact is which.\nfunc BuildArtifacts(t testing.TestingT, artifactNameToOptions map[string]*Options) map[string]string {\n\tresult, err := BuildArtifactsE(t, artifactNameToOptions)\n\n\tif err != nil {\n\t\tt.Fatalf(\"Error building artifacts: %s\", err.Error())\n\t}\n\n\treturn result\n}\n\n// BuildArtifactsE can take a map of identifierName <-> Options and then parallelize\n// the packer builds. Once all the packer builds have completed a map of identifierName <-> generated identifier\n// is returned. If any artifact fails to build, the errors are accumulated and returned\n// as a MultiError. The identifierName can be anything you want, it is only used so that you can\n// know which generated artifact is which.\nfunc BuildArtifactsE(t testing.TestingT, artifactNameToOptions map[string]*Options) (map[string]string, error) {\n\tvar waitForArtifacts sync.WaitGroup\n\twaitForArtifacts.Add(len(artifactNameToOptions))\n\n\tvar artifactNameToArtifactId = map[string]string{}\n\tvar errorsOccurred = new(multierror.Error)\n\n\tfor artifactName, curOptions := range artifactNameToOptions {\n\t\t// The following is necessary to make sure artifactName and curOptions don't\n\t\t// get updated due to concurrency within the scope of t.Run(..) below\n\t\tartifactName := artifactName\n\t\tcurOptions := curOptions\n\t\tgo func() {\n\t\t\tdefer waitForArtifacts.Done()\n\t\t\tartifactId, err := BuildArtifactE(t, curOptions)\n\n\t\t\tif err != nil {\n\t\t\t\terrorsOccurred = multierror.Append(errorsOccurred, err)\n\t\t\t} else {\n\t\t\t\tartifactNameToArtifactId[artifactName] = artifactId\n\t\t\t}\n\t\t}()\n\t}\n\n\twaitForArtifacts.Wait()\n\n\treturn artifactNameToArtifactId, errorsOccurred.ErrorOrNil()\n}\n\n// BuildArtifact builds the given Packer template and return the generated Artifact ID.\nfunc BuildArtifact(t testing.TestingT, options *Options) string {\n\tartifactID, err := BuildArtifactE(t, options)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn artifactID\n}\n\n// BuildArtifactE builds the given Packer template and return the generated Artifact ID.\nfunc BuildArtifactE(t testing.TestingT, options *Options) (string, error) {\n\toptions.Logger.Logf(t, \"Running Packer to generate a custom artifact for template %s\", options.Template)\n\n\t// By default, we download packer plugins to a temporary directory rather than use the global plugin path.\n\t// This prevents race conditions when multiple tests are running in parallel and each of them attempt\n\t// to download the same plugin at the same time to the global path.\n\t// Set DisableTemporaryPluginPath to disable this behavior.\n\tif !options.DisableTemporaryPluginPath {\n\t\t// The built-in env variable defining where plugins are downloaded\n\t\tconst packerPluginPathEnvVar = \"PACKER_PLUGIN_PATH\"\n\t\toptions.Logger.Logf(t, \"Creating a temporary directory for Packer plugins\")\n\t\tpluginDir, err := os.MkdirTemp(\"\", \"terratest-packer-\")\n\t\trequire.NoError(t, err)\n\t\tif len(options.Env) == 0 {\n\t\t\toptions.Env = make(map[string]string)\n\t\t}\n\t\toptions.Env[packerPluginPathEnvVar] = pluginDir\n\t\tdefer os.RemoveAll(pluginDir)\n\t}\n\n\terr := packerInit(t, options)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tcmd := shell.Command{\n\t\tCommand:    \"packer\",\n\t\tArgs:       formatPackerArgs(options),\n\t\tEnv:        options.Env,\n\t\tWorkingDir: options.WorkingDir,\n\t}\n\n\tdescription := fmt.Sprintf(\"%s %v\", cmd.Command, cmd.Args)\n\toutput, err := retry.DoWithRetryableErrorsE(t, description, options.RetryableErrors, options.MaxRetries, options.TimeBetweenRetries, func() (string, error) {\n\t\treturn shell.RunCommandAndGetOutputE(t, cmd)\n\t})\n\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn extractArtifactID(output)\n}\n\n// BuildAmi builds the given Packer template and return the generated AMI ID.\n//\n// Deprecated: Use BuildArtifact instead.\nfunc BuildAmi(t testing.TestingT, options *Options) string {\n\treturn BuildArtifact(t, options)\n}\n\n// BuildAmiE builds the given Packer template and return the generated AMI ID.\n//\n// Deprecated: Use BuildArtifactE instead.\nfunc BuildAmiE(t testing.TestingT, options *Options) (string, error) {\n\treturn BuildArtifactE(t, options)\n}\n\n// The Packer machine-readable log output should contain an entry of this format:\n//\n// AWS: <timestamp>,<builder>,artifact,<index>,id,<region>:<image_id>\n// GCP: <timestamp>,<builder>,artifact,<index>,id,<image_id>\n//\n// For example:\n//\n// 1456332887,amazon-ebs,artifact,0,id,us-east-1:ami-b481b3de\n// 1533742764,googlecompute,artifact,0,id,terratest-packer-example-2018-08-08t15-35-19z\nfunc extractArtifactID(packerLogOutput string) (string, error) {\n\tre := regexp.MustCompile(`.+artifact,\\d+?,id,(?:.+?:|)(.+)`)\n\tmatches := re.FindStringSubmatch(packerLogOutput)\n\n\tif len(matches) == 2 {\n\t\treturn matches[1], nil\n\t}\n\treturn \"\", errors.New(\"Could not find Artifact ID pattern in Packer output\")\n}\n\n// Check if the local version of Packer has init\nfunc hasPackerInit(t testing.TestingT, options *Options) (bool, error) {\n\t// The init command was introduced in Packer 1.7.0\n\tconst packerInitVersion = \"1.7.0\"\n\tminInitVersion, err := version.NewVersion(packerInitVersion)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tcmd := shell.Command{\n\t\tCommand:    \"packer\",\n\t\tArgs:       []string{\"-version\"},\n\t\tEnv:        options.Env,\n\t\tWorkingDir: options.WorkingDir,\n\t}\n\tversionCmdOutput, err := shell.RunCommandAndGetOutputE(t, cmd)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tlocalVersion := trimPackerVersion(versionCmdOutput)\n\tthisVersion, err := version.NewVersion(localVersion)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tif thisVersion.LessThan(minInitVersion) {\n\t\treturn false, nil\n\t}\n\n\treturn true, nil\n}\n\n// packerInit runs 'packer init' if it is supported by the local packer\nfunc packerInit(t testing.TestingT, options *Options) error {\n\thasInit, err := hasPackerInit(t, options)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !hasInit {\n\t\toptions.Logger.Logf(t, \"Skipping 'packer init' because it is not present in this version\")\n\t\treturn nil\n\t}\n\n\textension := filepath.Ext(options.Template)\n\tif extension != \".hcl\" {\n\t\toptions.Logger.Logf(t, \"Skipping 'packer init' because it is only supported for HCL2 templates\")\n\t\treturn nil\n\t}\n\n\tcmd := shell.Command{\n\t\tCommand:    \"packer\",\n\t\tArgs:       []string{\"init\", options.Template},\n\t\tEnv:        options.Env,\n\t\tWorkingDir: options.WorkingDir,\n\t}\n\n\tdescription := \"Running Packer init\"\n\t_, err = retry.DoWithRetryableErrorsE(t, description, options.RetryableErrors, options.MaxRetries, options.TimeBetweenRetries, func() (string, error) {\n\t\treturn shell.RunCommandAndGetOutputE(t, cmd)\n\t})\n\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// Convert the inputs to a format palatable to packer. The build command should have the format:\n//\n// packer build [OPTIONS] template\nfunc formatPackerArgs(options *Options) []string {\n\targs := []string{\"build\", \"-machine-readable\"}\n\n\tfor key, value := range options.Vars {\n\t\targs = append(args, \"-var\", fmt.Sprintf(\"%s=%s\", key, value))\n\t}\n\n\tfor _, filePath := range options.VarFiles {\n\t\targs = append(args, \"-var-file\", filePath)\n\t}\n\n\tif options.Only != \"\" {\n\t\targs = append(args, fmt.Sprintf(\"-only=%s\", options.Only))\n\t}\n\n\tif options.Except != \"\" {\n\t\targs = append(args, fmt.Sprintf(\"-except=%s\", options.Except))\n\t}\n\n\treturn append(args, options.Template)\n}\n\n// From packer 1.10 the -version command output is prefixed with Packer v\nfunc trimPackerVersion(versionCmdOutput string) string {\n\tre := regexp.MustCompile(`(?:Packer v?|)(\\d+\\.\\d+\\.\\d+)`)\n\tmatches := re.FindStringSubmatch(versionCmdOutput)\n\tif len(matches) > 1 {\n\t\treturn matches[1]\n\t}\n\treturn \"\"\n}\n\ntype packerManifest struct {\n\tBuilds      []packerManifestBuild `json:\"builds\"`\n\tLastRunUUID string                `json:\"last_run_uuid\"`\n}\n\ntype packerManifestBuild struct {\n\tName          string                    `json:\"name\"`\n\tBuilderType   string                    `json:\"builder_type\"`\n\tBuildTime     int64                     `json:\"build_time\"`\n\tFiles         []packerManifestBuildFile `json:\"files\"`\n\tArtifactID    string                    `json:\"artifact_id\"`\n\tPackerRunUUID string                    `json:\"packer_run_uuid\"`\n\tCustomData    map[string]interface{}    `json:\"custom_data\"`\n}\n\ntype packerManifestBuildFile struct {\n\tName string `json:\"name\"`\n\tSize int64  `json:\"size\"`\n}\n\n// GetArtifactIDFromManifestBuildName returns the artifact id from a build name contained in the manifest file\n// see https://developer.hashicorp.com/packer/docs/post-processors/manifest for more info\n// if the build name is not found, it will fail the test\nfunc GetArtifactIDFromManifestBuildName(t testing.TestingT, manifestPath string, buildName string) string {\n\tartifactID, err := GetArtifactIDFromManifestBuildNameE(t, manifestPath, buildName)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to get artifact id from manifest build name: %s\", err)\n\t}\n\treturn artifactID\n}\n\n// GetArtifactIDFromManifestBuildNameE returns the artifact id from a build name contained in the manifest file\n// see https://developer.hashicorp.com/packer/docs/post-processors/manifest for more info\nfunc GetArtifactIDFromManifestBuildNameE(t testing.TestingT, manifestPath string, buildName string) (artifactID string, err error) {\n\tb, err := os.ReadFile(manifestPath)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"error reading manifest file: %w\", err)\n\t}\n\n\tvar manifest packerManifest\n\tif err = json.Unmarshal(b, &manifest); err != nil {\n\t\treturn \"\", fmt.Errorf(\"error unmarshalling manifest file: %w\", err)\n\t}\n\n\tfound := false\n\tfor _, build := range manifest.Builds {\n\t\tif build.Name != buildName {\n\t\t\tcontinue\n\t\t}\n\n\t\tartifactID, found = build.ArtifactID, true\n\t\tbreak\n\t}\n\tif !found {\n\t\treturn \"\", fmt.Errorf(\"build name %s not found in manifest file %s\", buildName, manifestPath)\n\t}\n\n\treturn artifactID, nil\n}\n"
  },
  {
    "path": "modules/packer/packer_test.go",
    "content": "package packer\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestExtractAmiIdFromOneLine(t *testing.T) {\n\tt.Parallel()\n\n\texpectedAMIID := \"ami-b481b3de\"\n\ttext := fmt.Sprintf(\"1456332887,amazon-ebs,artifact,0,id,us-east-1:%s\", expectedAMIID)\n\tactualAMIID, err := extractArtifactID(text)\n\n\tif err != nil {\n\t\tt.Errorf(\"Did not expect to get an error when extracting a valid AMI ID: %s\", err)\n\t}\n\n\tif actualAMIID != expectedAMIID {\n\t\tt.Errorf(\"Did not get expected AMI ID. Expected: %s. Actual: %s.\", expectedAMIID, actualAMIID)\n\t}\n}\n\nfunc TestExtractImageIdFromOneLine(t *testing.T) {\n\tt.Parallel()\n\n\texpectedImageID := \"terratest-packer-example-2018-08-09t12-02-58z\"\n\ttext := fmt.Sprintf(\"1533816302,googlecompute,artifact,0,id,%s\", expectedImageID)\n\tactualImageID, err := extractArtifactID(text)\n\n\tif err != nil {\n\t\tt.Errorf(\"Did not expect to get an error when extracting a valid Image ID: %s\", err)\n\t}\n\n\tif actualImageID != expectedImageID {\n\t\tt.Errorf(\"Did not get expected Image ID. Expected: %s. Actual: %s.\", expectedImageID, actualImageID)\n\t}\n}\n\nfunc TestExtractAmiIdFromMultipleLines(t *testing.T) {\n\tt.Parallel()\n\n\texpectedAMIID := \"ami-b481b3de\"\n\ttext := fmt.Sprintf(`\n\tfoo\n\tbar\n\t1456332887,amazon-ebs,artifact,0,id,us-east-1:%s\n\tbaz\n\tblah\n\t`, expectedAMIID)\n\n\tactualAMIID, err := extractArtifactID(text)\n\n\tif err != nil {\n\t\tt.Errorf(\"Did not expect to get an error when extracting a valid AMI ID: %s\", err)\n\t}\n\n\tif actualAMIID != expectedAMIID {\n\t\tt.Errorf(\"Did not get expected AMI ID. Expected: %s. Actual: %s.\", expectedAMIID, actualAMIID)\n\t}\n}\n\nfunc TestExtractImageIdFromMultipleLines(t *testing.T) {\n\tt.Parallel()\n\n\texpectedImageID := \"terratest-packer-example-2018-08-09t12-02-58z\"\n\ttext := fmt.Sprintf(`\n\tfoo\n\tbar\n\t1533816302,googlecompute,artifact,0,id,%s\n\tbaz\n\tblah\n\t`, expectedImageID)\n\n\tactualImageID, err := extractArtifactID(text)\n\n\tif err != nil {\n\t\tt.Errorf(\"Did not expect to get an error when extracting a valid Image ID: %s\", err)\n\t}\n\n\tif actualImageID != expectedImageID {\n\t\tt.Errorf(\"Did not get the expected Image ID. Expected: %s. Actual: %s.\", expectedImageID, actualImageID)\n\t}\n}\n\nfunc TestExtractAmiIdNoIdPresent(t *testing.T) {\n\tt.Parallel()\n\n\ttext := `\n\tfoo\n\tbar\n\tbaz\n\tblah\n\t`\n\n\t_, err := extractArtifactID(text)\n\n\tif err == nil {\n\t\tt.Error(\"Expected to get an error when extracting an AMI ID from text with no AMI in it, but got nil\")\n\t}\n\n}\n\nfunc TestExtractArtifactINoIdPresent(t *testing.T) {\n\tt.Parallel()\n\n\ttext := `\n\tfoo\n\tbar\n\tbaz\n\tblah\n\t`\n\n\t_, err := extractArtifactID(text)\n\n\tif err == nil {\n\t\tt.Error(\"Expected to get an error when extracting an Artifact ID from text with no Artifact ID in it, but got nil\")\n\t}\n}\n\nfunc TestFormatPackerArgs(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\toption   *Options\n\t\texpected string\n\t}{\n\t\t{\n\t\t\toption: &Options{\n\t\t\t\tTemplate: \"packer.json\",\n\t\t\t},\n\t\t\texpected: \"build -machine-readable packer.json\",\n\t\t},\n\t\t{\n\t\t\toption: &Options{\n\t\t\t\tTemplate: \"packer.json\",\n\t\t\t\tVars: map[string]string{\n\t\t\t\t\t\"foo\": \"bar\",\n\t\t\t\t},\n\t\t\t\tOnly: \"onlythis\",\n\t\t\t},\n\t\t\texpected: \"build -machine-readable -var foo=bar -only=onlythis packer.json\",\n\t\t},\n\t\t{\n\t\t\toption: &Options{\n\t\t\t\tTemplate: \"packer.json\",\n\t\t\t\tVars: map[string]string{\n\t\t\t\t\t\"foo\": \"bar\",\n\t\t\t\t},\n\t\t\t\tOnly:   \"onlythis\",\n\t\t\t\tExcept: \"long-run-pp,artifact\",\n\t\t\t},\n\t\t\texpected: \"build -machine-readable -var foo=bar -only=onlythis -except=long-run-pp,artifact packer.json\",\n\t\t},\n\t\t{\n\t\t\toption: &Options{\n\t\t\t\tTemplate: \"packer.json\",\n\t\t\t\tVars: map[string]string{\n\t\t\t\t\t\"foo\": \"bar\",\n\t\t\t\t},\n\t\t\t\tVarFiles: []string{\n\t\t\t\t\t\"foofile.json\",\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: \"build -machine-readable -var foo=bar -var-file foofile.json packer.json\",\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\targs := formatPackerArgs(test.option)\n\t\tassert.Equal(t, strings.Join(args, \" \"), test.expected)\n\t}\n}\n\nfunc TestTrimPackerVersion(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tversionOutput string\n\t\texpected      string\n\t}{\n\t\t{\n\t\t\t// Pre 1.10 output\n\t\t\tversionOutput: \"1.7.0\",\n\t\t\texpected:      \"1.7.0\",\n\t\t},\n\t\t{\n\t\t\t// From 1.10 matches the output of packer version\n\t\t\tversionOutput: \"Packer v1.10.0\",\n\t\t\texpected:      \"1.10.0\",\n\t\t},\n\t\t{\n\t\t\t// From 1.10 matches the output of packer version\n\t\t\tversionOutput: \"Packer v1.10.0\\n\\nYour version of Packer is out of date! The latest version\\nis 1.10.3. You can update by downloading from www.packer.io/downloads\\n\",\n\t\t\texpected:      \"1.10.0\",\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.versionOutput, func(t *testing.T) {\n\t\t\tout := trimPackerVersion(test.versionOutput)\n\t\t\tassert.Equal(t, test.expected, out)\n\t\t})\n\t}\n}\n\nfunc TestGetArtifactIDFromManifestBuildNameE(t *testing.T) {\n\tt.Parallel()\n\n\t// example manifest from https://developer.hashicorp.com/packer/docs/post-processors/manifest\n\tmanifest := `\n{\n  \"builds\": [\n    {\n      \"name\": \"docker\",\n      \"builder_type\": \"docker\",\n      \"build_time\": 1507245986,\n      \"files\": [\n        {\n          \"name\": \"packer_example\",\n          \"size\": 102219776\n        }\n      ],\n      \"artifact_id\": \"Container\",\n      \"packer_run_uuid\": \"6d5d3185-fa95-44e1-8775-9e64fe2e2d8f\",\n      \"custom_data\": {\n        \"my_custom_data\": \"example\"\n      }\n    }\n  ],\n  \"last_run_uuid\": \"6d5d3185-fa95-44e1-8775-9e64fe2e2d8f\"\n}\n`\n\tmanifestPath := filepath.Join(t.TempDir(), \"manifest.json\")\n\terr := os.WriteFile(manifestPath, []byte(manifest), 0600)\n\trequire.NoError(t, err)\n\n\tt.Run(\"Found\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tartifactID, err := GetArtifactIDFromManifestBuildNameE(t, manifestPath, \"docker\")\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, \"Container\", artifactID)\n\n\t\tartifactID2 := GetArtifactIDFromManifestBuildName(t, manifestPath, \"docker\")\n\t\tassert.Equal(t, \"Container\", artifactID2)\n\t})\n\n\tt.Run(\"Not Found\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\t_, err := GetArtifactIDFromManifestBuildNameE(t, manifestPath, \"notfound\")\n\t\trequire.Error(t, err)\n\t})\n\n}\n"
  },
  {
    "path": "modules/random/random.go",
    "content": "// Package random contains different random generators.\npackage random\n\nimport (\n\t\"bytes\"\n\t\"math/rand\"\n\t\"time\"\n)\n\n// Random generates a random int between min and max, inclusive.\nfunc Random(min int, max int) int {\n\treturn newRand().Intn(max-min+1) + min\n}\n\n// RandomInt picks a random element in the slice of ints.\nfunc RandomInt(elements []int) int {\n\tindex := Random(0, len(elements)-1)\n\treturn elements[index]\n}\n\n// RandomString picks a random element in the slice of string.\nfunc RandomString(elements []string) string {\n\tindex := Random(0, len(elements)-1)\n\treturn elements[index]\n}\n\nconst base62chars = \"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz\"\nconst uniqueIDLength = 6 // Should be good for 62^6 = 56+ billion combinations\n\n// UniqueID returns a unique (ish) id we can attach to resources and tfstate files so they don't conflict with each other\n// Uses base 62 to generate a 6 character string that's unlikely to collide with the handful of tests we run in\n// parallel. Based on code here: http://stackoverflow.com/a/9543797/483528\nfunc UniqueID() string {\n\tvar out bytes.Buffer\n\n\tgenerator := newRand()\n\tfor i := 0; i < uniqueIDLength; i++ {\n\t\tout.WriteByte(base62chars[generator.Intn(len(base62chars))])\n\t}\n\n\treturn out.String()\n}\n\n// UniqueId is deprecated, use UniqueID instead.\n//\n// Deprecated: Use UniqueID.\n//\n//nolint:staticcheck,revive\nfunc UniqueId() string {\n\treturn UniqueID()\n}\n\n// newRand creates a new random number generator, seeding it with the current system time.\nfunc newRand() *rand.Rand {\n\treturn rand.New(rand.NewSource(time.Now().UnixNano()))\n}\n"
  },
  {
    "path": "modules/random/random_test.go",
    "content": "package random_test\n\nimport (\n\t\"strconv\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestRandom(t *testing.T) {\n\tt.Parallel()\n\n\tmin := 0\n\tmax := 100\n\n\tfor i := 0; i < 100000; i++ {\n\t\tvalue := random.Random(min, max)\n\t\tassert.True(t, value >= min && value <= max)\n\t}\n}\n\nfunc TestRandomInt(t *testing.T) {\n\tt.Parallel()\n\n\tmin := 0\n\tmax := 1000\n\n\tlist := []int{}\n\tfor i := min; i < max; i++ {\n\t\tlist = append(list, i)\n\t}\n\n\tfor i := 0; i < 100000; i++ {\n\t\tvalue := random.RandomInt(list)\n\t\tassert.Contains(t, list, value)\n\t}\n}\n\nfunc TestRandomString(t *testing.T) {\n\tt.Parallel()\n\n\tmin := 0\n\tmax := 1000\n\n\tlist := []string{}\n\tfor i := min; i < max; i++ {\n\t\tlist = append(list, strconv.Itoa(i))\n\t}\n\n\tfor i := 0; i < 100000; i++ {\n\t\tvalue := random.RandomString(list)\n\t\tassert.Contains(t, list, value)\n\t}\n}\n\nfunc TestUniqueID(t *testing.T) {\n\tt.Parallel()\n\n\tpreviouslySeen := map[string]bool{}\n\n\tfor i := 0; i < 100; i++ {\n\t\tuniqueID := random.UniqueID()\n\t\tassert.Len(t, uniqueID, 6)\n\t\tassert.NotContains(t, previouslySeen, uniqueID)\n\n\t\tpreviouslySeen[uniqueID] = true\n\t}\n}\n"
  },
  {
    "path": "modules/retry/retry.go",
    "content": "// Package retry contains logic to retry actions with certain conditions.\npackage retry\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"regexp\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/gruntwork-io/terratest/modules/logger\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n)\n\n// Either contains a result and potentially an error.\ntype Either struct {\n\tError  error\n\tResult string\n}\n\n// DoWithTimeout runs the specified action and waits up to the specified timeout for it to complete. Return the output of the action if\n// it completes on time or fail the test otherwise.\nfunc DoWithTimeout(t testing.TestingT, actionDescription string, timeout time.Duration, action func() (string, error)) string {\n\tout, err := DoWithTimeoutE(t, actionDescription, timeout, action)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\treturn out\n}\n\n// DoWithTimeoutE runs the specified action and waits up to the specified timeout for it to complete. Return the output of the action if\n// it completes on time or an error otherwise.\nfunc DoWithTimeoutE(t testing.TestingT, actionDescription string, timeout time.Duration, action func() (string, error)) (string, error) {\n\tctx, cancel := context.WithTimeout(context.Background(), timeout)\n\tdefer cancel()\n\n\tresultChannel := make(chan Either, 1)\n\n\tgo func() {\n\t\tout, err := action()\n\t\tresultChannel <- Either{Result: out, Error: err}\n\t}()\n\n\tselect {\n\tcase either := <-resultChannel:\n\t\treturn either.Result, either.Error\n\tcase <-ctx.Done():\n\t\treturn \"\", TimeoutExceeded{Description: actionDescription, Timeout: timeout}\n\t}\n}\n\n// DoWithRetry runs the specified action. If it returns a string, return that string. If it returns a FatalError, return that error\n// immediately. If it returns any other type of error, sleep for sleepBetweenRetries and try again, up to a maximum of\n// maxRetries retries. If maxRetries is exceeded, fail the test.\nfunc DoWithRetry(t testing.TestingT, actionDescription string, maxRetries int, sleepBetweenRetries time.Duration, action func() (string, error)) string {\n\tout, err := DoWithRetryE(t, actionDescription, maxRetries, sleepBetweenRetries, action)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\treturn out\n}\n\n// DoWithRetryE runs the specified action. If it returns a string, return that string. If it returns a FatalError, return that error\n// immediately. If it returns any other type of error, sleep for sleepBetweenRetries and try again, up to a maximum of\n// maxRetries retries. If maxRetries is exceeded, return a MaxRetriesExceeded error.\nfunc DoWithRetryE(t testing.TestingT, actionDescription string, maxRetries int, sleepBetweenRetries time.Duration, action func() (string, error)) (string, error) {\n\tout, err := DoWithRetryInterfaceE(t, actionDescription, maxRetries, sleepBetweenRetries, func() (any, error) { return action() })\n\n\treturn out.(string), err\n}\n\n// DoWithRetryInterface runs the specified action. If it returns a value, return that value. If it returns a FatalError, return that error\n// immediately. If it returns any other type of error, sleep for sleepBetweenRetries and try again, up to a maximum of\n// maxRetries retries. If maxRetries is exceeded, fail the test.\nfunc DoWithRetryInterface(t testing.TestingT, actionDescription string, maxRetries int, sleepBetweenRetries time.Duration, action func() (any, error)) any {\n\tout, err := DoWithRetryInterfaceE(t, actionDescription, maxRetries, sleepBetweenRetries, action)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\treturn out\n}\n\n// DoWithRetryInterfaceE runs the specified action. If it returns a value, return that value. If it returns a FatalError, return that error\n// immediately. If it returns any other type of error, sleep for sleepBetweenRetries and try again, up to a maximum of\n// maxRetries retries. If maxRetries is exceeded, return a MaxRetriesExceeded error.\nfunc DoWithRetryInterfaceE(t testing.TestingT, actionDescription string, maxRetries int, sleepBetweenRetries time.Duration, action func() (any, error)) (any, error) {\n\tvar output any\n\n\tvar err error\n\n\tfor i := 0; i <= maxRetries; i++ {\n\t\tlogger.Default.Logf(t, \"%s\", actionDescription)\n\n\t\toutput, err = action()\n\t\tif err == nil {\n\t\t\treturn output, nil\n\t\t}\n\n\t\tvar fatalErr FatalError\n\t\tif errors.As(err, &fatalErr) {\n\t\t\tlogger.Default.Logf(t, \"Returning due to fatal error: %v\", err)\n\n\t\t\treturn output, err\n\t\t}\n\n\t\tlogger.Default.Logf(t, \"%s returned an error: %s. Sleeping for %s and will try again.\", actionDescription, err.Error(), sleepBetweenRetries)\n\t\ttime.Sleep(sleepBetweenRetries)\n\t}\n\n\treturn output, MaxRetriesExceeded{Description: actionDescription, MaxRetries: maxRetries}\n}\n\n// DoWithRetryableErrors runs the specified action. If it returns a value, return that value. If it returns an error,\n// check if error message or the string output from the action (which is often stdout/stderr from running some command)\n// matches any of the regular expressions in the specified retryableErrors map. If there is a match, sleep for\n// sleepBetweenRetries, and retry the specified action, up to a maximum of maxRetries retries. If there is no match,\n// return that error immediately, wrapped in a FatalError. If maxRetries is exceeded, return a MaxRetriesExceeded error.\nfunc DoWithRetryableErrors(t testing.TestingT, actionDescription string, retryableErrors map[string]string, maxRetries int, sleepBetweenRetries time.Duration, action func() (string, error)) string {\n\tout, err := DoWithRetryableErrorsE(t, actionDescription, retryableErrors, maxRetries, sleepBetweenRetries, action)\n\trequire.NoError(t, err)\n\n\treturn out\n}\n\n// DoWithRetryableErrorsE runs the specified action. If it returns a value, return that value. If it returns an error,\n// check if error message or the string output from the action (which is often stdout/stderr from running some command)\n// matches any of the regular expressions in the specified retryableErrors map. If there is a match, sleep for\n// sleepBetweenRetries, and retry the specified action, up to a maximum of maxRetries retries. If there is no match,\n// return that error immediately, wrapped in a FatalError. If maxRetries is exceeded, return a MaxRetriesExceeded error.\nfunc DoWithRetryableErrorsE(t testing.TestingT, actionDescription string, retryableErrors map[string]string, maxRetries int, sleepBetweenRetries time.Duration, action func() (string, error)) (string, error) {\n\tretryableErrorsRegexp := map[*regexp.Regexp]string{}\n\n\tfor errorStr, errorMessage := range retryableErrors {\n\t\terrorRegex, err := regexp.Compile(errorStr)\n\t\tif err != nil {\n\t\t\treturn \"\", FatalError{Underlying: err}\n\t\t}\n\n\t\tretryableErrorsRegexp[errorRegex] = errorMessage\n\t}\n\n\treturn DoWithRetryE(t, actionDescription, maxRetries, sleepBetweenRetries, func() (string, error) {\n\t\toutput, err := action()\n\t\tif err == nil {\n\t\t\treturn output, nil\n\t\t}\n\n\t\tfor errorRegexp, errorMessage := range retryableErrorsRegexp {\n\t\t\tif errorRegexp.MatchString(output) || errorRegexp.MatchString(err.Error()) {\n\t\t\t\tlogger.Default.Logf(t, \"'%s' failed with the error '%s' but this error was expected and warrants a retry. Further details: %s\\n\", actionDescription, err.Error(), errorMessage)\n\t\t\t\treturn output, err\n\t\t\t}\n\t\t}\n\n\t\treturn output, FatalError{Underlying: err}\n\t})\n}\n\n// Done can be stopped.\ntype Done struct {\n\tstop chan bool\n}\n\n// Done stops the execution.\nfunc (done Done) Done() {\n\tdone.stop <- true\n}\n\n// DoInBackgroundUntilStopped runs the specified action in the background (in a goroutine) repeatedly, waiting the specified amount of time between\n// repetitions. To stop this action, call the Done() function on the returned value.\nfunc DoInBackgroundUntilStopped(t testing.TestingT, actionDescription string, sleepBetweenRepeats time.Duration, action func()) Done {\n\tstop := make(chan bool)\n\n\tgo func() {\n\t\tfor {\n\t\t\tlogger.Default.Logf(t, \"Executing action '%s'\", actionDescription)\n\n\t\t\taction()\n\n\t\t\tlogger.Default.Logf(t, \"Sleeping for %s before repeating action '%s'\", sleepBetweenRepeats, actionDescription)\n\n\t\t\tselect {\n\t\t\tcase <-time.After(sleepBetweenRepeats):\n\t\t\t\t// Nothing to do, just allow the loop to continue\n\t\t\tcase <-stop:\n\t\t\t\tlogger.Default.Logf(t, \"Received stop signal for action '%s'.\", actionDescription)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}()\n\n\treturn Done{stop: stop}\n}\n\n// Custom error types\n\n// TimeoutExceeded is an error that occurs when a timeout is exceeded.\ntype TimeoutExceeded struct {\n\tDescription string\n\tTimeout     time.Duration\n}\n\nfunc (err TimeoutExceeded) Error() string {\n\treturn fmt.Sprintf(\"'%s' did not complete before timeout of %s\", err.Description, err.Timeout)\n}\n\n// MaxRetriesExceeded is an error that occurs when the maximum amount of retries is exceeded.\ntype MaxRetriesExceeded struct {\n\tDescription string\n\tMaxRetries  int\n}\n\nfunc (err MaxRetriesExceeded) Error() string {\n\treturn fmt.Sprintf(\"'%s' unsuccessful after %d retries\", err.Description, err.MaxRetries)\n}\n\n// FatalError is a marker interface for errors that should not be retried.\ntype FatalError struct {\n\tUnderlying error\n}\n\nfunc (err FatalError) Error() string {\n\treturn fmt.Sprintf(\"FatalError{Underlying: %v}\", err.Underlying)\n}\n"
  },
  {
    "path": "modules/retry/retry_test.go",
    "content": "package retry_test\n\nimport (\n\t\"errors\"\n\t\"strconv\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/gruntwork-io/terratest/modules/retry\"\n)\n\nfunc TestDoWithRetry(t *testing.T) {\n\tt.Parallel()\n\n\texpectedOutput := \"expected\"\n\texpectedError := errors.New(\"expected error\")\n\n\tactionAlwaysReturnsExpected := func() (string, error) { return expectedOutput, nil }\n\tactionAlwaysReturnsError := func() (string, error) { return expectedOutput, expectedError }\n\n\tcreateActionThatReturnsExpectedAfterFiveRetries := func() func() (string, error) {\n\t\tcount := 0\n\n\t\treturn func() (string, error) {\n\t\t\tcount++\n\n\t\t\tif count > 5 {\n\t\t\t\treturn expectedOutput, nil\n\t\t\t}\n\n\t\t\treturn expectedOutput, expectedError\n\t\t}\n\t}\n\n\ttestCases := []struct {\n\t\texpectedError error\n\t\taction        func() (string, error)\n\t\tdescription   string\n\t\tmaxRetries    int\n\t}{\n\t\t{description: \"Return value on first try\", maxRetries: 10, action: actionAlwaysReturnsExpected},\n\t\t{description: \"Return error on all retries\", maxRetries: 10, expectedError: retry.MaxRetriesExceeded{Description: \"Return error on all retries\", MaxRetries: 10}, action: actionAlwaysReturnsError},\n\t\t{description: \"Return value after 5 retries\", maxRetries: 10, action: createActionThatReturnsExpectedAfterFiveRetries()},\n\t\t{description: \"Return value after 5 retries, but only do 4 retries\", maxRetries: 4, expectedError: retry.MaxRetriesExceeded{Description: \"Return value after 5 retries, but only do 4 retries\", MaxRetries: 4}, action: createActionThatReturnsExpectedAfterFiveRetries()},\n\t}\n\n\tfor _, testCase := range testCases {\n\t\ttestCase := testCase // capture range variable for each test case\n\n\t\tt.Run(testCase.description, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tactualOutput, err := retry.DoWithRetryE(t, testCase.description, testCase.maxRetries, 1*time.Millisecond, testCase.action)\n\t\t\tassert.Equal(t, expectedOutput, actualOutput)\n\n\t\t\tif testCase.expectedError != nil {\n\t\t\t\tassert.Equal(t, testCase.expectedError, err)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tassert.Equal(t, expectedOutput, actualOutput)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDoWithTimeout(t *testing.T) {\n\tt.Parallel()\n\n\texpectedOutput := \"expected\"\n\texpectedError := errors.New(\"expected error\")\n\n\tactionReturnsValueImmediately := func() (string, error) { return expectedOutput, nil }\n\tactionReturnsErrorImmediately := func() (string, error) { return \"\", expectedError }\n\n\tcreateActionThatReturnsValueAfterDelay := func(delay time.Duration) func() (string, error) {\n\t\treturn func() (string, error) {\n\t\t\ttime.Sleep(delay)\n\n\t\t\treturn expectedOutput, nil\n\t\t}\n\t}\n\n\tcreateActionThatReturnsErrorAfterDelay := func(delay time.Duration) func() (string, error) {\n\t\treturn func() (string, error) {\n\t\t\ttime.Sleep(delay)\n\n\t\t\treturn \"\", expectedError\n\t\t}\n\t}\n\n\ttestCases := []struct {\n\t\texpectedError error\n\t\taction        func() (string, error)\n\t\tdescription   string\n\t\ttimeout       time.Duration\n\t}{\n\t\t{description: \"Returns value immediately\", timeout: 5 * time.Second, action: actionReturnsValueImmediately},\n\t\t{description: \"Returns error immediately\", timeout: 5 * time.Second, expectedError: expectedError, action: actionReturnsErrorImmediately},\n\t\t{description: \"Returns value after 2 seconds\", timeout: 5 * time.Second, action: createActionThatReturnsValueAfterDelay(2 * time.Second)},\n\t\t{description: \"Returns error after 2 seconds\", timeout: 5 * time.Second, expectedError: expectedError, action: createActionThatReturnsErrorAfterDelay(2 * time.Second)},\n\t\t{description: \"Returns value after timeout exceeded\", timeout: 5 * time.Second, expectedError: retry.TimeoutExceeded{Description: \"Returns value after timeout exceeded\", Timeout: 5 * time.Second}, action: createActionThatReturnsValueAfterDelay(10 * time.Second)},\n\t\t{description: \"Returns error after timeout exceeded\", timeout: 5 * time.Second, expectedError: retry.TimeoutExceeded{Description: \"Returns error after timeout exceeded\", Timeout: 5 * time.Second}, action: createActionThatReturnsErrorAfterDelay(10 * time.Second)},\n\t}\n\n\tfor _, testCase := range testCases {\n\t\ttestCase := testCase // capture range variable for each test case\n\n\t\tt.Run(testCase.description, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tactualOutput, err := retry.DoWithTimeoutE(t, testCase.description, testCase.timeout, testCase.action)\n\t\t\tif testCase.expectedError != nil {\n\t\t\t\tassert.Equal(t, testCase.expectedError, err)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tassert.Equal(t, expectedOutput, actualOutput)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDoInBackgroundUntilStopped(t *testing.T) {\n\tt.Parallel()\n\n\tsleepBetweenRetries := 2 * time.Second\n\twaitStop := sleepBetweenRetries*2 + sleepBetweenRetries/2\n\tcounter := 0\n\n\tstop := retry.DoInBackgroundUntilStopped(t, t.Name(), sleepBetweenRetries, func() {\n\t\tcounter++\n\t\tt.Log(time.Now(), counter)\n\t})\n\n\ttime.Sleep(waitStop)\n\tstop.Done()\n\tassert.Equal(t, 3, counter)\n\n\ttime.Sleep(waitStop)\n\tassert.Equal(t, 3, counter)\n}\n\nfunc TestDoWithRetryableErrors(t *testing.T) {\n\tt.Parallel()\n\n\texpectedOutput := \"this is the expected output\"\n\texpectedError := errors.New(\"expected error\")\n\tunexpectedError := errors.New(\"some other error\")\n\n\tactionAlwaysReturnsExpected := func() (string, error) { return expectedOutput, nil }\n\tactionAlwaysReturnsExpectedError := func() (string, error) { return expectedOutput, expectedError }\n\tactionAlwaysReturnsUnexpectedError := func() (string, error) { return expectedOutput, unexpectedError }\n\n\tcreateActionThatReturnsExpectedAfterFiveRetriesOfExpectedErrors := func() func() (string, error) {\n\t\tcount := 0\n\n\t\treturn func() (string, error) {\n\t\t\tcount++\n\n\t\t\tif count > 5 {\n\t\t\t\treturn expectedOutput, nil\n\t\t\t}\n\n\t\t\treturn expectedOutput, expectedError\n\t\t}\n\t}\n\n\tcreateActionThatReturnsExpectedAfterFiveRetriesOfUnexpectedErrors := func() func() (string, error) {\n\t\tcount := 0\n\n\t\treturn func() (string, error) {\n\t\t\tcount++\n\n\t\t\tif count > 5 {\n\t\t\t\treturn expectedOutput, nil\n\t\t\t}\n\n\t\t\treturn expectedOutput, unexpectedError\n\t\t}\n\t}\n\n\tcreateActionThatReturnsErrorCounterAfterFiveRetriesOfExpectedErrors := func() func() (string, error) {\n\t\tcount := 0\n\n\t\treturn func() (string, error) {\n\t\t\tcount++\n\n\t\t\tif count > 5 {\n\t\t\t\treturn expectedOutput, ErrorCounter(count)\n\t\t\t}\n\n\t\t\treturn expectedOutput, expectedError\n\t\t}\n\t}\n\n\tmatchAllRegexp := \".*\"\n\tmatchExpectedErrorExactRegexp := expectedError.Error()\n\tmatchExpectedErrorRegexp := \"^expected.*$\"\n\tmatchNothingRegexp1 := \"this won't match any of our errors\"\n\tmatchNothingRegexp2 := \"this also won't match any of our errors\"\n\tmatchStdoutExactlyRegexp := expectedOutput\n\tmatchStdoutRegexp := \"this.*output\"\n\n\tnoRetryableErrors := map[string]string{}\n\tretryOnAllErrors := map[string]string{\n\t\tmatchAllRegexp: \"match all errors\",\n\t}\n\tretryOnExpectedErrorExactMatch := map[string]string{\n\t\tmatchExpectedErrorExactRegexp: \"match expected error exactly\",\n\t}\n\tretryOnExpectedErrorRegexpMatch := map[string]string{\n\t\tmatchExpectedErrorRegexp: \"match expected error using a regex\",\n\t}\n\tretryOnExpectedErrorRegexpMatchWithOthers := map[string]string{\n\t\tmatchNothingRegexp1:      \"unrelated regex that shouldn't match anything\",\n\t\tmatchExpectedErrorRegexp: \"match expected error using a regex\",\n\t\tmatchNothingRegexp2:      \"another unrelated regex that shouldn't match anything\",\n\t}\n\tretryOnErrorsThatWontMatch := map[string]string{\n\t\tmatchNothingRegexp1: \"unrelated regex that shouldn't match anything\",\n\t\tmatchNothingRegexp2: \"another unrelated regex that shouldn't match anything\",\n\t}\n\tretryOnExpectedStdoutExactMatch := map[string]string{\n\t\tmatchStdoutExactlyRegexp: \"match expected stdout exactly\",\n\t}\n\tretryOnExpectedStdoutRegex := map[string]string{\n\t\tmatchStdoutRegexp: \"match expected stdout using a regex\",\n\t}\n\n\ttestCases := []struct {\n\t\texpectedError   error\n\t\tretryableErrors map[string]string\n\t\taction          func() (string, error)\n\t\tdescription     string\n\t\tmaxRetries      int\n\t}{\n\t\t{description: \"Return value on first try\", retryableErrors: noRetryableErrors, maxRetries: 10, action: actionAlwaysReturnsExpected},\n\t\t{description: \"Return expected error, but no retryable errors requested\", retryableErrors: noRetryableErrors, maxRetries: 10, expectedError: retry.FatalError{Underlying: expectedError}, action: actionAlwaysReturnsExpectedError},\n\t\t{description: \"Return expected error, but retryable errors do not match\", retryableErrors: retryOnErrorsThatWontMatch, maxRetries: 10, expectedError: retry.FatalError{Underlying: expectedError}, action: actionAlwaysReturnsExpectedError},\n\t\t{description: \"Return expected error on all retries, use match all regex\", retryableErrors: retryOnAllErrors, maxRetries: 10, expectedError: retry.MaxRetriesExceeded{Description: \"Return expected error on all retries, use match all regex\", MaxRetries: 10}, action: actionAlwaysReturnsExpectedError},\n\t\t{description: \"Return expected error on all retries, use match exactly regex\", retryableErrors: retryOnExpectedErrorExactMatch, maxRetries: 3, expectedError: retry.MaxRetriesExceeded{Description: \"Return expected error on all retries, use match exactly regex\", MaxRetries: 3}, action: actionAlwaysReturnsExpectedError},\n\t\t{description: \"Return expected error on all retries, use regex\", retryableErrors: retryOnExpectedErrorRegexpMatch, maxRetries: 1, expectedError: retry.MaxRetriesExceeded{Description: \"Return expected error on all retries, use regex\", MaxRetries: 1}, action: actionAlwaysReturnsExpectedError},\n\t\t{description: \"Return expected error on all retries, use regex amidst others\", retryableErrors: retryOnExpectedErrorRegexpMatchWithOthers, maxRetries: 1, expectedError: retry.MaxRetriesExceeded{Description: \"Return expected error on all retries, use regex amidst others\", MaxRetries: 1}, action: actionAlwaysReturnsExpectedError},\n\t\t{description: \"Return unexpected error on all retries, but match stdout exactly\", retryableErrors: retryOnExpectedStdoutExactMatch, maxRetries: 10, expectedError: retry.MaxRetriesExceeded{Description: \"Return unexpected error on all retries, but match stdout exactly\", MaxRetries: 10}, action: actionAlwaysReturnsUnexpectedError},\n\t\t{description: \"Return unexpected error on all retries, but match stdout with regex\", retryableErrors: retryOnExpectedStdoutRegex, maxRetries: 3, expectedError: retry.MaxRetriesExceeded{Description: \"Return unexpected error on all retries, but match stdout with regex\", MaxRetries: 3}, action: actionAlwaysReturnsUnexpectedError},\n\t\t{description: \"Return value after 5 retries with expected error, match all\", retryableErrors: retryOnAllErrors, maxRetries: 10, action: createActionThatReturnsExpectedAfterFiveRetriesOfExpectedErrors()},\n\t\t{description: \"Return value after 5 retries with expected error, match exactly\", retryableErrors: retryOnExpectedErrorExactMatch, maxRetries: 10, action: createActionThatReturnsExpectedAfterFiveRetriesOfExpectedErrors()},\n\t\t{description: \"Return value after 5 retries with expected error, match regex\", retryableErrors: retryOnExpectedErrorRegexpMatch, maxRetries: 10, action: createActionThatReturnsExpectedAfterFiveRetriesOfExpectedErrors()},\n\t\t{description: \"Return value after 5 retries with expected error, match multiple regex\", retryableErrors: retryOnExpectedErrorRegexpMatchWithOthers, maxRetries: 10, action: createActionThatReturnsExpectedAfterFiveRetriesOfExpectedErrors()},\n\t\t{description: \"Return value after 5 retries with expected error, match stdout exactly\", retryableErrors: retryOnExpectedStdoutExactMatch, maxRetries: 10, action: createActionThatReturnsExpectedAfterFiveRetriesOfUnexpectedErrors()},\n\t\t{description: \"Return value after 5 retries with expected error, match stdout with regex\", retryableErrors: retryOnExpectedStdoutRegex, maxRetries: 10, action: createActionThatReturnsExpectedAfterFiveRetriesOfUnexpectedErrors()},\n\t\t{description: \"Return value after 5 retries with expected error, match exactly, but only do 4 retries\", retryableErrors: retryOnExpectedErrorExactMatch, maxRetries: 4, expectedError: retry.MaxRetriesExceeded{Description: \"Return value after 5 retries with expected error, match exactly, but only do 4 retries\", MaxRetries: 4}, action: createActionThatReturnsExpectedAfterFiveRetriesOfExpectedErrors()},\n\t\t{description: \"Return unexpected error after 5 retries with expected error, match exactly\", retryableErrors: retryOnExpectedErrorExactMatch, maxRetries: 10, expectedError: retry.FatalError{Underlying: ErrorCounter(6)}, action: createActionThatReturnsErrorCounterAfterFiveRetriesOfExpectedErrors()},\n\t\t{description: \"Return unexpected error after 5 retries with expected error, match regex\", retryableErrors: retryOnExpectedErrorRegexpMatch, maxRetries: 10, expectedError: retry.FatalError{Underlying: ErrorCounter(6)}, action: createActionThatReturnsErrorCounterAfterFiveRetriesOfExpectedErrors()},\n\t\t{description: \"Return unexpected error after 5 retries with expected error, match multiple regex\", retryableErrors: retryOnExpectedErrorRegexpMatchWithOthers, maxRetries: 10, expectedError: retry.FatalError{Underlying: ErrorCounter(6)}, action: createActionThatReturnsErrorCounterAfterFiveRetriesOfExpectedErrors()},\n\t\t{description: \"Return unexpected error after 5 retries with expected error, match all\", retryableErrors: retryOnAllErrors, maxRetries: 10, expectedError: retry.MaxRetriesExceeded{Description: \"Return unexpected error after 5 retries with expected error, match all\", MaxRetries: 10}, action: createActionThatReturnsErrorCounterAfterFiveRetriesOfExpectedErrors()},\n\t}\n\n\tfor _, testCase := range testCases {\n\t\ttestCase := testCase // capture range variable for each test case\n\n\t\tt.Run(testCase.description, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tactualOutput, err := retry.DoWithRetryableErrorsE(t, testCase.description, testCase.retryableErrors, testCase.maxRetries, 1*time.Millisecond, testCase.action)\n\t\t\tassert.Equal(t, expectedOutput, actualOutput)\n\n\t\t\tif testCase.expectedError != nil {\n\t\t\t\tassert.Equal(t, testCase.expectedError, err)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tassert.Equal(t, expectedOutput, actualOutput)\n\t\t\t}\n\t\t})\n\t}\n}\n\ntype ErrorCounter int\n\nfunc (count ErrorCounter) Error() string {\n\treturn strconv.Itoa(int(count))\n}\n"
  },
  {
    "path": "modules/shell/command.go",
    "content": "package shell\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\t\"sync\"\n\t\"syscall\"\n\n\t\"github.com/gruntwork-io/terratest/modules/logger\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// Command is a simpler struct for defining commands than Go's built-in Cmd.\ntype Command struct {\n\t// Use the specified logger for the command's output. Use logger.Discard to not print the output while executing the command.\n\tLogger     *logger.Logger\n\tStdin      io.Reader\n\tEnv        map[string]string // Additional environment variables to set\n\tCommand    string            // The command to run\n\tWorkingDir string            // The working directory\n\tArgs       []string          // The args to pass to the command\n}\n\n// RunCommand runs a shell command and redirects its stdout and stderr to the stdout of the atomic script itself. If\n// there are any errors, fail the test.\n//\n// Deprecated: Use RunCommandContext instead.\n//\n//nolint:gocritic // hugeParam - changing to pointer would break public API\nfunc RunCommand(t testing.TestingT, command Command) {\n\tRunCommandContext(t, context.Background(), &command)\n}\n\n// RunCommandContext is like RunCommand but includes a context.\nfunc RunCommandContext(t testing.TestingT, ctx context.Context, command *Command) {\n\terr := RunCommandContextE(t, ctx, command)\n\trequire.NoError(t, err)\n}\n\n// RunCommandE runs a shell command and redirects its stdout and stderr to the stdout of the atomic script itself. Any\n// returned error will be of type ErrWithCmdOutput, containing the output streams and the underlying error.\n//\n// Deprecated: Use RunCommandContextE instead.\n//\n//nolint:gocritic // hugeParam - changing to pointer would break public API\nfunc RunCommandE(t testing.TestingT, command Command) error {\n\treturn RunCommandContextE(t, context.Background(), &command)\n}\n\n// RunCommandContextE is like RunCommandE but includes a context.\nfunc RunCommandContextE(t testing.TestingT, ctx context.Context, command *Command) error {\n\toutput, err := runCommand(t, ctx, command)\n\tif err != nil {\n\t\treturn &ErrWithCmdOutput{err, output}\n\t}\n\n\treturn nil\n}\n\n// RunCommandAndGetOutput runs a shell command and returns its stdout and stderr as a string. The stdout and stderr of\n// that command will also be logged with Command.Log to make debugging easier. If there are any errors, fail the test.\n//\n// Deprecated: Use RunCommandContextAndGetOutput instead.\n//\n//nolint:gocritic // hugeParam - changing to pointer would break public API\nfunc RunCommandAndGetOutput(t testing.TestingT, command Command) string {\n\treturn RunCommandContextAndGetOutput(t, context.Background(), &command)\n}\n\n// RunCommandContextAndGetOutput is like RunCommandAndGetOutput but includes a context.\nfunc RunCommandContextAndGetOutput(t testing.TestingT, ctx context.Context, command *Command) string {\n\tout, err := RunCommandContextAndGetOutputE(t, ctx, command)\n\trequire.NoError(t, err)\n\n\treturn out\n}\n\n// RunCommandAndGetOutputE runs a shell command and returns its stdout and stderr as a string. The stdout and stderr of\n// that command will also be logged with Command.Log to make debugging easier. Any returned error will be of type\n// ErrWithCmdOutput, containing the output streams and the underlying error.\n//\n// Deprecated: Use RunCommandContextAndGetOutputE instead.\n//\n//nolint:gocritic // hugeParam - changing to pointer would break public API\nfunc RunCommandAndGetOutputE(t testing.TestingT, command Command) (string, error) {\n\treturn RunCommandContextAndGetOutputE(t, context.Background(), &command)\n}\n\n// RunCommandContextAndGetOutputE is like RunCommandAndGetOutputE but includes a context.\nfunc RunCommandContextAndGetOutputE(t testing.TestingT, ctx context.Context, command *Command) (string, error) {\n\toutput, err := runCommand(t, ctx, command)\n\tif err != nil {\n\t\treturn output.Combined(), &ErrWithCmdOutput{err, output}\n\t}\n\n\treturn output.Combined(), nil\n}\n\n// RunCommandAndGetStdOut runs a shell command and returns solely its stdout (but not stderr) as a string. The stdout and\n// stderr of that command will also be logged with Command.Log to make debugging easier. If there are any errors, fail\n// the test.\n//\n// Deprecated: Use RunCommandContextAndGetStdOut instead.\n//\n//nolint:gocritic // hugeParam - changing to pointer would break public API\nfunc RunCommandAndGetStdOut(t testing.TestingT, command Command) string {\n\treturn RunCommandContextAndGetStdOut(t, context.Background(), &command)\n}\n\n// RunCommandContextAndGetStdOut is like RunCommandAndGetStdOut but includes a context.\nfunc RunCommandContextAndGetStdOut(t testing.TestingT, ctx context.Context, command *Command) string {\n\toutput, err := RunCommandContextAndGetStdOutE(t, ctx, command)\n\trequire.NoError(t, err)\n\n\treturn output\n}\n\n// RunCommandAndGetStdOutE runs a shell command and returns solely its stdout (but not stderr) as a string. The stdout\n// and stderr of that command will also be printed to the stdout and stderr of this Go program to make debugging easier.\n// Any returned error will be of type ErrWithCmdOutput, containing the output streams and the underlying error.\n//\n// Deprecated: Use RunCommandContextAndGetStdOutE instead.\n//\n//nolint:gocritic // hugeParam - changing to pointer would break public API\nfunc RunCommandAndGetStdOutE(t testing.TestingT, command Command) (string, error) {\n\treturn RunCommandContextAndGetStdOutE(t, context.Background(), &command)\n}\n\n// RunCommandContextAndGetStdOutE is like RunCommandAndGetStdOutE but includes a context.\nfunc RunCommandContextAndGetStdOutE(t testing.TestingT, ctx context.Context, command *Command) (string, error) {\n\toutput, err := runCommand(t, ctx, command)\n\tif err != nil {\n\t\treturn output.Stdout(), &ErrWithCmdOutput{err, output}\n\t}\n\n\treturn output.Stdout(), nil\n}\n\n// RunCommandAndGetStdOutErr runs a shell command and returns solely its stdout and stderr as a string. The stdout and\n// stderr of that command will also be logged with Command.Log to make debugging easier. If there are any errors, fail\n// the test.\n//\n// Deprecated: Use RunCommandContextAndGetStdOutErr instead.\n//\n//nolint:gocritic // hugeParam - changing to pointer would break public API\nfunc RunCommandAndGetStdOutErr(t testing.TestingT, command Command) (stdout string, stderr string) {\n\treturn RunCommandContextAndGetStdOutErr(t, context.Background(), &command)\n}\n\n// RunCommandContextAndGetStdOutErr is like RunCommandAndGetStdOutErr but includes a context.\nfunc RunCommandContextAndGetStdOutErr(t testing.TestingT, ctx context.Context, command *Command) (stdout string, stderr string) {\n\tstdout, stderr, err := RunCommandContextAndGetStdOutErrE(t, ctx, command)\n\trequire.NoError(t, err)\n\n\treturn stdout, stderr\n}\n\n// RunCommandAndGetStdOutErrE runs a shell command and returns solely its stdout and stderr as a string. The stdout\n// and stderr of that command will also be printed to the stdout and stderr of this Go program to make debugging easier.\n// Any returned error will be of type ErrWithCmdOutput, containing the output streams and the underlying error.\n//\n// Deprecated: Use RunCommandContextAndGetStdOutErrE instead.\n//\n//nolint:gocritic // hugeParam - changing to pointer would break public API\nfunc RunCommandAndGetStdOutErrE(t testing.TestingT, command Command) (stdout string, stderr string, err error) {\n\treturn RunCommandContextAndGetStdOutErrE(t, context.Background(), &command)\n}\n\n// RunCommandContextAndGetStdOutErrE is like RunCommandAndGetStdOutErrE but includes a context.\nfunc RunCommandContextAndGetStdOutErrE(t testing.TestingT, ctx context.Context, command *Command) (stdout string, stderr string, err error) {\n\toutput, err := runCommand(t, ctx, command)\n\tif err != nil {\n\t\treturn output.Stdout(), output.Stderr(), &ErrWithCmdOutput{err, output}\n\t}\n\n\treturn output.Stdout(), output.Stderr(), nil\n}\n\ntype ErrWithCmdOutput struct {\n\tUnderlying error\n\tOutput     *output\n}\n\nfunc (e *ErrWithCmdOutput) Error() string {\n\treturn fmt.Sprintf(\"error while running command: %v; %s\", e.Underlying, e.Output.Stderr())\n}\n\n// runCommand runs a shell command and stores each line from stdout and stderr in Output. Depending on the logger, the\n// stdout and stderr of that command will also be printed to the stdout and stderr of this Go program to make debugging\n// easier.\nfunc runCommand(t testing.TestingT, ctx context.Context, command *Command) (*output, error) {\n\tcommand.Logger.Logf(t, \"Running command %s with args %s\", command.Command, command.Args)\n\n\tcmd := exec.CommandContext(ctx, command.Command, command.Args...)\n\n\tcmd.Dir = command.WorkingDir\n\tif command.Stdin != nil {\n\t\tcmd.Stdin = command.Stdin\n\t} else {\n\t\tcmd.Stdin = os.Stdin\n\t}\n\n\tcmd.Env = formatEnvVars(command)\n\n\tstdout, err := cmd.StdoutPipe()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tstderr, err := cmd.StderrPipe()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\terr = cmd.Start()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\toutput, err := readStdoutAndStderr(t, command.Logger, stdout, stderr)\n\tif err != nil {\n\t\treturn output, err\n\t}\n\n\treturn output, cmd.Wait()\n}\n\n// This function captures stdout and stderr into the given variables while still printing it to the stdout and stderr\n// of this Go program\nfunc readStdoutAndStderr(t testing.TestingT, log *logger.Logger, stdout, stderr io.ReadCloser) (*output, error) {\n\tout := newOutput()\n\tstdoutReader := bufio.NewReader(stdout)\n\tstderrReader := bufio.NewReader(stderr)\n\n\twg := &sync.WaitGroup{}\n\n\twg.Add(2) //nolint:mnd // 2 goroutines: one for stdout, one for stderr\n\n\tvar stdoutErr, stderrErr error\n\n\tgo func() {\n\t\tdefer wg.Done()\n\n\t\tstdoutErr = readData(t, log, stdoutReader, out.stdout)\n\t}()\n\n\tgo func() {\n\t\tdefer wg.Done()\n\n\t\tstderrErr = readData(t, log, stderrReader, out.stderr)\n\t}()\n\n\twg.Wait()\n\n\tif stdoutErr != nil {\n\t\treturn out, stdoutErr\n\t}\n\n\tif stderrErr != nil {\n\t\treturn out, stderrErr\n\t}\n\n\treturn out, nil\n}\n\nfunc readData(t testing.TestingT, log *logger.Logger, reader *bufio.Reader, writer io.StringWriter) error {\n\tvar (\n\t\tline    string\n\t\treadErr error\n\t)\n\n\tfor {\n\t\tline, readErr = reader.ReadString('\\n')\n\n\t\t// remove newline, our output is in a slice,\n\t\t// one element per line.\n\t\tline = strings.TrimSuffix(line, \"\\n\")\n\n\t\t// only return early if the line does not have\n\t\t// any contents. We could have a line that does\n\t\t// not not have a newline before io.EOF, we still\n\t\t// need to add it to the output.\n\t\tif len(line) == 0 && readErr == io.EOF {\n\t\t\tbreak\n\t\t}\n\n\t\t// logger.Logger has a Logf method, but not a Log method.\n\t\t// We have to use the format string indirection to avoid\n\t\t// interpreting any possible formatting characters in\n\t\t// the line.\n\t\t//\n\t\t// See https://github.com/gruntwork-io/terratest/issues/982.\n\t\tlog.Logf(t, \"%s\", line)\n\n\t\tif _, err := writer.WriteString(line); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif readErr != nil {\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif readErr != io.EOF {\n\t\treturn readErr\n\t}\n\n\treturn nil\n}\n\n// GetExitCodeForRunCommandError tries to read the exit code for the error object returned from running a shell command. This is a bit tricky to do\n// in a way that works across platforms.\nfunc GetExitCodeForRunCommandError(err error) (int, error) {\n\tvar errWithOutput *ErrWithCmdOutput\n\tif errors.As(err, &errWithOutput) {\n\t\terr = errWithOutput.Underlying\n\t}\n\n\t// http://stackoverflow.com/a/10385867/483528\n\tvar exitErr *exec.ExitError\n\tif errors.As(err, &exitErr) {\n\t\t// The program has exited with an exit code != 0\n\n\t\t// This works on both Unix and Windows. Although package\n\t\t// syscall is generally platform dependent, WaitStatus is\n\t\t// defined for both Unix and Windows and in both cases has\n\t\t// an ExitStatus() method with the same signature.\n\t\tif status, ok := exitErr.Sys().(syscall.WaitStatus); ok {\n\t\t\treturn status.ExitStatus(), nil\n\t\t}\n\n\t\treturn 1, errors.New(\"could not determine exit code\")\n\t}\n\n\treturn 0, nil\n}\n\nfunc formatEnvVars(command *Command) []string {\n\tenv := os.Environ()\n\tfor key, value := range command.Env {\n\t\tenv = append(env, fmt.Sprintf(\"%s=%s\", key, value))\n\t}\n\n\treturn env\n}\n"
  },
  {
    "path": "modules/shell/command_test.go",
    "content": "package shell_test\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"fmt\"\n\t\"regexp\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/gruntwork-io/terratest/modules/logger\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/gruntwork-io/terratest/modules/shell\"\n)\n\nfunc TestRunCommandAndGetOutput(t *testing.T) {\n\tt.Parallel()\n\n\ttext := \"Hello, World\"\n\tcmd := &shell.Command{\n\t\tCommand: \"echo\",\n\t\tArgs:    []string{text},\n\t}\n\n\tout := shell.RunCommandContextAndGetOutput(t, t.Context(), cmd)\n\tassert.Equal(t, text, strings.TrimSpace(out))\n}\n\nfunc TestRunCommandAndGetOutputOrder(t *testing.T) {\n\tt.Parallel()\n\n\tstderrText := \"Hello, Error\"\n\tstdoutText := \"Hello, World\"\n\texpectedText := \"Hello, Error\\nHello, World\\nHello, Error\\nHello, World\\nHello, Error\\nHello, Error\"\n\tbashCode := fmt.Sprintf(`\necho_stderr(){\n\t(>&2 echo \"%s\")\n\t# Add sleep to stabilize the test\n\tsleep .01s\n}\necho_stdout(){\n\techo \"%s\"\n\t# Add sleep to stabilize the test\n\tsleep .01s\n}\necho_stderr\necho_stdout\necho_stderr\necho_stdout\necho_stderr\necho_stderr\n`,\n\t\tstderrText,\n\t\tstdoutText,\n\t)\n\tcmd := &shell.Command{\n\t\tCommand: \"bash\",\n\t\tArgs:    []string{\"-c\", bashCode},\n\t}\n\n\tout := shell.RunCommandContextAndGetOutput(t, t.Context(), cmd)\n\tassert.Equal(t, expectedText, strings.TrimSpace(out))\n}\n\nfunc TestRunCommandGetExitCode(t *testing.T) {\n\tt.Parallel()\n\n\tcmd := &shell.Command{\n\t\tCommand: \"bash\",\n\t\tArgs:    []string{\"-c\", \"exit 42\"},\n\t\tLogger:  logger.Discard,\n\t}\n\n\tout, err := shell.RunCommandContextAndGetOutputE(t, t.Context(), cmd)\n\tassert.Empty(t, out)\n\trequire.Error(t, err)\n\n\tcode, err := shell.GetExitCodeForRunCommandError(err)\n\trequire.NoError(t, err)\n\tassert.Equal(t, 42, code)\n}\n\nfunc TestRunCommandAndGetOutputConcurrency(t *testing.T) {\n\tt.Parallel()\n\n\tuniqueStderr := random.UniqueID()\n\tuniqueStdout := random.UniqueID()\n\n\tbashCode := fmt.Sprintf(`\necho_stderr(){\n\tsleep .0$[ ( $RANDOM %% 10 ) + 1 ]s\n\t(>&2 echo \"%s\")\n}\necho_stdout(){\n\tsleep .0$[ ( $RANDOM %% 10 ) + 1 ]s\n\techo \"%s\"\n}\nfor i in {1..500}\ndo\n\techo_stderr &\n\techo_stdout &\ndone\nwait\n`,\n\t\tuniqueStderr,\n\t\tuniqueStdout,\n\t)\n\tcmd := &shell.Command{\n\t\tCommand: \"bash\",\n\t\tArgs:    []string{\"-c\", bashCode},\n\t\tLogger:  logger.Discard,\n\t}\n\n\tout := shell.RunCommandContextAndGetOutput(t, t.Context(), cmd)\n\n\tstdoutReg := regexp.MustCompile(uniqueStdout)\n\tstderrReg := regexp.MustCompile(uniqueStderr)\n\n\tassert.Len(t, stdoutReg.FindAllString(out, -1), 500)\n\tassert.Len(t, stderrReg.FindAllString(out, -1), 500)\n}\n\nfunc TestRunCommandWithHugeLineOutput(t *testing.T) {\n\tt.Parallel()\n\n\t// generate a ~100KB line\n\tbashCode := `\nfor i in {0..35000}\ndo\n  echo -n foo\ndone\necho\n`\n\n\tcmd := &shell.Command{\n\t\tCommand: \"bash\",\n\t\tArgs:    []string{\"-c\", bashCode},\n\t\tLogger:  logger.Discard, // don't print that line to stdout\n\t}\n\n\tout, err := shell.RunCommandContextAndGetOutputE(t, t.Context(), cmd)\n\trequire.NoError(t, err)\n\n\tvar buffer bytes.Buffer\n\n\tfor i := 0; i <= 35000; i++ {\n\t\tbuffer.WriteString(\"foo\")\n\t}\n\n\tassert.Equal(t, out, buffer.String())\n}\n\n// TestRunCommandOutputError ensures that getting the output never panics, even if no command was ever run.\nfunc TestRunCommandOutputError(t *testing.T) {\n\tt.Parallel()\n\n\tcmd := &shell.Command{\n\t\tCommand: \"thisbinarydoesnotexistbecausenobodyusesnamesthatlong\",\n\t\tArgs:    []string{\"-no-flag\"},\n\t\tLogger:  logger.Discard,\n\t}\n\n\tout, err := shell.RunCommandContextAndGetOutputE(t, t.Context(), cmd)\n\tassert.Empty(t, out)\n\tassert.Error(t, err)\n}\n\nfunc TestCommandOutputType(t *testing.T) {\n\tt.Parallel()\n\n\tstdout := \"hello world\"\n\tstderr := \"this command has failed\"\n\n\t_, err := shell.RunCommandContextAndGetOutputE(t, t.Context(), &shell.Command{\n\t\tCommand: \"sh\",\n\t\tArgs:    []string{\"-c\", `echo \"` + stdout + `\" && echo \"` + stderr + `\" >&2 && exit 1`},\n\t\tLogger:  logger.Discard,\n\t})\n\tif err != nil {\n\t\tvar o *shell.ErrWithCmdOutput\n\t\tif !errors.As(err, &o) {\n\t\t\tt.Fatalf(\"did not get correct type. got=%T\", err)\n\t\t}\n\n\t\tassert.Len(t, o.Output.Stdout(), len(stdout))\n\t\tassert.Len(t, o.Output.Stderr(), len(stderr))\n\t\tassert.Len(t, o.Output.Combined(), len(stdout)+len(stderr)+1) // +1 for newline\n\t}\n}\n\nfunc TestCommandWithStdoutAndStdErr(t *testing.T) {\n\tt.Parallel()\n\n\tstdout := \"hello world\"\n\tstderr := \"this command has failed\"\n\tcommand := &shell.Command{\n\t\tCommand: \"sh\",\n\t\tArgs:    []string{\"-c\", `echo \"` + stdout + `\" && echo \"` + stderr + `\" >&2`},\n\t\tLogger:  logger.Discard,\n\t}\n\n\tt.Run(\"MustNotError\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tostdout, ostderr := shell.RunCommandContextAndGetStdOutErr(t, t.Context(), command)\n\t\tassert.Equal(t, stdout, ostdout)\n\t\tassert.Equal(t, stderr, ostderr)\n\t})\n\n\tt.Run(\"ReturnError\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tostdout, ostderr, err := shell.RunCommandContextAndGetStdOutErrE(t, t.Context(), command)\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, stdout, ostdout)\n\t\tassert.Equal(t, stderr, ostderr)\n\t})\n}\n\nfunc TestRunCommandWithStdinAndGetOutput(t *testing.T) {\n\tt.Parallel()\n\n\ttext := \"Hello, World\"\n\tcmd := &shell.Command{\n\t\tCommand: \"cat\",\n\t\tStdin:   strings.NewReader(text),\n\t}\n\n\tout := shell.RunCommandContextAndGetOutput(t, t.Context(), cmd)\n\tassert.Equal(t, text, strings.TrimSpace(out))\n}\n"
  },
  {
    "path": "modules/shell/output.go",
    "content": "package shell\n\nimport (\n\t\"strings\"\n\t\"sync\"\n)\n\n// output contains the output after runnig a command.\ntype output struct {\n\tstdout *outputStream\n\tstderr *outputStream\n\t// merged contains stdout  and stderr merged into one stream.\n\tmerged *merged\n}\n\nfunc newOutput() *output {\n\tm := new(merged)\n\n\treturn &output{\n\t\tmerged: m,\n\t\tstdout: &outputStream{\n\t\t\tmerged: m,\n\t\t},\n\t\tstderr: &outputStream{\n\t\t\tmerged: m,\n\t\t},\n\t}\n}\n\nfunc (o *output) Stdout() string {\n\tif o == nil {\n\t\treturn \"\"\n\t}\n\n\treturn o.stdout.String()\n}\n\nfunc (o *output) Stderr() string {\n\tif o == nil {\n\t\treturn \"\"\n\t}\n\n\treturn o.stderr.String()\n}\n\nfunc (o *output) Combined() string {\n\tif o == nil {\n\t\treturn \"\"\n\t}\n\n\treturn o.merged.String()\n}\n\ntype outputStream struct {\n\t*merged\n\tLines []string\n}\n\nfunc (st *outputStream) WriteString(s string) (n int, err error) {\n\tst.Lines = append(st.Lines, s)\n\n\treturn st.merged.WriteString(s)\n}\n\nfunc (st *outputStream) String() string {\n\tif st == nil {\n\t\treturn \"\"\n\t}\n\n\treturn strings.Join(st.Lines, \"\\n\")\n}\n\ntype merged struct {\n\tLines []string\n\t// ensure that there are no parallel writes\n\tsync.Mutex\n}\n\nfunc (m *merged) String() string {\n\tif m == nil {\n\t\treturn \"\"\n\t}\n\n\treturn strings.Join(m.Lines, \"\\n\")\n}\n\nfunc (m *merged) WriteString(s string) (n int, err error) {\n\tm.Lock()\n\tdefer m.Unlock()\n\n\tm.Lines = append(m.Lines, s)\n\n\treturn len(s), nil\n}\n"
  },
  {
    "path": "modules/shell/shell.go",
    "content": "// Package shell allows to run commands in a shell.\npackage shell\n"
  },
  {
    "path": "modules/slack/doc.go",
    "content": "// Package slack contains routines useful for testing slack integrations.\npackage slack\n"
  },
  {
    "path": "modules/slack/validate.go",
    "content": "package slack\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/slack-go/slack\"\n)\n\n// ValidateExpectedSlackMessageE validates whether a message containing the expected text was posted in the given channel\n// ID, looking back historyLimit messages up to the given duration. For example, if you set (15*time.Minute) as the\n// lookBack parameter with historyLimit set to 50, then this will look back the last 50 messages, up to 15 minutes ago.\n// This expects a slack token to be provided. This returns MessageNotFoundErr when there is no match.\n// For the purposes of matching, this only checks the following blocks:\n// - Section block text\n// - Header block text\n// All other blocks are ignored in the validation.\n// NOTE: This only looks for bot posted messages.\nfunc ValidateExpectedSlackMessageE(\n\tt testing.TestingT,\n\ttoken,\n\tchannelID,\n\texpectedText string,\n\thistoryLimit int,\n\tlookBack time.Duration,\n) error {\n\tlookBackTime := time.Now().Add(-1 * lookBack)\n\tslackClt := slack.New(token)\n\tparams := slack.GetConversationHistoryParameters{\n\t\tChannelID: channelID,\n\t\tLimit:     historyLimit,\n\t\tOldest:    strconv.FormatInt(lookBackTime.Unix(), 10),\n\t}\n\n\tresp, err := slackClt.GetConversationHistory(&params)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfor i := range resp.Messages {\n\t\tif checkMessageContainsText(&resp.Messages[i].Msg, expectedText) {\n\t\t\treturn nil\n\t\t}\n\n\t\tif resp.Messages[i].SubMessage != nil {\n\t\t\tif checkMessageContainsText(resp.Messages[i].SubMessage, expectedText) {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t}\n\n\treturn MessageNotFoundErr{}\n}\n\nfunc checkMessageContainsText(msg *slack.Msg, expectedText string) bool {\n\t// If this message is not a bot message, ignore.\n\tif msg.Type != slack.MsgSubTypeBotMessage && msg.BotID == \"\" {\n\t\treturn false\n\t}\n\n\t// Check message text\n\tif strings.Contains(msg.Text, expectedText) {\n\t\treturn true\n\t}\n\n\t// Check attachments\n\tfor i := range msg.Attachments {\n\t\tif strings.Contains(msg.Attachments[i].Text, expectedText) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\t// Check blocks\n\tfor _, block := range msg.Blocks.BlockSet {\n\t\tswitch block.BlockType() {\n\t\tcase slack.MBTSection:\n\t\t\tsectionBlk := block.(*slack.SectionBlock)\n\t\t\tif sectionBlk.Text != nil && strings.Contains(sectionBlk.Text.Text, expectedText) {\n\t\t\t\treturn true\n\t\t\t}\n\t\tcase slack.MBTHeader:\n\t\t\theaderBlk := block.(*slack.HeaderBlock)\n\t\t\tif headerBlk.Text != nil && strings.Contains(headerBlk.Text.Text, expectedText) {\n\t\t\t\treturn true\n\t\t\t}\n\t\tcase slack.MBTDivider, slack.MBTImage, slack.MBTAction, slack.MBTContext,\n\t\t\tslack.MBTFile, slack.MBTInput, slack.MBTRichText, slack.MBTCall, slack.MBTVideo:\n\t\t}\n\t}\n\n\treturn false\n}\n\ntype MessageNotFoundErr struct{}\n\nfunc (err MessageNotFoundErr) Error() string {\n\treturn \"Could not find the expected text in any of the messages posted in the given channel.\"\n}\n"
  },
  {
    "path": "modules/slack/validate_test.go",
    "content": "package slack_test\n\nimport (\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/slack-go/slack\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/gruntwork-io/terratest/modules/environment\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/gruntwork-io/terratest/modules/retry\"\n\tterratestslack \"github.com/gruntwork-io/terratest/modules/slack\"\n)\n\nconst (\n\tslackTokenEnv     = \"SLACK_TOKEN_FOR_TEST\"\n\tslackChannelIDEnv = \"SLACK_CHANNEL_ID_FOR_TEST\"\n)\n\nfunc TestValidateSlackMessage(t *testing.T) {\n\tt.Parallel()\n\n\tenvironment.RequireEnvVar(t, slackTokenEnv)\n\tenvironment.RequireEnvVar(t, slackChannelIDEnv)\n\n\ttoken := os.Getenv(slackTokenEnv)\n\tchannelID := os.Getenv(slackChannelIDEnv)\n\n\tuniqueID := random.UniqueID()\n\tmsgTxt := \"Test message from terratest: \" + uniqueID\n\n\tslackClt := slack.New(token)\n\n\t_, _, err := slackClt.PostMessage(\n\t\tchannelID,\n\t\tslack.MsgOptionText(msgTxt, false),\n\t)\n\trequire.NoError(t, err)\n\n\tretry.DoWithRetry(\n\t\tt,\n\t\t\"wait for slack message\",\n\t\t10, 10*time.Second,\n\t\tfunc() (string, error) {\n\t\t\terr := terratestslack.ValidateExpectedSlackMessageE(t, token, channelID, msgTxt, 10, 5*time.Minute)\n\t\t\treturn \"\", err\n\t\t},\n\t)\n}\n"
  },
  {
    "path": "modules/ssh/agent.go",
    "content": "package ssh\n\nimport (\n\t\"crypto/x509\"\n\t\"encoding/pem\"\n\t\"net\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/gruntwork-io/terratest/modules/logger\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"golang.org/x/crypto/ssh/agent\"\n)\n\ntype SshAgent struct {\n\tstop       chan bool\n\tstopped    chan bool\n\tsocketDir  string\n\tsocketFile string\n\tagent      agent.Agent\n\tln         net.Listener\n}\n\n// Create SSH agent, start it in background and returns control back to the main thread\n// You should stop the agent to cleanup files afterwards by calling `defer s.Stop()`\nfunc NewSshAgent(t testing.TestingT, socketDir string, socketFile string) (*SshAgent, error) {\n\tvar err error\n\ts := &SshAgent{make(chan bool), make(chan bool), socketDir, socketFile, agent.NewKeyring(), nil}\n\ts.ln, err = net.Listen(\"unix\", s.socketFile)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tgo s.run(t)\n\treturn s, nil\n}\n\n// expose socketFile variable\nfunc (s *SshAgent) SocketFile() string {\n\treturn s.socketFile\n}\n\n// SSH Agent listener and handler\nfunc (s *SshAgent) run(t testing.TestingT) {\n\tdefer close(s.stopped)\n\tfor {\n\t\tselect {\n\t\tcase <-s.stop:\n\t\t\treturn\n\t\tdefault:\n\t\t\tc, err := s.ln.Accept()\n\t\t\tif err != nil {\n\t\t\t\tselect {\n\t\t\t\t// When s.Stop() closes the listener, s.ln.Accept() returns an error that can be ignored\n\t\t\t\t// since the agent is in stopping process\n\t\t\t\tcase <-s.stop:\n\t\t\t\t\treturn\n\t\t\t\t\t// When s.ln.Accept() returns a legit error, we print it and continue accepting further requests\n\t\t\t\tdefault:\n\t\t\t\t\tlogger.Default.Logf(t, \"could not accept connection to agent %v\", err)\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tgo func(c net.Conn) {\n\t\t\t\t\tdefer c.Close()\n\t\t\t\t\terr := agent.ServeAgent(s.agent, c)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tlogger.Default.Logf(t, \"could not serve ssh agent %v\", err)\n\t\t\t\t\t}\n\t\t\t\t}(c)\n\t\t\t}\n\t\t}\n\t}\n}\n\n// Stop and clean up SSH agent\nfunc (s *SshAgent) Stop() {\n\tclose(s.stop)\n\ts.ln.Close()\n\t<-s.stopped\n\tos.RemoveAll(s.socketDir)\n}\n\n// Instantiates and returns an in-memory ssh agent with the given KeyPair already added\n// You should stop the agent to cleanup files afterwards by calling `defer sshAgent.Stop()`\nfunc SshAgentWithKeyPair(t testing.TestingT, keyPair *KeyPair) *SshAgent {\n\tsshAgent, err := SshAgentWithKeyPairE(t, keyPair)\n\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\treturn sshAgent\n}\n\nfunc SshAgentWithKeyPairE(t testing.TestingT, keyPair *KeyPair) (*SshAgent, error) {\n\tsshAgent, err := SshAgentWithKeyPairsE(t, []*KeyPair{keyPair})\n\treturn sshAgent, err\n}\n\nfunc SshAgentWithKeyPairs(t testing.TestingT, keyPairs []*KeyPair) *SshAgent {\n\tsshAgent, err := SshAgentWithKeyPairsE(t, keyPairs)\n\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\treturn sshAgent\n}\n\n// Instantiates and returns an in-memory ssh agent with the given KeyPair(s) already added\n// You should stop the agent to cleanup files afterwards by calling `defer sshAgent.Stop()`\nfunc SshAgentWithKeyPairsE(t testing.TestingT, keyPairs []*KeyPair) (*SshAgent, error) {\n\tlogger.Default.Logf(t, \"Generating SSH Agent with given KeyPair(s)\")\n\n\t// Instantiate a temporary SSH agent\n\tsocketDir, err := os.MkdirTemp(\"\", \"ssh-agent-\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tsocketFile := filepath.Join(socketDir, \"ssh_auth.sock\")\n\tsshAgent, err := NewSshAgent(t, socketDir, socketFile)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// add given ssh keys to the newly created agent\n\tfor _, keyPair := range keyPairs {\n\t\t// Create SSH key for the agent using the given SSH key pair(s)\n\t\tblock, _ := pem.Decode([]byte(keyPair.PrivateKey))\n\t\tprivateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tkey := agent.AddedKey{PrivateKey: privateKey}\n\t\tsshAgent.agent.Add(key)\n\t}\n\n\treturn sshAgent, err\n}\n"
  },
  {
    "path": "modules/ssh/agent_test.go",
    "content": "package ssh\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestSshAgentWithKeyPair(t *testing.T) {\n\tt.Parallel()\n\n\tkeyPair := GenerateRSAKeyPair(t, 2048)\n\tsshAgent := SshAgentWithKeyPair(t, keyPair)\n\n\t// ensure that socket directory is set in environment, and it exists\n\tsockFile := filepath.Join(sshAgent.socketDir, \"ssh_auth.sock\")\n\tassert.FileExists(t, sockFile)\n\n\t// assert that there's 1 key in the agent\n\tkeys, err := sshAgent.agent.List()\n\tassert.NoError(t, err)\n\tassert.Len(t, keys, 1)\n\n\tsshAgent.Stop()\n\n\t// is socketDir removed as expected?\n\tif _, err := os.Stat(sshAgent.socketDir); !os.IsNotExist(err) {\n\t\tassert.FailNow(t, \"ssh agent failed to remove socketDir on Stop()\")\n\t}\n}\n\nfunc TestSshAgentWithKeyPairs(t *testing.T) {\n\tt.Parallel()\n\n\tkeyPair := GenerateRSAKeyPair(t, 2048)\n\tkeyPair2 := GenerateRSAKeyPair(t, 2048)\n\tsshAgent := SshAgentWithKeyPairs(t, []*KeyPair{keyPair, keyPair2})\n\tdefer sshAgent.Stop()\n\n\tkeys, err := sshAgent.agent.List()\n\tassert.NoError(t, err)\n\tassert.Len(t, keys, 2)\n}\n\nfunc TestMultipleSshAgents(t *testing.T) {\n\tt.Parallel()\n\n\tkeyPair := GenerateRSAKeyPair(t, 2048)\n\tkeyPair2 := GenerateRSAKeyPair(t, 2048)\n\n\t// start a couple of agents\n\tsshAgent := SshAgentWithKeyPair(t, keyPair)\n\tsshAgent2 := SshAgentWithKeyPair(t, keyPair2)\n\tdefer sshAgent.Stop()\n\tdefer sshAgent2.Stop()\n\n\t// collect public keys from the agents\n\tkeys, err := sshAgent.agent.List()\n\tassert.NoError(t, err)\n\tkeys2, err := sshAgent2.agent.List()\n\tassert.NoError(t, err)\n\n\t// check that all keys match up to expected\n\tassert.NotEqual(t, keys, keys2)\n\tassert.Equal(t, strings.TrimSpace(keyPair.PublicKey), keys[0].String())\n\tassert.Equal(t, strings.TrimSpace(keyPair2.PublicKey), keys2[0].String())\n\n}\n"
  },
  {
    "path": "modules/ssh/key_pair.go",
    "content": "package ssh\n\nimport (\n\t\"crypto/rand\"\n\t\"crypto/rsa\"\n\t\"crypto/x509\"\n\t\"encoding/pem\"\n\n\t\"github.com/gruntwork-io/terratest/modules/logger\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"golang.org/x/crypto/ssh\"\n)\n\n// KeyPair is a public and private key pair that can be used for SSH access.\ntype KeyPair struct {\n\tPublicKey  string\n\tPrivateKey string\n}\n\n// GenerateRSAKeyPair generates an RSA Keypair and return the public and private keys.\nfunc GenerateRSAKeyPair(t testing.TestingT, keySize int) *KeyPair {\n\tkeyPair, err := GenerateRSAKeyPairE(t, keySize)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn keyPair\n}\n\n// GenerateRSAKeyPairE generates an RSA Keypair and return the public and private keys.\nfunc GenerateRSAKeyPairE(t testing.TestingT, keySize int) (*KeyPair, error) {\n\tlogger.Default.Logf(t, \"Generating new public/private key of size %d\", keySize)\n\n\trsaKeyPair, err := rsa.GenerateKey(rand.Reader, keySize)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Extract the private key\n\tkeyPemBlock := &pem.Block{\n\t\tType:  \"RSA PRIVATE KEY\",\n\t\tBytes: x509.MarshalPKCS1PrivateKey(rsaKeyPair),\n\t}\n\n\tkeyPem := string(pem.EncodeToMemory(keyPemBlock))\n\n\t// Extract the public key\n\tsshPubKey, err := ssh.NewPublicKey(rsaKeyPair.Public())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tsshPubKeyBytes := ssh.MarshalAuthorizedKey(sshPubKey)\n\tsshPubKeyStr := string(sshPubKeyBytes)\n\n\t// Return\n\treturn &KeyPair{PublicKey: sshPubKeyStr, PrivateKey: keyPem}, nil\n}\n"
  },
  {
    "path": "modules/ssh/key_pair_test.go",
    "content": "package ssh\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\n// Basic test to ensure we can successfully generate key pairs (no explicit validation for now)\nfunc TestGenerateRSAKeyPair(t *testing.T) {\n\tt.Parallel()\n\n\tkeyPair := GenerateRSAKeyPair(t, 2048)\n\tassert.Contains(t, keyPair.PublicKey, \"ssh-rsa\")\n\tassert.Contains(t, keyPair.PrivateKey, \"-----BEGIN RSA PRIVATE KEY-----\")\n}\n"
  },
  {
    "path": "modules/ssh/session.go",
    "content": "package ssh\n\nimport (\n\t\"io\"\n\t\"net\"\n\t\"reflect\"\n\t\"slices\"\n\t\"strconv\"\n\n\t\"github.com/gruntwork-io/terratest/modules/logger\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"golang.org/x/crypto/ssh\"\n)\n\n// SshConnectionOptions are the options for an SSH connection.\ntype SshConnectionOptions struct {\n\tUsername    string\n\tAddress     string\n\tPort        int\n\tAuthMethods []ssh.AuthMethod\n\tCommand     string\n\tJumpHost    *SshConnectionOptions\n}\n\n// ConnectionString returns the connection string for an SSH connection.\nfunc (options *SshConnectionOptions) ConnectionString() string {\n\treturn net.JoinHostPort(options.Address, strconv.Itoa(options.Port))\n}\n\n// SshSession is a container object for all resources created by an SSH session. The reason we need this is so that we can do a\n// single defer in a top-level method that calls the Cleanup method to go through and ensure all of these resources are\n// released and cleaned up.\ntype SshSession struct {\n\tOptions  *SshConnectionOptions\n\tClient   *ssh.Client\n\tSession  *ssh.Session\n\tJumpHost *JumpHostSession\n\tInput    *func(io.WriteCloser)\n}\n\n// Cleanup cleans up an existing SSH session.\nfunc (sshSession *SshSession) Cleanup(t testing.TestingT) {\n\tif sshSession == nil {\n\t\treturn\n\t}\n\n\t// Closing the session may result in an EOF error if it's already closed (e.g. due to hitting CTRL + D), so\n\t// don't report those errors, as there is nothing actually wrong in that case.\n\tClose(t, sshSession.Session, io.EOF.Error())\n\tClose(t, sshSession.Client)\n\tsshSession.JumpHost.Cleanup(t)\n}\n\n// JumpHostSession is a session with a jump host.\ntype JumpHostSession struct {\n\tJumpHostClient        *ssh.Client\n\tHostVirtualConnection net.Conn\n\tHostConnection        ssh.Conn\n}\n\n// Cleanup cleans the jump host session up.\nfunc (jumpHost *JumpHostSession) Cleanup(t testing.TestingT) {\n\tif jumpHost == nil {\n\t\treturn\n\t}\n\n\t// Closing a connection may result in an EOF error if it's already closed (e.g. due to hitting CTRL + D), so\n\t// don't report those errors, as there is nothing actually wrong in that case.\n\tClose(t, jumpHost.HostConnection, io.EOF.Error())\n\tClose(t, jumpHost.HostVirtualConnection, io.EOF.Error())\n\tClose(t, jumpHost.JumpHostClient)\n}\n\n// Closeable can be closed.\ntype Closeable interface {\n\tClose() error\n}\n\n// Close closes a Closeable.\nfunc Close(t testing.TestingT, closeable Closeable, ignoreErrors ...string) {\n\tif interfaceIsNil(closeable) {\n\t\treturn\n\t}\n\n\tif err := closeable.Close(); err != nil && !slices.Contains(ignoreErrors, err.Error()) {\n\t\tlogger.Default.Logf(t, \"Error closing %s: %s\", closeable, err.Error())\n\t}\n}\n\n// Go is a shitty language. Checking an interface directly against nil does not work, and if you don't know the exact\n// types the interface may be ahead of time, the only way to know if you're dealing with nil is to use reflection.\n// http://stackoverflow.com/questions/13476349/check-for-nil-and-nil-interface-in-go\nfunc interfaceIsNil(i interface{}) bool {\n\treturn i == nil || reflect.ValueOf(i).IsNil()\n}\n"
  },
  {
    "path": "modules/ssh/session_test.go",
    "content": "package ssh\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestSshConnectionOptions_ConnectionString(t *testing.T) {\n\ttype fields struct {\n\t\tAddress string\n\t\tPort    int\n\t}\n\ttests := []struct {\n\t\tname   string\n\t\tfields fields\n\t\twant   string\n\t}{\n\t\t{\n\t\t\tname: \"plain ipv4\",\n\t\t\tfields: fields{\n\t\t\t\tAddress: \"192.168.86.68\",\n\t\t\t\tPort:    22,\n\t\t\t},\n\t\t\twant: \"192.168.86.68:22\",\n\t\t},\n\t\t{\n\t\t\tname: \"plain ipv6\",\n\t\t\tfields: fields{\n\t\t\t\tAddress: \"2001:0db8:85a3:0000:0000:8a2e:0370:7334\",\n\t\t\t\tPort:    22,\n\t\t\t},\n\t\t\twant: \"[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:22\",\n\t\t},\n\t\t{\n\t\t\tname: \"host fqdn\",\n\t\t\tfields: fields{\n\t\t\t\tAddress: \"host.for.test.com\",\n\t\t\t\tPort:    443,\n\t\t\t},\n\t\t\twant: \"host.for.test.com:443\",\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\toptions := &SshConnectionOptions{\n\t\t\t\tAddress: tt.fields.Address,\n\t\t\t\tPort:    tt.fields.Port,\n\t\t\t}\n\t\t\tgot := options.ConnectionString()\n\t\t\trequire.Equal(t, tt.want, got)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "modules/ssh/ssh.go",
    "content": "// Package ssh allows to manage SSH connections and send commands through them.\npackage ssh\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/gruntwork-io/terratest/modules/files\"\n\t\"github.com/gruntwork-io/terratest/modules/logger\"\n\t\"github.com/gruntwork-io/terratest/modules/retry\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/hashicorp/go-multierror\"\n\t\"golang.org/x/crypto/ssh\"\n\t\"golang.org/x/crypto/ssh/agent\"\n)\n\n// Host is a remote host.\ntype Host struct {\n\tHostname    string // host name or ip address\n\tSshUserName string // user name\n\t// set one or more authentication methods,\n\t// the first valid method will be used\n\tSshKeyPair       *KeyPair  // ssh key pair to use as authentication method (disabled by default)\n\tSshAgent         bool      // enable authentication using your existing local SSH agent (disabled by default)\n\tOverrideSshAgent *SshAgent // enable an in process `SshAgent` for connections to this host (disabled by default)\n\tPassword         string    // plain text password (blank by default)\n\tCustomPort       int       // port number to use to connect to the host (port 22 will be used if unset)\n}\n\ntype ScpDownloadOptions struct {\n\tFileNameFilters []string //File names to match. May include bash-style wildcards. E.g., *.log.\n\tMaxFileSizeMB   int      //Don't grab any files > MaxFileSizeMB\n\tRemoteDir       string   //Copy from this directory on the remote machine\n\tLocalDir        string   //Copy RemoteDir to this directory on the local machine\n\tRemoteHost      Host     //Connection information for the remote machine\n}\n\n// ScpFileToE uploads the contents using SCP to the given host and fails the test if the connection fails.\nfunc ScpFileTo(t testing.TestingT, host Host, mode os.FileMode, remotePath, contents string) {\n\terr := ScpFileToE(t, host, mode, remotePath, contents)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n\n// ScpFileToE uploads the contents using SCP to the given host and return an error if the process fails.\nfunc ScpFileToE(t testing.TestingT, host Host, mode os.FileMode, remotePath, contents string) error {\n\tauthMethods, err := createAuthMethodsForHost(host)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdir, file := filepath.Split(remotePath)\n\n\thostOptions := SshConnectionOptions{\n\t\tUsername:    host.SshUserName,\n\t\tAddress:     host.Hostname,\n\t\tPort:        host.getPort(),\n\t\tCommand:     \"/usr/bin/scp -t \" + dir,\n\t\tAuthMethods: authMethods,\n\t}\n\n\tscp := sendScpCommandsToCopyFile(mode, file, contents)\n\n\tsshSession := &SshSession{\n\t\tOptions:  &hostOptions,\n\t\tJumpHost: &JumpHostSession{},\n\t\tInput:    &scp,\n\t}\n\n\tdefer sshSession.Cleanup(t)\n\n\t_, err = runSSHCommand(t, sshSession)\n\treturn err\n}\n\n// ScpFileFrom downloads the file from remotePath on the given host using SCP.\nfunc ScpFileFrom(t testing.TestingT, host Host, remotePath string, localDestination *os.File, useSudo bool) {\n\terr := ScpFileFromE(t, host, remotePath, localDestination, useSudo)\n\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n\n// ScpFileFromE downloads the file from remotePath on the given host using SCP and returns an error if the process fails.\nfunc ScpFileFromE(t testing.TestingT, host Host, remotePath string, localDestination *os.File, useSudo bool) error {\n\tauthMethods, err := createAuthMethodsForHost(host)\n\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdir := filepath.Dir(remotePath)\n\n\thostOptions := SshConnectionOptions{\n\t\tUsername:    host.SshUserName,\n\t\tAddress:     host.Hostname,\n\t\tPort:        host.getPort(),\n\t\tCommand:     \"/usr/bin/scp -t \" + dir,\n\t\tAuthMethods: authMethods,\n\t}\n\n\tsshSession := &SshSession{\n\t\tOptions:  &hostOptions,\n\t\tJumpHost: &JumpHostSession{},\n\t}\n\n\tdefer sshSession.Cleanup(t)\n\n\treturn copyFileFromRemote(t, sshSession, localDestination, remotePath, useSudo)\n}\n\n// ScpDirFrom downloads all the files from remotePath on the given host using SCP.\nfunc ScpDirFrom(t testing.TestingT, options ScpDownloadOptions, useSudo bool) {\n\terr := ScpDirFromE(t, options, useSudo)\n\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n\n// ScpDirFromE downloads all the files from remotePath on the given host using SCP\n// and returns an error if the process fails. NOTE: only files within remotePath will\n// be downloaded. This function will not recursively download subdirectories or follow\n// symlinks.\nfunc ScpDirFromE(t testing.TestingT, options ScpDownloadOptions, useSudo bool) error {\n\tauthMethods, err := createAuthMethodsForHost(options.RemoteHost)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\thostOptions := SshConnectionOptions{\n\t\tUsername:    options.RemoteHost.SshUserName,\n\t\tAddress:     options.RemoteHost.Hostname,\n\t\tPort:        options.RemoteHost.getPort(),\n\t\tCommand:     \"/usr/bin/scp -t \" + options.RemoteDir,\n\t\tAuthMethods: authMethods,\n\t}\n\n\tsshSession := &SshSession{\n\t\tOptions:  &hostOptions,\n\t\tJumpHost: &JumpHostSession{},\n\t}\n\n\tdefer sshSession.Cleanup(t)\n\n\tfilesInDir, err := listFileInRemoteDir(t, sshSession, options, useSudo)\n\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif !files.FileExists(options.LocalDir) {\n\t\terr := os.MkdirAll(options.LocalDir, 0755)\n\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tvar errorsOccurred = new(multierror.Error)\n\n\tfor _, fullRemoteFilePath := range filesInDir {\n\t\tfileName := filepath.Base(fullRemoteFilePath)\n\n\t\tlocalFilePath := filepath.Join(options.LocalDir, fileName)\n\t\tlocalFile, err := os.Create(localFilePath)\n\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tlogger.Default.Logf(t, \"Copying remote file: %s to local path %s\", fullRemoteFilePath, localFilePath)\n\n\t\terr = copyFileFromRemote(t, sshSession, localFile, fullRemoteFilePath, useSudo)\n\t\terrorsOccurred = multierror.Append(errorsOccurred, err)\n\t}\n\n\treturn errorsOccurred.ErrorOrNil()\n}\n\n// CheckSshConnection checks that you can connect via SSH to the given host and fail the test if the connection fails.\nfunc CheckSshConnection(t testing.TestingT, host Host) {\n\terr := CheckSshConnectionE(t, host)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n\n// CheckSshConnectionE checks that you can connect via SSH to the given host and return an error if the connection fails.\nfunc CheckSshConnectionE(t testing.TestingT, host Host) error {\n\t_, err := CheckSshCommandE(t, host, \"'exit'\")\n\treturn err\n}\n\n// CheckSshConnectionWithRetry attempts to connect via SSH until max retries has been exceeded and fails the test\n// if the connection fails\nfunc CheckSshConnectionWithRetry(t testing.TestingT, host Host, retries int, sleepBetweenRetries time.Duration, f ...func(testing.TestingT, Host) error) {\n\thandler := CheckSshConnectionE\n\tif f != nil {\n\t\thandler = f[0]\n\t}\n\terr := CheckSshConnectionWithRetryE(t, host, retries, sleepBetweenRetries, handler)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n\n// CheckSshConnectionWithRetryE attempts to connect via SSH until max retries has been exceeded and returns an error if\n// the connection fails\nfunc CheckSshConnectionWithRetryE(t testing.TestingT, host Host, retries int, sleepBetweenRetries time.Duration, f ...func(testing.TestingT, Host) error) error {\n\thandler := CheckSshConnectionE\n\tif f != nil {\n\t\thandler = f[0]\n\t}\n\t_, err := retry.DoWithRetryE(t, fmt.Sprintf(\"Checking SSH connection to %s\", host.Hostname), retries, sleepBetweenRetries, func() (string, error) {\n\t\treturn \"\", handler(t, host)\n\t})\n\n\treturn err\n}\n\n// CheckSshCommand checks that you can connect via SSH to the given host and run the given command. Returns the stdout/stderr.\nfunc CheckSshCommand(t testing.TestingT, host Host, command string) string {\n\tout, err := CheckSshCommandE(t, host, command)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn out\n}\n\n// CheckSshCommandE checks that you can connect via SSH to the given host and run the given command. Returns the stdout/stderr.\nfunc CheckSshCommandE(t testing.TestingT, host Host, command string) (string, error) {\n\tauthMethods, err := createAuthMethodsForHost(host)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\thostOptions := SshConnectionOptions{\n\t\tUsername:    host.SshUserName,\n\t\tAddress:     host.Hostname,\n\t\tPort:        host.getPort(),\n\t\tCommand:     command,\n\t\tAuthMethods: authMethods,\n\t}\n\n\tsshSession := &SshSession{\n\t\tOptions:  &hostOptions,\n\t\tJumpHost: &JumpHostSession{},\n\t}\n\n\tdefer sshSession.Cleanup(t)\n\n\treturn runSSHCommand(t, sshSession)\n}\n\n// CheckSshCommandWithRetry checks that you can connect via SSH to the given host and run the given command until max retries have been exceeded. Returns the stdout/stderr.\nfunc CheckSshCommandWithRetry(t testing.TestingT, host Host, command string, retries int, sleepBetweenRetries time.Duration, f ...func(testing.TestingT, Host, string) (string, error)) string {\n\thandler := CheckSshCommandE\n\tif f != nil {\n\t\thandler = f[0]\n\t}\n\tout, err := CheckSshCommandWithRetryE(t, host, command, retries, sleepBetweenRetries, handler)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn out\n}\n\n// CheckSshCommandWithRetryE checks that you can connect via SSH to the given host and run the given command until max retries has been exceeded.\n// It return an error if the command fails after max retries has been exceeded.\n\nfunc CheckSshCommandWithRetryE(t testing.TestingT, host Host, command string, retries int, sleepBetweenRetries time.Duration, f ...func(testing.TestingT, Host, string) (string, error)) (string, error) {\n\thandler := CheckSshCommandE\n\tif f != nil {\n\t\thandler = f[0]\n\t}\n\treturn retry.DoWithRetryE(t, fmt.Sprintf(\"Checking SSH connection to %s\", host.Hostname), retries, sleepBetweenRetries, func() (string, error) {\n\t\treturn handler(t, host, command)\n\t})\n}\n\n// CheckPrivateSshConnection attempts to connect to privateHost (which is not addressable from the Internet) via a\n// separate publicHost (which is addressable from the Internet) and then executes \"command\" on privateHost and returns\n// its output. It is useful for checking that it's possible to SSH from a Bastion Host to a private instance.\nfunc CheckPrivateSshConnection(t testing.TestingT, publicHost Host, privateHost Host, command string) string {\n\tout, err := CheckPrivateSshConnectionE(t, publicHost, privateHost, command)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn out\n}\n\n// CheckPrivateSshConnectionE attempts to connect to privateHost (which is not addressable from the Internet) via a\n// separate publicHost (which is addressable from the Internet) and then executes \"command\" on privateHost and returns\n// its output. It is useful for checking that it's possible to SSH from a Bastion Host to a private instance.\nfunc CheckPrivateSshConnectionE(t testing.TestingT, publicHost Host, privateHost Host, command string) (string, error) {\n\tjumpHostAuthMethods, err := createAuthMethodsForHost(publicHost)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tjumpHostOptions := SshConnectionOptions{\n\t\tUsername:    publicHost.SshUserName,\n\t\tAddress:     publicHost.Hostname,\n\t\tPort:        publicHost.getPort(),\n\t\tAuthMethods: jumpHostAuthMethods,\n\t}\n\n\thostAuthMethods, err := createAuthMethodsForHost(privateHost)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\thostOptions := SshConnectionOptions{\n\t\tUsername:    privateHost.SshUserName,\n\t\tAddress:     privateHost.Hostname,\n\t\tPort:        privateHost.getPort(),\n\t\tCommand:     command,\n\t\tAuthMethods: hostAuthMethods,\n\t\tJumpHost:    &jumpHostOptions,\n\t}\n\n\tsshSession := &SshSession{\n\t\tOptions:  &hostOptions,\n\t\tJumpHost: &JumpHostSession{},\n\t}\n\n\tdefer sshSession.Cleanup(t)\n\n\treturn runSSHCommand(t, sshSession)\n}\n\n// FetchContentsOfFiles connects to the given host via SSH and fetches the contents of the files at the given filePaths.\n// If useSudo is true, then the contents will be retrieved using sudo. This method returns a map from file path to\n// contents.\nfunc FetchContentsOfFiles(t testing.TestingT, host Host, useSudo bool, filePaths ...string) map[string]string {\n\tout, err := FetchContentsOfFilesE(t, host, useSudo, filePaths...)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn out\n}\n\n// FetchContentsOfFilesE connects to the given host via SSH and fetches the contents of the files at the given filePaths.\n// If useSudo is true, then the contents will be retrieved using sudo. This method returns a map from file path to\n// contents.\nfunc FetchContentsOfFilesE(t testing.TestingT, host Host, useSudo bool, filePaths ...string) (map[string]string, error) {\n\tfilePathToContents := map[string]string{}\n\n\tfor _, filePath := range filePaths {\n\t\tcontents, err := FetchContentsOfFileE(t, host, useSudo, filePath)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tfilePathToContents[filePath] = contents\n\t}\n\n\treturn filePathToContents, nil\n}\n\n// FetchContentsOfFile connects to the given host via SSH and fetches the contents of the file at the given filePath.\n// If useSudo is true, then the contents will be retrieved using sudo. This method returns the contents of that file.\nfunc FetchContentsOfFile(t testing.TestingT, host Host, useSudo bool, filePath string) string {\n\tout, err := FetchContentsOfFileE(t, host, useSudo, filePath)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn out\n}\n\n// FetchContentsOfFileE connects to the given host via SSH and fetches the contents of the file at the given filePath.\n// If useSudo is true, then the contents will be retrieved using sudo. This method returns the contents of that file.\nfunc FetchContentsOfFileE(t testing.TestingT, host Host, useSudo bool, filePath string) (string, error) {\n\tcommand := fmt.Sprintf(\"cat %s\", filePath)\n\tif useSudo {\n\t\tcommand = fmt.Sprintf(\"sudo %s\", command)\n\t}\n\n\treturn CheckSshCommandE(t, host, command)\n}\n\nfunc listFileInRemoteDir(t testing.TestingT, sshSession *SshSession, options ScpDownloadOptions, useSudo bool) ([]string, error) {\n\tlogger.Default.Logf(t, \"Running command %s on %s@%s\", sshSession.Options.Command, sshSession.Options.Username, sshSession.Options.Address)\n\n\tvar result []string\n\tvar findCommandArgs []string\n\n\tif useSudo {\n\t\tfindCommandArgs = append(findCommandArgs, \"sudo\")\n\t}\n\n\tfindCommandArgs = append(findCommandArgs, \"find\", options.RemoteDir)\n\tfindCommandArgs = append(findCommandArgs, \"-type\", \"f\")\n\n\tfiltersLength := len(options.FileNameFilters)\n\tif options.FileNameFilters != nil && filtersLength > 0 {\n\n\t\tfindCommandArgs = append(findCommandArgs, \"\\\\(\")\n\t\tfor i, curFilter := range options.FileNameFilters {\n\t\t\t// due to inconsistent bash behavior we need to wrap the\n\t\t\t// filter in single quotes\n\t\t\tcurFilter = fmt.Sprintf(\"'%s'\", curFilter)\n\t\t\tfindCommandArgs = append(findCommandArgs, \"-name\", curFilter)\n\n\t\t\t// only add the or flag if we're not the last element\n\t\t\tif filtersLength-i > 1 {\n\t\t\t\tfindCommandArgs = append(findCommandArgs, \"-o\")\n\t\t\t}\n\t\t}\n\t\tfindCommandArgs = append(findCommandArgs, \"\\\\)\")\n\t}\n\n\tif options.MaxFileSizeMB != 0 {\n\t\tfindCommandArgs = append(findCommandArgs, \"-size\", fmt.Sprintf(\"-%dM\", options.MaxFileSizeMB))\n\t}\n\n\tfinalCommandString := strings.Join(findCommandArgs, \" \")\n\tresultString, err := CheckSshCommandE(t, options.RemoteHost, finalCommandString)\n\n\tif err != nil {\n\t\treturn result, err\n\t}\n\n\t// The last character returned is `\\n` this results in an extra \"\" array\n\t// member when we do the split below. Cut off the last character to avoid\n\t// having to remove the blank entry in the array.\n\tresultString = resultString[:len(resultString)-1]\n\n\tresult = append(result, strings.Split(resultString, \"\\n\")...)\n\treturn result, nil\n}\n\n// Added based on code: https://github.com/bramvdbogaerde/go-scp/pull/6/files\nfunc copyFileFromRemote(t testing.TestingT, sshSession *SshSession, file *os.File, remotePath string, useSudo bool) error {\n\tif err := setUpSSHClient(sshSession); err != nil {\n\t\treturn err\n\t}\n\n\tif err := setUpSSHSession(sshSession); err != nil {\n\t\treturn err\n\t}\n\n\tcommand := fmt.Sprintf(\"dd if=%s\", remotePath)\n\tif useSudo {\n\t\tcommand = fmt.Sprintf(\"sudo %s\", command)\n\t}\n\n\tlogger.Default.Logf(t, \"Running command %s on %s@%s\", command, sshSession.Options.Username, sshSession.Options.Address)\n\n\tr, err := sshSession.Session.Output(command)\n\tif err != nil {\n\t\tfmt.Printf(\"error reading from remote stdout: %s\", err)\n\t}\n\tdefer sshSession.Session.Close()\n\t//write to local file\n\t_, err = file.Write(r)\n\n\treturn err\n}\n\nfunc runSSHCommand(t testing.TestingT, sshSession *SshSession) (string, error) {\n\tlogger.Default.Logf(t, \"Running command %s on %s@%s\", sshSession.Options.Command, sshSession.Options.Username, sshSession.Options.Address)\n\tif err := setUpSSHClient(sshSession); err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif err := setUpSSHSession(sshSession); err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif sshSession.Input != nil {\n\t\tw, err := sshSession.Session.StdinPipe()\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\tgo func() {\n\t\t\tdefer w.Close()\n\t\t\t(*sshSession.Input)(w)\n\t\t}()\n\t}\n\n\tbytes, err := sshSession.Session.CombinedOutput(sshSession.Options.Command)\n\tif err != nil {\n\t\treturn string(bytes), err\n\t}\n\n\treturn string(bytes), nil\n}\n\nfunc setUpSSHClient(sshSession *SshSession) error {\n\tif sshSession.Options.JumpHost == nil {\n\t\treturn fillSSHClientForHost(sshSession)\n\t}\n\treturn fillSSHClientForJumpHost(sshSession)\n}\n\nfunc fillSSHClientForHost(sshSession *SshSession) error {\n\tclient, err := createSSHClient(sshSession.Options)\n\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tsshSession.Client = client\n\treturn nil\n}\n\nfunc fillSSHClientForJumpHost(sshSession *SshSession) error {\n\tjumpHostClient, err := createSSHClient(sshSession.Options.JumpHost)\n\tif err != nil {\n\t\treturn err\n\t}\n\tsshSession.JumpHost.JumpHostClient = jumpHostClient\n\n\thostVirtualConn, err := jumpHostClient.Dial(\"tcp\", sshSession.Options.ConnectionString())\n\tif err != nil {\n\t\treturn err\n\t}\n\tsshSession.JumpHost.HostVirtualConnection = hostVirtualConn\n\n\thostConn, hostIncomingChannels, hostIncomingRequests, err := ssh.NewClientConn(hostVirtualConn, sshSession.Options.ConnectionString(), createSSHClientConfig(sshSession.Options))\n\tif err != nil {\n\t\treturn err\n\t}\n\tsshSession.JumpHost.HostConnection = hostConn\n\n\tsshSession.Client = ssh.NewClient(hostConn, hostIncomingChannels, hostIncomingRequests)\n\treturn nil\n}\n\nfunc setUpSSHSession(sshSession *SshSession) error {\n\tsession, err := sshSession.Client.NewSession()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tsshSession.Session = session\n\treturn nil\n}\n\nfunc createSSHClient(options *SshConnectionOptions) (*ssh.Client, error) {\n\tsshClientConfig := createSSHClientConfig(options)\n\treturn ssh.Dial(\"tcp\", options.ConnectionString(), sshClientConfig)\n}\n\nfunc createSSHClientConfig(hostOptions *SshConnectionOptions) *ssh.ClientConfig {\n\tclientConfig := &ssh.ClientConfig{\n\t\tUser: hostOptions.Username,\n\t\tAuth: hostOptions.AuthMethods,\n\t\t// Do not do a host key check, as Terratest is only used for testing, not prod\n\t\tHostKeyCallback: NoOpHostKeyCallback,\n\t\t// By default, Go does not impose a timeout, so a SSH connection attempt can hang for a LONG time.\n\t\tTimeout: 10 * time.Second,\n\t}\n\tclientConfig.SetDefaults()\n\treturn clientConfig\n}\n\n// NoOpHostKeyCallback is an ssh.HostKeyCallback that does nothing. Only use this when you're sure you don't want to check the host key at all\n// (e.g., only for testing and non-production use cases).\nfunc NoOpHostKeyCallback(hostname string, remote net.Addr, key ssh.PublicKey) error {\n\treturn nil\n}\n\n// Returns an array of authentication methods\nfunc createAuthMethodsForHost(host Host) ([]ssh.AuthMethod, error) {\n\tvar methods []ssh.AuthMethod\n\n\t// override local ssh agent with given sshAgent instance\n\tif host.OverrideSshAgent != nil {\n\t\tconn, err := net.Dial(\"unix\", host.OverrideSshAgent.socketFile)\n\t\tif err != nil {\n\t\t\tfmt.Print(\"Failed to dial in memory ssh agent\")\n\t\t\treturn methods, err\n\t\t}\n\t\tagentClient := agent.NewClient(conn)\n\t\tmethods = append(methods, []ssh.AuthMethod{ssh.PublicKeysCallback(agentClient.Signers)}...)\n\t}\n\n\t// use existing ssh agent socket\n\t// if agent authentication is enabled and no agent is set up, returns an error\n\tif host.SshAgent {\n\t\tsocket := os.Getenv(\"SSH_AUTH_SOCK\")\n\t\tconn, err := net.Dial(\"unix\", socket)\n\t\tif err != nil {\n\t\t\treturn methods, err\n\t\t}\n\t\tagentClient := agent.NewClient(conn)\n\t\tmethods = append(methods, []ssh.AuthMethod{ssh.PublicKeysCallback(agentClient.Signers)}...)\n\t}\n\n\t// use provided ssh key pair\n\tif host.SshKeyPair != nil {\n\t\tsigner, err := ssh.ParsePrivateKey([]byte(host.SshKeyPair.PrivateKey))\n\t\tif err != nil {\n\t\t\treturn methods, err\n\t\t}\n\n\t\tpublicKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(host.SshKeyPair.PublicKey))\n\t\tif err != nil {\n\t\t\treturn methods, err\n\t\t}\n\t\tif cert, ok := publicKey.(*ssh.Certificate); ok {\n\t\t\tsigner, err = ssh.NewCertSigner(cert, signer)\n\t\t\tif err != nil {\n\t\t\t\treturn methods, err\n\t\t\t}\n\t\t}\n\n\t\tmethods = append(methods, []ssh.AuthMethod{ssh.PublicKeys(signer)}...)\n\t}\n\n\t// Use given password\n\tif len(host.Password) > 0 {\n\t\tmethods = append(methods, []ssh.AuthMethod{ssh.Password(host.Password)}...)\n\t}\n\n\t// no valid authentication method was provided\n\tif len(methods) < 1 {\n\t\treturn methods, errors.New(\"no authentication method defined\")\n\t}\n\n\treturn methods, nil\n}\n\n// sendScpCommandsToCopyFile returns a function which will send commands to the SCP binary to output a file on the remote machine.\n// A full explanation of the SCP protocol can be found at\n// https://web.archive.org/web/20170215184048/https://blogs.oracle.com/janp/entry/how_the_scp_protocol_works\nfunc sendScpCommandsToCopyFile(mode os.FileMode, fileName, contents string) func(io.WriteCloser) {\n\treturn func(input io.WriteCloser) {\n\n\t\toctalMode := \"0\" + strconv.FormatInt(int64(mode), 8)\n\n\t\t// Create a file at <filename> with Unix permissions set to <octalMost> and the file will be <len(content)> bytes long.\n\t\tfmt.Fprintln(input, \"C\"+octalMode, len(contents), fileName)\n\n\t\t// Actually send the file\n\t\tfmt.Fprint(input, contents)\n\n\t\t// End of transfer\n\t\tfmt.Fprint(input, \"\\x00\")\n\t}\n}\n\n// Gets the port that should be used to communicate with the host\nfunc (h Host) getPort() int {\n\n\t//If a CustomPort is not set use standard ssh port\n\tif h.CustomPort == 0 {\n\t\treturn 22\n\t} else {\n\t\treturn h.CustomPort\n\t}\n}\n"
  },
  {
    "path": "modules/ssh/ssh_test.go",
    "content": "package ssh\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"testing\"\n\n\tgrunttest \"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestHostWithDefaultPort(t *testing.T) {\n\tt.Parallel()\n\n\thost := Host{}\n\n\tassert.Equal(t, 22, host.getPort(), \"host.getPort() did not return the default ssh port of 22\")\n}\n\nfunc TestHostWithCustomPort(t *testing.T) {\n\tt.Parallel()\n\n\tcustomPort := 2222\n\thost := Host{CustomPort: customPort}\n\n\tassert.Equal(t, customPort, host.getPort(), \"host.getPort() did not return the custom port number\")\n}\n\n// global var for use in mock callback\nvar timesCalled int\n\nfunc TestCheckSshConnectionWithRetryE(t *testing.T) {\n\t// Reset the global call count\n\ttimesCalled = 0\n\n\thost := Host{Hostname: \"Host\"}\n\tretries := 10\n\n\tassert.Nil(t, CheckSshConnectionWithRetryE(t, host, retries, 3, mockSshConnectionE))\n}\n\nfunc TestCheckSshConnectionWithRetryEExceedsMaxRetries(t *testing.T) {\n\t// Reset the global call count\n\ttimesCalled = 0\n\n\thost := Host{Hostname: \"Host\"}\n\n\t// Not enough retries\n\tretries := 3\n\n\tassert.Error(t, CheckSshConnectionWithRetryE(t, host, retries, 3, mockSshConnectionE))\n}\n\nfunc TestCheckSshConnectionWithRetry(t *testing.T) {\n\t// Reset the global call count\n\ttimesCalled = 0\n\n\thost := Host{Hostname: \"Host\"}\n\tretries := 10\n\n\tCheckSshConnectionWithRetry(t, host, retries, 3, mockSshConnectionE)\n}\n\nfunc TestCheckSshCommandWithRetryE(t *testing.T) {\n\t// Reset the global call count\n\ttimesCalled = 0\n\n\thost := Host{Hostname: \"Host\"}\n\tcommand := \"echo -n hello world\"\n\tretries := 10\n\n\t_, err := CheckSshCommandWithRetryE(t, host, command, retries, 3, mockSshCommandE)\n\tassert.Nil(t, err)\n}\n\nfunc TestCheckSshCommandWithRetryEExceedsRetries(t *testing.T) {\n\t// Reset the global call count\n\ttimesCalled = 0\n\n\thost := Host{Hostname: \"Host\"}\n\tcommand := \"echo -n hello world\"\n\n\t// Not enough retries\n\tretries := 3\n\n\t_, err := CheckSshCommandWithRetryE(t, host, command, retries, 3, mockSshCommandE)\n\tassert.Error(t, err)\n}\n\nfunc TestCheckSshCommandWithRetry(t *testing.T) {\n\t// Reset the global call count\n\ttimesCalled = 0\n\n\thost := Host{Hostname: \"Host\"}\n\tcommand := \"echo -n hello world\"\n\tretries := 10\n\n\tCheckSshCommandWithRetry(t, host, command, retries, 3, mockSshCommandE)\n}\n\nfunc mockSshConnectionE(t grunttest.TestingT, host Host) error {\n\ttimesCalled += 1\n\tif timesCalled >= 5 {\n\t\treturn nil\n\t} else {\n\t\treturn errors.New(fmt.Sprintf(\"Called %v times\", timesCalled))\n\t}\n}\n\nfunc mockSshCommandE(t grunttest.TestingT, host Host, command string) (string, error) {\n\treturn \"\", mockSshConnectionE(t, host)\n}\n"
  },
  {
    "path": "modules/terraform/apply.go",
    "content": "package terraform\n\nimport (\n\t\"errors\"\n\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// InitAndApply runs terraform init and apply with the given options and return stdout/stderr from the apply command. Note that this\n// method does NOT call destroy and assumes the caller is responsible for cleaning up any resources created by running\n// apply.\nfunc InitAndApply(t testing.TestingT, options *Options) string {\n\tout, err := InitAndApplyE(t, options)\n\trequire.NoError(t, err)\n\treturn out\n}\n\n// InitAndApplyE runs terraform init and apply with the given options and return stdout/stderr from the apply command. Note that this\n// method does NOT call destroy and assumes the caller is responsible for cleaning up any resources created by running\n// apply.\nfunc InitAndApplyE(t testing.TestingT, options *Options) (string, error) {\n\tif _, err := InitE(t, options); err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn ApplyE(t, options)\n}\n\n// Apply runs terraform apply with the given options and return stdout/stderr. Note that this method does NOT call destroy and\n// assumes the caller is responsible for cleaning up any resources created by running apply.\nfunc Apply(t testing.TestingT, options *Options) string {\n\tout, err := ApplyE(t, options)\n\trequire.NoError(t, err)\n\treturn out\n}\n\n// ApplyE runs terraform apply with the given options and return stdout/stderr. Note that this method does NOT call destroy and\n// assumes the caller is responsible for cleaning up any resources created by running apply.\nfunc ApplyE(t testing.TestingT, options *Options) (string, error) {\n\treturn RunTerraformCommandE(t, options, FormatArgs(options, prepend(options.ExtraArgs.Apply, \"apply\", \"-input=false\", \"-auto-approve\")...)...)\n}\n\n// ApplyAndIdempotent runs terraform apply with the given options and return stdout/stderr from the apply command. It then runs\n// plan again and will fail the test if plan requires additional changes. Note that this method does NOT call destroy and assumes\n// the caller is responsible for cleaning up any resources created by running apply.\nfunc ApplyAndIdempotent(t testing.TestingT, options *Options) string {\n\tout, err := ApplyAndIdempotentE(t, options)\n\trequire.NoError(t, err)\n\n\treturn out\n}\n\n// ApplyAndIdempotentE runs terraform apply with the given options and return stdout/stderr from the apply command. It then runs\n// plan again and will fail the test if plan requires additional changes. Note that this method does NOT call destroy and assumes\n// the caller is responsible for cleaning up any resources created by running apply.\nfunc ApplyAndIdempotentE(t testing.TestingT, options *Options) (string, error) {\n\tout, err := ApplyE(t, options)\n\n\tif err != nil {\n\t\treturn out, err\n\t}\n\n\texitCode, err := PlanExitCodeE(t, options)\n\n\tif err != nil {\n\t\treturn out, err\n\t}\n\n\tif exitCode != 0 {\n\t\treturn out, errors.New(\"terraform configuration not idempotent\")\n\t}\n\n\treturn out, nil\n}\n\n// InitAndApplyAndIdempotent runs terraform init and apply with the given options and return stdout/stderr from the apply command. It then runs\n// plan again and will fail the test if plan requires additional changes. Note that this method does NOT call destroy and assumes\n// the caller is responsible for cleaning up any resources created by running apply.\nfunc InitAndApplyAndIdempotent(t testing.TestingT, options *Options) string {\n\tout, err := InitAndApplyAndIdempotentE(t, options)\n\trequire.NoError(t, err)\n\n\treturn out\n}\n\n// InitAndApplyAndIdempotentE runs terraform init and apply with the given options and return stdout/stderr from the apply command. It then runs\n// plan again and will fail the test if plan requires additional changes. Note that this method does NOT call destroy and assumes\n// the caller is responsible for cleaning up any resources created by running apply.\nfunc InitAndApplyAndIdempotentE(t testing.TestingT, options *Options) (string, error) {\n\tif _, err := InitE(t, options); err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn ApplyAndIdempotentE(t, options)\n}\n"
  },
  {
    "path": "modules/terraform/apply_test.go",
    "content": "package terraform\n\nimport (\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/gruntwork-io/terratest/modules/files\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestApplyNoError(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerraformFolderToTemp(\"../../test/fixtures/terraform-no-error\", t.Name())\n\trequire.NoError(t, err)\n\n\toptions := WithDefaultRetryableErrors(t, &Options{\n\t\tTerraformDir: testFolder,\n\t\tNoColor:      true,\n\t})\n\n\tout := InitAndApply(t, options)\n\n\trequire.Contains(t, out, \"Hello, World\")\n\n\t// Check that NoColor correctly doesn't output the colour escape codes which look like [0m,\u001b[1m\u001b or [32m\n\trequire.NotRegexp(t, `\\[\\d*m`, out, \"Output should not contain color escape codes\")\n}\n\nfunc TestApplyWithErrorNoRetry(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerraformFolderToTemp(\"../../test/fixtures/terraform-with-error\", t.Name())\n\trequire.NoError(t, err)\n\n\toptions := WithDefaultRetryableErrors(t, &Options{\n\t\tTerraformDir: testFolder,\n\t})\n\n\tout, err := InitAndApplyE(t, options)\n\n\trequire.Error(t, err)\n\trequire.Contains(t, out, \"This is the first run, exiting with an error\")\n}\n\nfunc TestApplyWithErrorWithRetry(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerraformFolderToTemp(\"../../test/fixtures/terraform-with-error\", t.Name())\n\trequire.NoError(t, err)\n\n\toptions := WithDefaultRetryableErrors(t, &Options{\n\t\tTerraformDir: testFolder,\n\t\tMaxRetries:   1,\n\t\tRetryableTerraformErrors: map[string]string{\n\t\t\t\"This is the first run, exiting with an error\": \"Intentional failure in test fixture\",\n\t\t},\n\t})\n\n\tout := InitAndApply(t, options)\n\n\trequire.Contains(t, out, \"This is the first run, exiting with an error\")\n}\n\nfunc TestApplyWithWarning(t *testing.T) {\n\tscenarios := []struct {\n\t\tname     string\n\t\tfolder   string\n\t\tisError  bool\n\t\twarnings map[string]string\n\t}{\n\t\t{\n\t\t\tname:    \"Warning\",\n\t\t\tfolder:  \"../../test/fixtures/terraform-with-warning\",\n\t\t\tisError: true,\n\t\t\twarnings: map[string]string{\n\t\t\t\t\"lorem ipsum\": \"lorem ipsum warning\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:    \"WarningNotMatch\",\n\t\t\tfolder:  \"../../test/fixtures/terraform-with-warning\",\n\t\t\tisError: false,\n\t\t\twarnings: map[string]string{\n\t\t\t\t\"lorem ipsum dolor sit amet\": \"some warning\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:    \"Error\",\n\t\t\tfolder:  \"../../test/fixtures/terraform-with-error\",\n\t\t\tisError: true,\n\t\t\twarnings: map[string]string{\n\t\t\t\t\"lorem ipsum\": \"lorem ipsum warning\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:    \"NoError\",\n\t\t\tfolder:  \"../../test/fixtures/terraform-no-error\",\n\t\t\tisError: false,\n\t\t\twarnings: map[string]string{\n\t\t\t\t\"lorem ipsum\": \"lorem ipsum warning\",\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, scenario := range scenarios {\n\t\tscenario := scenario\n\t\tt.Run(scenario.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\ttestFolder, err := files.CopyTerraformFolderToTemp(scenario.folder, strings.Replace(t.Name(), \"/\", \"-\", -1))\n\t\t\trequire.NoError(t, err)\n\n\t\t\toptions := WithDefaultRetryableErrors(t, &Options{\n\t\t\t\tTerraformDir:     testFolder,\n\t\t\t\tNoColor:          true,\n\t\t\t\tWarningsAsErrors: scenario.warnings,\n\t\t\t})\n\n\t\t\tout, err := InitAndApplyE(t, options)\n\t\t\tif scenario.isError {\n\t\t\t\tassert.Error(t, err)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t\tassert.NotEmpty(t, out)\n\t\t})\n\t}\n}\n\nfunc TestIdempotentNoChanges(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerraformFolderToTemp(\"../../test/fixtures/terraform-no-error\", t.Name())\n\trequire.NoError(t, err)\n\n\toptions := WithDefaultRetryableErrors(t, &Options{\n\t\tTerraformDir: testFolder,\n\t\tNoColor:      true,\n\t})\n\n\tInitAndApplyAndIdempotentE(t, options)\n}\n\nfunc TestIdempotentWithChanges(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerraformFolderToTemp(\"../../test/fixtures/terraform-not-idempotent\", t.Name())\n\trequire.NoError(t, err)\n\n\toptions := WithDefaultRetryableErrors(t, &Options{\n\t\tTerraformDir: testFolder,\n\t\tNoColor:      true,\n\t})\n\n\tout, err := InitAndApplyAndIdempotentE(t, options)\n\n\trequire.NotEmpty(t, out)\n\trequire.Error(t, err)\n\trequire.EqualError(t, err, \"terraform configuration not idempotent\")\n}\n\nfunc TestParallelism(t *testing.T) {\n\t// This test depends on precise timing of the concurrent parallel calls in terraform, so we need to run this test\n\t// serially by itself so that other concurrent test runs won't influence the timing.\n\n\ttestFolder, err := files.CopyTerraformFolderToTemp(\"../../test/fixtures/terraform-parallelism\", t.Name())\n\trequire.NoError(t, err)\n\n\toptions := WithDefaultRetryableErrors(t, &Options{\n\t\tTerraformDir: testFolder,\n\t\tNoColor:      true,\n\t})\n\n\tInit(t, options)\n\n\t// Run the first time with parallelism set to 5 and it should take about 5 seconds (plus or minus 10 seconds to\n\t// account for other CPU hogging stuff)\n\toptions.Parallelism = 5\n\tstart := time.Now()\n\tApply(t, options)\n\tend := time.Now()\n\trequire.WithinDuration(t, end, start, 15*time.Second)\n\n\t// Run the second time with parallelism set to 1 and it should take at least 25 seconds\n\toptions.Parallelism = 1\n\tstart = time.Now()\n\tApply(t, options)\n\tend = time.Now()\n\tduration := end.Sub(start)\n\trequire.GreaterOrEqual(t, int64(duration.Seconds()), int64(25))\n}\n\nfunc TestApplyWithPlanFile(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerraformFolderToTemp(\"../../test/fixtures/terraform-basic-configuration\", t.Name())\n\trequire.NoError(t, err)\n\tplanFilePath := filepath.Join(testFolder, \"plan.out\")\n\n\toptions := &Options{\n\t\tTerraformDir: testFolder,\n\t\tVars: map[string]interface{}{\n\t\t\t\"cnt\": 1,\n\t\t},\n\t\tNoColor:      true,\n\t\tPlanFilePath: planFilePath,\n\t}\n\t_, err = InitAndPlanE(t, options)\n\trequire.NoError(t, err)\n\trequire.FileExists(t, planFilePath, \"Plan file was not saved to expected location:\", planFilePath)\n\n\tout, err := ApplyE(t, options)\n\trequire.NoError(t, err)\n\trequire.Contains(t, out, \"1 added, 0 changed, 0 destroyed.\")\n\trequire.NotRegexp(t, `\\[\\d*m`, out, \"Output should not contain color escape codes\")\n}\n"
  },
  {
    "path": "modules/terraform/cmd.go",
    "content": "package terraform\n\nimport (\n\t\"fmt\"\n\t\"os/exec\"\n\t\"regexp\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"github.com/gruntwork-io/terratest/modules/retry\"\n\t\"github.com/gruntwork-io/terratest/modules/shell\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc generateCommand(options *Options, args ...string) shell.Command {\n\tcmd := shell.Command{\n\t\tCommand:    options.TerraformBinary,\n\t\tArgs:       args,\n\t\tWorkingDir: options.TerraformDir,\n\t\tEnv:        options.EnvVars,\n\t\tLogger:     options.Logger,\n\t\tStdin:      options.Stdin,\n\t}\n\treturn cmd\n}\n\nvar commandsWithParallelism = []string{\n\t\"plan\",\n\t\"apply\",\n\t\"destroy\",\n}\n\nconst (\n\t// TofuDefaultPath command to run tofu\n\tTofuDefaultPath = \"tofu\"\n\n\t// TerraformDefaultPath to run terraform\n\tTerraformDefaultPath = \"terraform\"\n)\n\nvar DefaultExecutable = defaultTerraformExecutable()\n\n// GetCommonOptions extracts commons terraform options\nfunc GetCommonOptions(options *Options, args ...string) (*Options, []string) {\n\tif options.TerraformBinary == \"\" {\n\t\toptions.TerraformBinary = DefaultExecutable\n\t}\n\n\tif options.Parallelism > 0 && len(args) > 0 && slices.Contains(commandsWithParallelism, args[0]) {\n\t\targs = append(args, fmt.Sprintf(\"--parallelism=%d\", options.Parallelism))\n\t}\n\n\t// if SshAgent is provided, override the local SSH agent with the socket of our in-process agent\n\tif options.SshAgent != nil {\n\t\t// Initialize EnvVars, if it hasn't been set yet\n\t\tif options.EnvVars == nil {\n\t\t\toptions.EnvVars = map[string]string{}\n\t\t}\n\t\toptions.EnvVars[\"SSH_AUTH_SOCK\"] = options.SshAgent.SocketFile()\n\t}\n\treturn options, args\n}\n\n// RunTerraformCommand runs terraform with the given arguments and options and return stdout/stderr.\nfunc RunTerraformCommand(t testing.TestingT, additionalOptions *Options, args ...string) string {\n\tout, err := RunTerraformCommandE(t, additionalOptions, args...)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn out\n}\n\n// RunTerraformCommandE runs terraform with the given arguments and options and return stdout/stderr.\nfunc RunTerraformCommandE(t testing.TestingT, additionalOptions *Options, additionalArgs ...string) (string, error) {\n\toptions, args := GetCommonOptions(additionalOptions, additionalArgs...)\n\n\tcmd := generateCommand(options, args...)\n\tdescription := fmt.Sprintf(\"%s %v\", options.TerraformBinary, args)\n\n\treturn retry.DoWithRetryableErrorsE(t, description, options.RetryableTerraformErrors, options.MaxRetries, options.TimeBetweenRetries, func() (string, error) {\n\t\ts, err := shell.RunCommandAndGetOutputE(t, cmd)\n\t\tif err != nil {\n\t\t\treturn s, err\n\t\t}\n\t\tif err := hasWarning(additionalOptions, s); err != nil {\n\t\t\treturn s, err\n\t\t}\n\t\treturn s, err\n\t})\n\n}\n\n// RunTerraformCommandAndGetStdout runs terraform with the given arguments and options and returns solely its stdout\n// (but not stderr).\nfunc RunTerraformCommandAndGetStdout(t testing.TestingT, additionalOptions *Options, additionalArgs ...string) string {\n\tout, err := RunTerraformCommandAndGetStdoutE(t, additionalOptions, additionalArgs...)\n\trequire.NoError(t, err)\n\treturn out\n}\n\n// RunTerraformCommandAndGetStdoutE runs terraform with the given arguments and options and returns solely its stdout\n// (but not stderr).\nfunc RunTerraformCommandAndGetStdoutE(t testing.TestingT, additionalOptions *Options, additionalArgs ...string) (string, error) {\n\tout, _, _, err := RunTerraformCommandAndGetStdOutErrCodeE(t, additionalOptions, additionalArgs...)\n\treturn out, err\n}\n\n// RunTerraformCommandAndGetStdOutErrCode runs terraform with the given arguments and options and returns its stdout, stderr, and exitcode\nfunc RunTerraformCommandAndGetStdOutErrCode(t testing.TestingT, additionalOptions *Options, additionalArgs ...string) (stdout string, stderr string, exit int) {\n\tstdout, stderr, exit, err := RunTerraformCommandAndGetStdOutErrCodeE(t, additionalOptions, additionalArgs...)\n\trequire.NoError(t, err)\n\treturn stdout, stderr, exit\n}\n\n// RunTerraformCommandAndGetStdOutErrCodeE runs terraform with the given arguments and options and returns its stdout, stderr, and exitcode\nfunc RunTerraformCommandAndGetStdOutErrCodeE(t testing.TestingT, additionalOptions *Options, additionalArgs ...string) (stdout string, stderr string, exit int, err error) {\n\toptions, args := GetCommonOptions(additionalOptions, additionalArgs...)\n\n\tcmd := generateCommand(options, args...)\n\tdescription := fmt.Sprintf(\"%s %v\", options.TerraformBinary, args)\n\n\texit = DefaultErrorExitCode\n\t_, err = retry.DoWithRetryableErrorsE(t, description, options.RetryableTerraformErrors, options.MaxRetries, options.TimeBetweenRetries, func() (string, error) {\n\t\tstdout, stderr, err = shell.RunCommandAndGetStdOutErrE(t, cmd)\n\t\tif err != nil {\n\t\t\texitCode, getExitCodeErr := shell.GetExitCodeForRunCommandError(err)\n\t\t\tif getExitCodeErr == nil {\n\t\t\t\texit = exitCode\n\t\t\t}\n\t\t\treturn \"\", err\n\t\t}\n\n\t\tif err = hasWarning(additionalOptions, stdout); err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\texit = DefaultSuccessExitCode\n\t\treturn \"\", nil\n\t})\n\n\treturn\n}\n\n// GetExitCodeForTerraformCommand runs terraform with the given arguments and options and returns exit code\nfunc GetExitCodeForTerraformCommand(t testing.TestingT, additionalOptions *Options, args ...string) int {\n\texitCode, err := GetExitCodeForTerraformCommandE(t, additionalOptions, args...)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn exitCode\n}\n\n// GetExitCodeForTerraformCommandE runs terraform with the given arguments and options and returns exit code\nfunc GetExitCodeForTerraformCommandE(t testing.TestingT, additionalOptions *Options, additionalArgs ...string) (int, error) {\n\toptions, args := GetCommonOptions(additionalOptions, additionalArgs...)\n\n\tadditionalOptions.Logger.Logf(t, \"Running %s with args %v\", options.TerraformBinary, args)\n\tcmd := generateCommand(options, args...)\n\t_, err := shell.RunCommandAndGetOutputE(t, cmd)\n\tif err == nil {\n\t\treturn DefaultSuccessExitCode, nil\n\t}\n\texitCode, getExitCodeErr := shell.GetExitCodeForRunCommandError(err)\n\tif getExitCodeErr == nil {\n\t\treturn exitCode, nil\n\t}\n\treturn DefaultErrorExitCode, getExitCodeErr\n}\n\nfunc defaultTerraformExecutable() string {\n\tcmd := exec.Command(TerraformDefaultPath, \"-version\")\n\tcmd.Stdin = nil\n\tcmd.Stdout = nil\n\tcmd.Stderr = nil\n\n\tif err := cmd.Run(); err == nil {\n\t\treturn TerraformDefaultPath\n\t}\n\n\t// fallback to Tofu if terraform is not available\n\treturn TofuDefaultPath\n}\n\nfunc hasWarning(opts *Options, out string) error {\n\tfor k, v := range opts.WarningsAsErrors {\n\t\tstr := fmt.Sprintf(\"\\n.*(?i:Warning): %s[^\\n]*\\n\", k)\n\t\tre, err := regexp.Compile(str)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"cannot compile regex for warning detection: %w\", err)\n\t\t}\n\t\tm := re.FindAllString(out, -1)\n\t\tif len(m) == 0 {\n\t\t\tcontinue\n\t\t}\n\t\treturn fmt.Errorf(\"warning(s) were found: %s:\\n%s\", v, strings.Join(m, \"\"))\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "modules/terraform/cmd_test.go",
    "content": "package terraform\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/files\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestTerraformCommand(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"Error\", func(t *testing.T) {\n\t\ttestFolder, err := files.CopyTerraformFolderToTemp(\"../../test/fixtures/terraform-with-error\", strings.ReplaceAll(t.Name(), \"/\", \"-\"))\n\t\trequire.NoError(t, err)\n\t\toptions := &Options{\n\t\t\tTerraformDir: testFolder,\n\t\t}\n\t\tInit(t, options)\n\n\t\tstdout, stderr, code, err := RunTerraformCommandAndGetStdOutErrCodeE(t, options, \"apply\", \"-input=false\", \"-auto-approve\")\n\t\tassert.Error(t, err)\n\t\tassert.Contains(t, stdout, \"Creating...\", \"should capture stdout\")\n\t\tassert.Contains(t, stderr, \"Error: \", \"should capture stderr\")\n\t\tassert.Greater(t, code, 0)\n\t})\n\n\tt.Run(\"WithWarning\", func(t *testing.T) {\n\t\ttestFolder, err := files.CopyTerraformFolderToTemp(\"../../test/fixtures/terraform-with-warning\", strings.ReplaceAll(t.Name(), \"/\", \"-\"))\n\t\trequire.NoError(t, err)\n\t\toptions := &Options{\n\t\t\tTerraformDir: testFolder,\n\t\t\tWarningsAsErrors: map[string]string{\n\t\t\t\t\".*lorem ipsum.*\": \"this warning message should shown.\",\n\t\t\t},\n\t\t}\n\t\tInit(t, options)\n\n\t\tstdout, stderr, code, err := RunTerraformCommandAndGetStdOutErrCodeE(t, options, \"apply\", \"-input=false\", \"-auto-approve\")\n\t\tassert.Error(t, err)\n\t\tassert.Contains(t, stdout, \"Creating...\", \"should capture stdout\")\n\t\tassert.Contains(t, stderr, \"\", \"should capture stderr\")\n\t\tassert.Greater(t, code, 0)\n\t})\n\n\tt.Run(\"NoError\", func(t *testing.T) {\n\t\ttestFolder, err := files.CopyTerraformFolderToTemp(\"../../test/fixtures/terraform-no-error\", strings.ReplaceAll(t.Name(), \"/\", \"-\"))\n\t\trequire.NoError(t, err)\n\t\toptions := &Options{\n\t\t\tTerraformDir: testFolder,\n\t\t}\n\n\t\t{\n\t\t\tstdout, stderr, code := RunTerraformCommandAndGetStdOutErrCode(t, options, \"apply\", \"-input=false\", \"-auto-approve\")\n\t\t\tassert.Contains(t, stdout, `test = \"Hello, World\"`, \"should capture stdout\")\n\t\t\tassert.Equal(t, code, 0)\n\t\t\tassert.Empty(t, stderr)\n\t\t}\n\n\t\t{\n\t\t\tstdout := RunTerraformCommandAndGetStdout(t, options, \"apply\", \"-input=false\", \"-auto-approve\")\n\t\t\tassert.Contains(t, stdout, `test = \"Hello, World\"`, \"should capture stdout\")\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "modules/terraform/count.go",
    "content": "package terraform\n\nimport (\n\t\"errors\"\n\t\"regexp\"\n\t\"strconv\"\n\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// ResourceCount represents counts of resources affected by terraform apply/plan/destroy command.\ntype ResourceCount struct {\n\tAdd     int\n\tChange  int\n\tDestroy int\n}\n\n// Regular expressions for terraform commands stdout pattern matching.\nconst (\n\tapplyRegexp             = `Apply complete! Resources: (\\d+) added, (\\d+) changed, (\\d+) destroyed\\.`\n\tdestroyRegexp           = `Destroy complete! Resources: (\\d+) destroyed\\.`\n\tplanWithChangesRegexp   = `(\\033\\[1m)?Plan:(\\033\\[0m)? (\\d+) to add, (\\d+) to change, (\\d+) to destroy`\n\tplanWithNoChangesRegexp = `No changes\\. (Infrastructure is up-to-date)|(Your infrastructure matches the configuration)\\.`\n\n\t// '.' doesn't match newline by default in go. We must instruct the regex to match it with the 's' flag.\n\tplanWithNoInfraChangesRegexp = `(?s)You can apply this plan.+without changing any real infrastructure`\n)\n\nconst getResourceCountErrMessage = \"Can't parse Terraform output\"\n\n// GetResourceCount parses stdout/stderr of apply/plan/destroy commands and returns number of affected resources.\n// This will fail the test if given stdout/stderr isn't a valid output of apply/plan/destroy.\nfunc GetResourceCount(t testing.TestingT, cmdout string) *ResourceCount {\n\tcnt, err := GetResourceCountE(t, cmdout)\n\trequire.NoError(t, err)\n\treturn cnt\n}\n\n// GetResourceCountE parses stdout/stderr of apply/plan/destroy commands and returns number of affected resources.\nfunc GetResourceCountE(t testing.TestingT, cmdout string) (*ResourceCount, error) {\n\tcnt := ResourceCount{}\n\n\tterraformCommandPatterns := []struct {\n\t\tregexpStr       string\n\t\taddPosition     int\n\t\tchangePosition  int\n\t\tdestroyPosition int\n\t}{\n\t\t{applyRegexp, 1, 2, 3},\n\t\t{destroyRegexp, -1, -1, 1},\n\t\t{planWithChangesRegexp, 3, 4, 5},\n\t\t{planWithNoChangesRegexp, -1, -1, -1},\n\t\t{planWithNoInfraChangesRegexp, -1, -1, -1},\n\t}\n\n\tfor _, tc := range terraformCommandPatterns {\n\t\tpattern, err := regexp.Compile(tc.regexpStr)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tmatches := pattern.FindStringSubmatch(cmdout)\n\t\tif matches != nil {\n\t\t\tif tc.addPosition != -1 {\n\t\t\t\tcnt.Add, err = strconv.Atoi(matches[tc.addPosition])\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif tc.changePosition != -1 {\n\t\t\t\tcnt.Change, err = strconv.Atoi(matches[tc.changePosition])\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif tc.destroyPosition != -1 {\n\t\t\t\tcnt.Destroy, err = strconv.Atoi(matches[tc.destroyPosition])\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn &cnt, nil\n\t\t}\n\t}\n\n\treturn nil, errors.New(getResourceCountErrMessage)\n}\n"
  },
  {
    "path": "modules/terraform/count_test.go",
    "content": "package terraform\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/files\"\n\tttesting \"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestGetResourceCount(t *testing.T) {\n\tt.Parallel()\n\ttestFolder, err := files.CopyTerraformFolderToTemp(\"../../test/fixtures/terraform-basic-configuration\", t.Name())\n\trequire.NoError(t, err)\n\n\tterraformOptions := &Options{\n\t\tTerraformDir: testFolder,\n\t\tVars: map[string]interface{}{\n\t\t\t\"cnt\": 1,\n\t\t},\n\t}\n\n\tcnt := GetResourceCount(t, InitAndPlan(t, terraformOptions))\n\tassert.Equal(t, 1, cnt.Add)\n\tassert.Equal(t, 0, cnt.Change)\n\tassert.Equal(t, 0, cnt.Destroy)\n}\n\nfunc TestGetResourceCountEColor(t *testing.T) {\n\tt.Parallel()\n\trunTestGetResourceCountE(t, false)\n}\n\nfunc TestGetResourceCountENoColor(t *testing.T) {\n\tt.Parallel()\n\trunTestGetResourceCountE(t, true)\n}\n\nfunc runTestGetResourceCountE(t *testing.T, noColor bool) {\n\ttestCases := []struct {\n\t\tName                                         string\n\t\ttfFuncToRun                                  func(t ttesting.TestingT, options *Options) string\n\t\tcntValue                                     int\n\t\texpectedAdd, expectedChange, expectedDestroy int\n\t}{\n\t\t{\"PlanZero\", InitAndPlan, 0, 0, 0, 0},\n\t\t{\"ApplyZero\", InitAndApply, 0, 0, 0, 0},\n\t\t{\"PlanAddResouce\", InitAndPlan, 2, 2, 0, 0},\n\t\t{\"ApplyAddResouce\", InitAndApply, 2, 2, 0, 0},\n\t\t{\"PlanNoOp\", InitAndApply, 2, 0, 0, 0},\n\t\t{\"ApplyNoOp\", InitAndApply, 2, 0, 0, 0},\n\t\t{\"PlanDestroyResource\", InitAndPlan, 1, 0, 0, 1},\n\t\t{\"ApplyDestroyResource\", InitAndApply, 1, 0, 0, 1},\n\t\t{\"Destroy\", Destroy, 1, 0, 0, 1},\n\t\t{\"DestroyNoOp\", Destroy, 1, 0, 0, 0},\n\t}\n\n\ttestFolder, err := files.CopyTerraformFolderToTemp(\"../../test/fixtures/terraform-basic-configuration\", t.Name())\n\trequire.NoError(t, err)\n\n\tterraformOptions := &Options{\n\t\tTerraformDir: testFolder,\n\t\tVars: map[string]interface{}{\n\t\t\t\"cnt\": 0,\n\t\t},\n\t\tNoColor: noColor,\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.Name,\n\t\t\tfunc(t *testing.T) {\n\t\t\t\tterraformOptions.Vars[\"cnt\"] = tc.cntValue\n\t\t\t\tcnt, err := GetResourceCountE(t, tc.tfFuncToRun(t, terraformOptions))\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.Equal(t, tc.expectedAdd, cnt.Add)\n\t\t\t\tassert.Equal(t, tc.expectedChange, cnt.Change)\n\t\t\t\tassert.Equal(t, tc.expectedDestroy, cnt.Destroy)\n\t\t\t})\n\t}\n\n\tt.Run(\"InvalidInput\",\n\t\tfunc(t *testing.T) {\n\t\t\tterraformOptions.Vars[\"cnt\"] = \"abc\"\n\t\t\tcmdout, _ := PlanE(t, terraformOptions)\n\t\t\tcnt, err := GetResourceCountE(t, cmdout)\n\t\t\tassert.EqualError(t, err, getResourceCountErrMessage)\n\t\t\tassert.Nil(t, cnt)\n\t\t})\n\n}\n"
  },
  {
    "path": "modules/terraform/destroy.go",
    "content": "package terraform\n\nimport (\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// Destroy runs terraform destroy with the given options and return stdout/stderr.\nfunc Destroy(t testing.TestingT, options *Options) string {\n\tout, err := DestroyE(t, options)\n\trequire.NoError(t, err)\n\treturn out\n}\n\n// DestroyE runs terraform destroy with the given options and return stdout/stderr.\nfunc DestroyE(t testing.TestingT, options *Options) (string, error) {\n\treturn RunTerraformCommandE(t, options, FormatArgs(options, prepend(options.ExtraArgs.Destroy, \"destroy\", \"-auto-approve\", \"-input=false\")...)...)\n}\n"
  },
  {
    "path": "modules/terraform/errors.go",
    "content": "package terraform\n\nimport (\n\t\"fmt\"\n\t\"reflect\"\n)\n\n// OutputKeyNotFound occurs when terraform output does not contain a value for the key\n// specified in the function call\ntype OutputKeyNotFound string\n\nfunc (err OutputKeyNotFound) Error() string {\n\treturn fmt.Sprintf(\"output doesn't contain a value for the key %q\", string(err))\n}\n\n// OutputValueNotMap occurs when casting a found output value to a map fails\ntype OutputValueNotMap struct {\n\tValue interface{}\n}\n\nfunc (err OutputValueNotMap) Error() string {\n\treturn fmt.Sprintf(\"Output value %q is not a map\", err.Value)\n}\n\n// OutputValueNotList occurs when casting a found output value to a\n// list of interfaces fails\ntype OutputValueNotList struct {\n\tValue interface{}\n}\n\nfunc (err OutputValueNotList) Error() string {\n\treturn fmt.Sprintf(\"Output value %q is not a list\", err.Value)\n}\n\n// EmptyOutput is an error that occurs when an output is empty.\ntype EmptyOutput string\n\nfunc (outputName EmptyOutput) Error() string {\n\treturn fmt.Sprintf(\"Required output %s was empty\", string(outputName))\n}\n\n// UnexpectedOutputType is an error that occurs when the output is not of the type we expect\ntype UnexpectedOutputType struct {\n\tKey          string\n\tExpectedType string\n\tActualType   string\n}\n\nfunc (err UnexpectedOutputType) Error() string {\n\treturn fmt.Sprintf(\"Expected output '%s' to be of type '%s' but got '%s'\", err.Key, err.ExpectedType, err.ActualType)\n}\n\n// VarFileNotFound is an error that occurs when a var file cannot be found in an option's VarFile list\ntype VarFileNotFound struct {\n\tPath string\n}\n\nfunc (err VarFileNotFound) Error() string {\n\treturn fmt.Sprintf(\"Var file '%s' not found\", err.Path)\n}\n\n// InputFileKeyNotFound occurs when tfvar file does not contain a value for the key\n// specified in the function call\ntype InputFileKeyNotFound struct {\n\tFilePath string\n\tKey      string\n}\n\nfunc (err InputFileKeyNotFound) Error() string {\n\treturn fmt.Sprintf(\"tfvar file %q doesn't contain a value for the key %q\", err.FilePath, err.Key)\n}\n\n// PanicWhileParsingVarFile is returned when the HCL parsing routine panics due to errors.\ntype PanicWhileParsingVarFile struct {\n\tConfigFile     string\n\tRecoveredValue interface{}\n}\n\nfunc (err PanicWhileParsingVarFile) Error() string {\n\treturn fmt.Sprintf(\"Recovering panic while parsing '%s'. Got error of type '%v': %v\", err.ConfigFile, reflect.TypeOf(err.RecoveredValue), err.RecoveredValue)\n}\n\n// UnsupportedDefaultWorkspaceDeletion is returned when user tries to delete the workspace \"default\"\ntype UnsupportedDefaultWorkspaceDeletion struct{}\n\nfunc (err *UnsupportedDefaultWorkspaceDeletion) Error() string {\n\treturn \"Deleting the workspace 'default' is not supported\"\n}\n\n// WorkspaceDoesNotExist is returned when user tries to delete a workspace which does not exist\ntype WorkspaceDoesNotExist string\n\nfunc (err WorkspaceDoesNotExist) Error() string {\n\treturn fmt.Sprintf(\"The workspace %q does not exist.\", string(err))\n}\n"
  },
  {
    "path": "modules/terraform/format.go",
    "content": "package terraform\n\nimport (\n\t\"fmt\"\n\t\"reflect\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/gruntwork-io/terratest/internal/lib/formatting\"\n)\n\n// TerraformCommandsWithLockSupport is a list of all the Terraform commands that\n// can obtain locks on Terraform state\nvar TerraformCommandsWithLockSupport = []string{\n\t\"plan\",\n\t\"apply\",\n\t\"destroy\",\n\t\"init\",\n\t\"refresh\",\n\t\"taint\",\n\t\"untaint\",\n\t\"import\",\n}\n\n// TerraformCommandsWithPlanFileSupport is a list of all the Terraform commands that support interacting with plan\n// files.\nvar TerraformCommandsWithPlanFileSupport = []string{\n\t\"plan\",\n\t\"apply\",\n\t\"show\",\n\t\"graph\",\n}\n\n// FormatArgs converts the inputs to a format palatable to terraform. This includes converting the given vars to the\n// format the Terraform CLI expects (-var key=value).\nfunc FormatArgs(options *Options, args ...string) []string {\n\tvar terraformArgs []string\n\tcommandType := args[0]\n\tlockSupported := slices.Contains(TerraformCommandsWithLockSupport, commandType)\n\tplanFileSupported := slices.Contains(TerraformCommandsWithPlanFileSupport, commandType)\n\n\t// Include -var and -var-file flags unless we're running 'apply' with a plan file\n\tincludeVars := !(commandType == \"apply\" && len(options.PlanFilePath) > 0)\n\n\tterraformArgs = append(terraformArgs, args...)\n\n\tif includeVars {\n\t\tfor _, v := range options.MixedVars {\n\t\t\tterraformArgs = append(terraformArgs, v.Args()...)\n\t\t}\n\n\t\tif options.SetVarsAfterVarFiles {\n\t\t\tterraformArgs = append(terraformArgs, FormatTerraformArgs(\"-var-file\", options.VarFiles)...)\n\t\t\tterraformArgs = append(terraformArgs, FormatTerraformVarsAsArgs(options.Vars)...)\n\t\t} else {\n\t\t\tterraformArgs = append(terraformArgs, FormatTerraformVarsAsArgs(options.Vars)...)\n\t\t\tterraformArgs = append(terraformArgs, FormatTerraformArgs(\"-var-file\", options.VarFiles)...)\n\t\t}\n\t}\n\n\tterraformArgs = append(terraformArgs, FormatTerraformArgs(\"-target\", options.Targets)...)\n\n\tif options.NoColor {\n\t\tterraformArgs = append(terraformArgs, \"-no-color\")\n\t}\n\n\tif lockSupported {\n\t\t// If command supports locking, handle lock arguments\n\t\tterraformArgs = append(terraformArgs, FormatTerraformLockAsArgs(options.Lock, options.LockTimeout)...)\n\t}\n\n\tif planFileSupported {\n\t\t// The plan file arg should be last in the terraformArgs slice. Some commands use it as an input (e.g. show, apply)\n\t\tterraformArgs = append(terraformArgs, FormatTerraformPlanFileAsArg(commandType, options.PlanFilePath)...)\n\t}\n\n\treturn terraformArgs\n}\n\n// FormatTerraformPlanFileAsArg formats the out variable as a command-line arg for Terraform (e.g. of the format\n// -out=/some/path/to/plan.out or /some/path/to/plan.out). Only plan supports passing in the plan file as -out; the\n// other commands expect it as the first positional argument. This returns an empty string if outPath is empty string.\nfunc FormatTerraformPlanFileAsArg(commandType string, outPath string) []string {\n\tif outPath == \"\" {\n\t\treturn nil\n\t}\n\tif commandType == \"plan\" {\n\t\treturn []string{fmt.Sprintf(\"%s=%s\", \"-out\", outPath)}\n\t}\n\treturn []string{outPath}\n}\n\n// FormatTerraformVarsAsArgs formats the given variables as command-line args for Terraform (e.g. of the format\n// -var key=value).\nfunc FormatTerraformVarsAsArgs(vars map[string]interface{}) []string {\n\treturn formatTerraformArgs(vars, \"-var\", true, false)\n}\n\n// FormatTerraformLockAsArgs formats the lock and lock-timeout variables\n// -lock, -lock-timeout\nfunc FormatTerraformLockAsArgs(lockCheck bool, lockTimeout string) []string {\n\tlockArgs := []string{fmt.Sprintf(\"-lock=%v\", lockCheck)}\n\tif lockTimeout != \"\" {\n\t\tlockTimeoutValue := fmt.Sprintf(\"%s=%s\", \"-lock-timeout\", lockTimeout)\n\t\tlockArgs = append(lockArgs, lockTimeoutValue)\n\t}\n\treturn lockArgs\n}\n\n// FormatTerraformPluginDirAsArgs formats the plugin-dir variable\n// -plugin-dir\nfunc FormatTerraformPluginDirAsArgs(pluginDir string) []string {\n\treturn formatting.FormatPluginDirAsArgs(pluginDir)\n}\n\n// FormatTerraformArgs will format multiple args with the arg name (e.g. \"-var-file\", []string{\"foo.tfvars\", \"bar.tfvars\", \"baz.tfvars.json\"})\n// returns \"-var-file foo.tfvars -var-file bar.tfvars -var-file baz.tfvars.json\"\nfunc FormatTerraformArgs(argName string, args []string) []string {\n\targsList := []string{}\n\tfor _, argValue := range args {\n\t\targsList = append(argsList, argName, argValue)\n\t}\n\treturn argsList\n}\n\n// FormatTerraformBackendConfigAsArgs formats the given variables as backend config args for Terraform (e.g. of the\n// format -backend-config=key=value).\nfunc FormatTerraformBackendConfigAsArgs(vars map[string]interface{}) []string {\n\treturn formatting.FormatBackendConfigAsArgs(vars)\n}\n\n// Format the given vars into 'Terraform' format, with each var being prefixed with the given prefix. If\n// useSpaceAsSeparator is true, a space will separate the prefix and each var (e.g., -var foo=bar). If\n// useSpaceAsSeparator is false, an equals will separate the prefix and each var (e.g., -backend-config=foo=bar). If\n// omitNil is false, then nil values will be included, (e.g. -backend-config=foo=null). If\n// omitNil is true, then nil values will not be included, (e.g. -backend-config=foo). If\nfunc formatTerraformArgs(vars map[string]interface{}, prefix string, useSpaceAsSeparator bool, omitNil bool) []string {\n\tvar args []string\n\n\tfor key, value := range vars {\n\t\tvar argValue string\n\t\tif omitNil && value == nil {\n\t\t\targValue = key\n\t\t} else {\n\t\t\thclString := toHclString(value, false)\n\t\t\targValue = fmt.Sprintf(\"%s=%s\", key, hclString)\n\t\t}\n\t\tif useSpaceAsSeparator {\n\t\t\targs = append(args, prefix, argValue)\n\t\t} else {\n\t\t\targs = append(args, fmt.Sprintf(\"%s=%s\", prefix, argValue))\n\t\t}\n\t}\n\n\treturn args\n}\n\n// Terraform allows you to pass in command-line variables using HCL syntax (e.g. -var foo=[1,2,3]). Unfortunately,\n// while their golang hcl library can convert an HCL string to a Go type, they don't seem to offer a library to convert\n// arbitrary Go types to an HCL string. Therefore, this method is a simple implementation that correctly handles\n// ints, booleans, lists, and maps. Everything else is forced into a string using Sprintf. Hopefully, this approach is\n// good enough for the type of variables we deal with in Terratest.\nfunc toHclString(value interface{}, isNested bool) string {\n\t// Ideally, we'd use a type switch here to identify slices and maps, but we can't do that, because Go doesn't\n\t// support generics, and the type switch only matches concrete types. So we could match []interface{}, but if\n\t// a user passes in []string{}, that would NOT match (the same logic applies to maps). Therefore, we have to\n\t// use reflection and manually convert into []interface{} and map[string]interface{}.\n\n\tif slice, isSlice := tryToConvertToGenericSlice(value); isSlice {\n\t\treturn sliceToHclString(slice)\n\t} else if m, isMap := tryToConvertToGenericMap(value); isMap {\n\t\treturn mapToHclString(m)\n\t} else {\n\t\treturn primitiveToHclString(value, isNested)\n\t}\n}\n\n// Try to convert the given value to a generic slice. Return the slice and true if the underlying value itself was a\n// slice and an empty slice and false if it wasn't. This is necessary because Go is a shitty language that doesn't\n// have generics, nor useful utility methods built-in. For more info, see: http://stackoverflow.com/a/12754757/483528\nfunc tryToConvertToGenericSlice(value interface{}) ([]interface{}, bool) {\n\treflectValue := reflect.ValueOf(value)\n\tif reflectValue.Kind() != reflect.Slice {\n\t\treturn []interface{}{}, false\n\t}\n\n\tgenericSlice := make([]interface{}, reflectValue.Len())\n\n\tfor i := 0; i < reflectValue.Len(); i++ {\n\t\tgenericSlice[i] = reflectValue.Index(i).Interface()\n\t}\n\n\treturn genericSlice, true\n}\n\n// Try to convert the given value to a generic map. Return the map and true if the underlying value itself was a\n// map and an empty map and false if it wasn't. This is necessary because Go is a shitty language that doesn't\n// have generics, nor useful utility methods built-in. For more info, see: http://stackoverflow.com/a/12754757/483528\nfunc tryToConvertToGenericMap(value interface{}) (map[string]interface{}, bool) {\n\treflectValue := reflect.ValueOf(value)\n\tif reflectValue.Kind() != reflect.Map {\n\t\treturn map[string]interface{}{}, false\n\t}\n\n\treflectType := reflect.TypeOf(value)\n\tif reflectType.Key().Kind() != reflect.String {\n\t\treturn map[string]interface{}{}, false\n\t}\n\n\tgenericMap := make(map[string]interface{}, reflectValue.Len())\n\n\tmapKeys := reflectValue.MapKeys()\n\tfor _, key := range mapKeys {\n\t\tgenericMap[key.String()] = reflectValue.MapIndex(key).Interface()\n\t}\n\n\treturn genericMap, true\n}\n\n// Convert a slice to an HCL string. See ToHclString for details.\nfunc sliceToHclString(slice []interface{}) string {\n\thclValues := []string{}\n\n\tfor _, value := range slice {\n\t\thclValue := toHclString(value, true)\n\t\thclValues = append(hclValues, hclValue)\n\t}\n\n\treturn fmt.Sprintf(\"[%s]\", strings.Join(hclValues, \", \"))\n}\n\n// Convert a map to an HCL string. See ToHclString for details.\nfunc mapToHclString(m map[string]interface{}) string {\n\tkeyValuePairs := []string{}\n\n\tfor key, value := range m {\n\t\tkeyValuePair := fmt.Sprintf(`\"%s\" = %s`, key, toHclString(value, true))\n\t\tkeyValuePairs = append(keyValuePairs, keyValuePair)\n\t}\n\n\treturn fmt.Sprintf(\"{%s}\", strings.Join(keyValuePairs, \", \"))\n}\n\n// Convert a primitive, such as a bool, int, or string, to an HCL string. If this isn't a primitive, force its value\n// using Sprintf. See ToHclString for details.\nfunc primitiveToHclString(value interface{}, isNested bool) string {\n\tif value == nil {\n\t\treturn \"null\"\n\t}\n\n\tswitch v := value.(type) {\n\n\tcase bool:\n\t\treturn strconv.FormatBool(v)\n\n\tcase string:\n\t\t// If string is nested in a larger data structure (e.g. list of string, map of string), ensure value is quoted\n\t\tif isNested {\n\t\t\treturn fmt.Sprintf(\"\\\"%v\\\"\", v)\n\t\t}\n\n\t\treturn fmt.Sprintf(\"%v\", v)\n\n\tdefault:\n\t\treturn fmt.Sprintf(\"%v\", v)\n\t}\n}\n"
  },
  {
    "path": "modules/terraform/format_test.go",
    "content": "package terraform\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestFormatTerraformPlanFileAsArgs(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tcommand  string\n\t\tout      string\n\t\texpected []string\n\t}{\n\t\t{\"plan\", \"/some/plan/output\", []string{\"-out=/some/plan/output\"}},\n\t\t{\"plan\", \"\", nil},\n\t\t{\"apply\", \"/some/plan/output\", []string{\"/some/plan/output\"}},\n\t\t{\"apply\", \"\", nil},\n\t\t{\"show\", \"/some/plan/output\", []string{\"/some/plan/output\"}},\n\t\t{\"show\", \"\", nil},\n\t}\n\n\tfor _, testCase := range testCases {\n\t\tcheckResultWithRetry(t, 100, testCase.expected, fmt.Sprintf(\"FormatTerraformPlanFileAsArgs(%v)\", testCase.out), func() interface{} {\n\t\t\treturn FormatTerraformPlanFileAsArg(testCase.command, testCase.out)\n\t\t})\n\t}\n}\n\nfunc TestFormatTerraformVarsAsArgs(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tvars     map[string]interface{}\n\t\texpected []string\n\t}{\n\t\t{map[string]interface{}{}, nil},\n\t\t{map[string]interface{}{\"foo\": \"bar\"}, []string{\"-var\", \"foo=bar\"}},\n\t\t{map[string]interface{}{\"foo\": 123}, []string{\"-var\", \"foo=123\"}},\n\t\t{map[string]interface{}{\"foo\": true}, []string{\"-var\", \"foo=true\"}},\n\t\t{map[string]interface{}{\"foo\": nil}, []string{\"-var\", \"foo=null\"}},\n\t\t{map[string]interface{}{\"foo\": []int{1, 2, 3}}, []string{\"-var\", \"foo=[1, 2, 3]\"}},\n\t\t{map[string]interface{}{\"foo\": map[string]string{\"baz\": \"blah\"}}, []string{\"-var\", \"foo={\\\"baz\\\" = \\\"blah\\\"}\"}},\n\t\t{\n\t\t\tmap[string]interface{}{\"str\": \"bar\", \"int\": -1, \"bool\": false, \"list\": []string{\"foo\", \"bar\", \"baz\"}, \"map\": map[string]int{\"foo\": 0}},\n\t\t\t[]string{\"-var\", \"str=bar\", \"-var\", \"int=-1\", \"-var\", \"bool=false\", \"-var\", \"list=[\\\"foo\\\", \\\"bar\\\", \\\"baz\\\"]\", \"-var\", \"map={\\\"foo\\\" = 0}\"},\n\t\t},\n\t}\n\n\tfor _, testCase := range testCases {\n\t\tcheckResultWithRetry(t, 100, testCase.expected, fmt.Sprintf(\"FormatTerraformVarsAsArgs(%v)\", testCase.vars), func() interface{} {\n\t\t\treturn FormatTerraformVarsAsArgs(testCase.vars)\n\t\t})\n\t}\n}\n\n// Some of our tests execute code that loops over a map to produce output. The problem is that the order of map\n// iteration is generally unpredictable and, to make it even more unpredictable, Go intentionally randomizes the\n// iteration order (https://blog.golang.org/go-maps-in-action#TOC_7). Therefore, the order of items in the output\n// is unpredictable, and doing a simple assert.Equals call will intermittently fail.\n//\n// We have a few unsatisfactory ways to solve this problem:\n//\n//  1. Enforce iteration order. This is easy to do in other languages, where you have built-in sorted maps. In Go, no\n//     such map exists, and if you create a custom one, you can't use the range keyword on it\n//     (http://stackoverflow.com/a/35810932/483528). As a result, we'd have to modify our implementation code to take\n//     iteration order into account which is a totally unnecessary feature that increases complexity.\n//  2. We could parse the output string and do an order-independent comparison. However, that adds a bunch of parsing\n//     logic into the test code which is a totally unnecessary feature that increases complexity.\n//  3. We accept that Go is a shitty language and, if the test fails, we re-run it a bunch of times in the hope that, if\n//     the bug is caused by key ordering, we will randomly get the proper order in a future run. The code being tested\n//     here is tiny & fast, so doing a hundred retries is still sub millisecond, so while ugly, this provides a very\n//     simple solution.\n//\n// Isn't it great that Go's designers built features into the language to prevent bugs that now force every Go\n// developer to write thousands of lines of extra code like this, which is of course likely to contain bugs itself?\nfunc checkResultWithRetry(t *testing.T, maxRetries int, expectedValue interface{}, description string, generateValue func() interface{}) {\n\tfor i := 0; i < maxRetries; i++ {\n\t\tactualValue := generateValue()\n\t\tif assert.ObjectsAreEqual(expectedValue, actualValue) {\n\t\t\treturn\n\t\t}\n\t\tt.Logf(\"Retry %d of %s failed: expected %v, got %v\", i, description, expectedValue, actualValue)\n\t}\n\n\tassert.Fail(t, \"checkResultWithRetry failed\", \"After %d retries, %s still not succeeding (see retries above)\", description)\n}\n\nfunc TestFormatArgsAppliesLockCorrectly(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tcommand  []string\n\t\texpected []string\n\t}{\n\t\t{[]string{\"plan\"}, []string{\"plan\", \"-lock=false\"}},\n\t\t{[]string{\"validate\"}, []string{\"validate\"}},\n\t\t{[]string{\"validate\", \"--all\"}, []string{\"validate\", \"--all\"}},\n\t\t{[]string{\"plan\", \"--all\"}, []string{\"plan\", \"--all\", \"-lock=false\"}},\n\t}\n\n\tfor _, testCase := range testCases {\n\t\tassert.Equal(t, testCase.expected, FormatArgs(&Options{}, testCase.command...))\n\t}\n}\n\nfunc TestFormatSetVarsAfterVarFilesFormatsCorrectly(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tcommand              []string\n\t\tvars                 map[string]interface{}\n\t\tvarFiles             []string\n\t\tsetVarsAfterVarFiles bool\n\t\texpected             []string\n\t}{\n\t\t{[]string{\"plan\"}, map[string]interface{}{\"foo\": \"bar\"}, []string{\"test.tfvars\"}, true, []string{\"plan\", \"-var-file\", \"test.tfvars\", \"-var\", \"foo=bar\", \"-lock=false\"}},\n\t\t{[]string{\"plan\"}, map[string]interface{}{\"foo\": \"bar\", \"hello\": \"world\"}, []string{\"test.tfvars\"}, true, []string{\"plan\", \"-var-file\", \"test.tfvars\", \"-var\", \"foo=bar\", \"-var\", \"hello=world\", \"-lock=false\"}},\n\t\t{[]string{\"plan\"}, map[string]interface{}{\"foo\": \"bar\", \"hello\": \"world\"}, []string{\"test.tfvars\"}, false, []string{\"plan\", \"-var\", \"foo=bar\", \"-var\", \"hello=world\", \"-var-file\", \"test.tfvars\", \"-lock=false\"}},\n\t\t{[]string{\"plan\"}, map[string]interface{}{\"foo\": \"bar\"}, []string{\"test.tfvars\"}, false, []string{\"plan\", \"-var\", \"foo=bar\", \"-var-file\", \"test.tfvars\", \"-lock=false\"}},\n\t}\n\n\tfor _, testCase := range testCases {\n\t\tresult := FormatArgs(&Options{SetVarsAfterVarFiles: testCase.setVarsAfterVarFiles, Vars: testCase.vars, VarFiles: testCase.varFiles}, testCase.command...)\n\n\t\t// Make sure that -var and -var-file options are in the expected order relative to each other\n\t\t// Note that the order of the different -var and -var-file options may change\n\t\t// See this comment for more info: https://github.com/gruntwork-io/terratest/blob/6fb86056797e3e62ebdd9011ba26605e0976a6f8/modules/terraform/format_test.go#L123-L142\n\t\tfor idx, arg := range result {\n\t\t\tif arg == \"-var-file\" || arg == \"-var\" {\n\t\t\t\tassert.Equal(t, testCase.expected[idx], arg)\n\t\t\t}\n\t\t}\n\n\t\t// Make sure that the order of other arguments hasn't been incorrectly modified\n\t\tassert.Equal(t, testCase.expected[0], result[0])\n\t\tassert.Equal(t, testCase.expected[len(testCase.expected)-1], result[len(result)-1])\n\t}\n}\n\nfunc TestMixedVars(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tcommand              []string\n\t\tmixedVars            []Var\n\t\tvars                 map[string]interface{}\n\t\tvarFiles             []string\n\t\tsetVarsAfterVarFiles bool\n\t\texpected             []string\n\t}{\n\t\t{[]string{\"plan\"}, []Var{VarFile(\"/path1\"), VarInline(\"name\", \"value\"), VarFile(\"/path2\")}, map[string]interface{}{\"foo\": \"bar\"}, []string{\"test.tfvars\"}, true, []string{\"plan\", \"-var-file\", \"/path1\", \"-var\", \"name=value\", \"-var-file\", \"/path2\", \"-var-file\", \"test.tfvars\", \"-var\", \"foo=bar\", \"-lock=false\"}},\n\t\t{[]string{\"plan\"}, []Var{VarInline(\"name1\", \"value\"), VarInline(\"name2\", \"value\"), VarFile(\"/path\")}, map[string]interface{}{\"foo\": \"bar\", \"hello\": \"world\"}, []string{\"test.tfvars\"}, true, []string{\"plan\", \"-var\", \"name1=value\", \"-var\", \"name2=value\", \"-var-file\", \"/path\", \"-var-file\", \"test.tfvars\", \"-var\", \"foo=bar\", \"-var\", \"hello=world\", \"-lock=false\"}},\n\t\t{[]string{\"plan\"}, []Var{VarFile(\"/path\"), VarInline(\"name1\", \"value\"), VarInline(\"name2\", \"value\")}, map[string]interface{}{\"foo\": \"bar\", \"hello\": \"world\"}, []string{\"test.tfvars\"}, false, []string{\"plan\", \"-var-file\", \"path\", \"-var\", \"name1=value\", \"-var\", \"name2=value\", \"-var\", \"foo=bar\", \"-var\", \"hello=world\", \"-var-file\", \"test.tfvars\", \"-lock=false\"}},\n\t\t{[]string{\"plan\"}, []Var{VarFile(\"/path\"), VarInline(\"name\", \"value\")}, map[string]interface{}{\"foo\": \"bar\"}, []string{\"test.tfvars\"}, false, []string{\"plan\", \"-var-file\", \"/path\", \"-var\", \"name=value\", \"-var\", \"foo=bar\", \"-var-file\", \"test.tfvars\", \"-lock=false\"}},\n\t}\n\n\tfor _, testCase := range testCases {\n\t\tresult := FormatArgs(&Options{SetVarsAfterVarFiles: testCase.setVarsAfterVarFiles, Vars: testCase.vars, VarFiles: testCase.varFiles, MixedVars: testCase.mixedVars}, testCase.command...)\n\n\t\t// Make sure that var defined in `MixedVars` are seriliazed in order and precede `Var`` and `VarFiles``\n\t\t// Make sure that -var and -var-file options are in the expected order relative to each other\n\t\t// Note that the order of the different -var and -var-file options may change\n\t\t// See this comment for more info: https://github.com/gruntwork-io/terratest/blob/6fb86056797e3e62ebdd9011ba26605e0976a6f8/modules/terraform/format_test.go#L123-L142\n\t\tfor idx, arg := range result {\n\t\t\tif arg == \"-var-file\" || arg == \"-var\" {\n\t\t\t\tassert.Equal(t, testCase.expected[idx], arg)\n\t\t\t}\n\t\t}\n\n\t\t// Make sure that the order of other arguments hasn't been incorrectly modified\n\t\tassert.Equal(t, testCase.expected[0], result[0])\n\t\tassert.Equal(t, testCase.expected[len(testCase.expected)-1], result[len(result)-1])\n\t}\n}\n"
  },
  {
    "path": "modules/terraform/get.go",
    "content": "package terraform\n\nimport (\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n)\n\n// Get calls terraform get and return stdout/stderr.\nfunc Get(t testing.TestingT, options *Options) string {\n\tout, err := GetE(t, options)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn out\n}\n\n// GetE calls terraform get and return stdout/stderr.\nfunc GetE(t testing.TestingT, options *Options) (string, error) {\n\treturn RunTerraformCommandE(t, options, prepend(options.ExtraArgs.Get, \"get\", \"-update\")...)\n}\n"
  },
  {
    "path": "modules/terraform/init.go",
    "content": "package terraform\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n)\n\n// Init calls terraform init and return stdout/stderr.\nfunc Init(t testing.TestingT, options *Options) string {\n\tout, err := InitE(t, options)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn out\n}\n\n// InitE calls terraform init and return stdout/stderr.\nfunc InitE(t testing.TestingT, options *Options) (string, error) {\n\targs := []string{\"init\", fmt.Sprintf(\"-upgrade=%t\", options.Upgrade)}\n\n\t// Append reconfigure option if specified\n\tif options.Reconfigure {\n\t\targs = append(args, \"-reconfigure\")\n\t}\n\t// Append combination of migrate-state and force-copy to suppress answer prompt\n\tif options.MigrateState {\n\t\targs = append(args, \"-migrate-state\", \"-force-copy\")\n\t}\n\t// Append no-color option if needed\n\tif options.NoColor {\n\t\targs = append(args, \"-no-color\")\n\t}\n\n\targs = append(args, FormatTerraformBackendConfigAsArgs(options.BackendConfig)...)\n\targs = append(args, FormatTerraformPluginDirAsArgs(options.PluginDir)...)\n\treturn RunTerraformCommandE(t, options, prepend(options.ExtraArgs.Init, args...)...)\n}\n"
  },
  {
    "path": "modules/terraform/init_test.go",
    "content": "package terraform\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/files\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestInitBackendConfig(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolderPath := \"../../test/fixtures/terraform-backend\"\n\ttestFolder, err := files.CopyTerraformFolderToTemp(testFolderPath, t.Name())\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\ttmpStateFile := filepath.Join(t.TempDir(), \"backend.tfstate\")\n\tttable := []struct {\n\t\tname    string\n\t\tpath    string\n\t\toptions *Options\n\t}{\n\t\t{\n\t\t\tname: \"KeyValue\",\n\t\t\tpath: tmpStateFile,\n\t\t\toptions: &Options{\n\t\t\t\tTerraformDir: testFolder,\n\t\t\t\tBackendConfig: map[string]interface{}{\n\t\t\t\t\t\"path\": tmpStateFile,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"File\",\n\t\t\tpath: filepath.Join(testFolder, \"backend.tfstate\"),\n\t\t\toptions: &Options{\n\t\t\t\tTerraformDir: testFolder,\n\t\t\t\tReconfigure:  true,\n\t\t\t\tBackendConfig: map[string]interface{}{\n\t\t\t\t\t\"backend.hcl\": nil,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range ttable {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tInitAndApply(t, tt.options)\n\t\t\tassert.FileExists(t, tt.path)\n\t\t})\n\t}\n}\n\nfunc TestInitPluginDir(t *testing.T) {\n\tt.Parallel()\n\n\ttestingDir := t.TempDir()\n\n\tterraformFixture := \"../../test/fixtures/terraform-basic-configuration\"\n\n\tinitializedFolder, err := files.CopyTerraformFolderToTemp(terraformFixture, t.Name())\n\trequire.NoError(t, err)\n\tdefer os.RemoveAll(initializedFolder)\n\n\ttestFolder, err := files.CopyTerraformFolderToTemp(terraformFixture, t.Name())\n\trequire.NoError(t, err)\n\tdefer os.RemoveAll(testFolder)\n\n\tterraformOptions := &Options{\n\t\tTerraformDir: initializedFolder,\n\t}\n\n\tterraformOptionsPluginDir := &Options{\n\t\tTerraformDir: testFolder,\n\t\tPluginDir:    testingDir,\n\t}\n\n\tInit(t, terraformOptions)\n\n\t_, err = InitE(t, terraformOptionsPluginDir)\n\trequire.Error(t, err)\n\n\t// In Terraform 0.13, the directory is \"plugins\"\n\tinitializedPluginDir := initializedFolder + \"/.terraform/plugins\"\n\n\t// In Terraform 0.14, the directory is \"providers\"\n\tinitializedProviderDir := initializedFolder + \"/.terraform/providers\"\n\n\tfiles.CopyFolderContents(initializedPluginDir, testingDir)\n\tfiles.CopyFolderContents(initializedProviderDir, testingDir)\n\n\tinitOutput := Init(t, terraformOptionsPluginDir)\n\n\tassert.Contains(t, initOutput, \"(unauthenticated)\")\n}\n\nfunc TestInitReconfigureBackend(t *testing.T) {\n\tt.Parallel()\n\n\tstateDirectory := t.TempDir()\n\n\ttestFolder, err := files.CopyTerraformFolderToTemp(\"../../test/fixtures/terraform-backend\", t.Name())\n\trequire.NoError(t, err)\n\tdefer os.RemoveAll(testFolder)\n\n\toptions := &Options{\n\t\tTerraformDir: testFolder,\n\t\tBackendConfig: map[string]interface{}{\n\t\t\t\"path\":          filepath.Join(stateDirectory, \"backend.tfstate\"),\n\t\t\t\"workspace_dir\": \"current\",\n\t\t},\n\t}\n\n\tInit(t, options)\n\n\toptions.BackendConfig[\"workspace_dir\"] = \"new\"\n\t_, err = InitE(t, options)\n\tassert.Error(t, err, \"Backend initialization with changed configuration should fail without -reconfigure option\")\n\n\toptions.Reconfigure = true\n\t_, err = InitE(t, options)\n\tassert.NoError(t, err, \"Backend initialization with changed configuration should success with -reconfigure option\")\n}\n\nfunc TestInitBackendMigration(t *testing.T) {\n\tt.Parallel()\n\n\tstateDirectory := t.TempDir()\n\n\ttestFolder, err := files.CopyTerraformFolderToTemp(\"../../test/fixtures/terraform-backend\", t.Name())\n\trequire.NoError(t, err)\n\tdefer os.RemoveAll(testFolder)\n\n\toptions := &Options{\n\t\tTerraformDir: testFolder,\n\t\tBackendConfig: map[string]interface{}{\n\t\t\t\"path\":          filepath.Join(stateDirectory, \"backend.tfstate\"),\n\t\t\t\"workspace_dir\": \"current\",\n\t\t},\n\t}\n\n\tInit(t, options)\n\n\toptions.BackendConfig[\"workspace_dir\"] = \"new\"\n\t_, err = InitE(t, options)\n\tassert.Error(t, err, \"Backend initialization with changed configuration should fail without -migrate-state option\")\n\n\toptions.MigrateState = true\n\t_, err = InitE(t, options)\n\tassert.NoError(t, err, \"Backend initialization with changed configuration should success with -migrate-state option\")\n}\n\nfunc TestInitNoColorOption(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerraformFolderToTemp(\"../../test/fixtures/terraform-no-error\", t.Name())\n\trequire.NoError(t, err)\n\n\toptions := WithDefaultRetryableErrors(t, &Options{\n\t\tTerraformDir: testFolder,\n\t\tNoColor:      true,\n\t})\n\n\tout := InitAndApply(t, options)\n\n\trequire.Contains(t, out, \"Hello, World\")\n\n\t// Check that NoColor correctly doesn't output the colour escape codes which look like [0m,\u001b[1m\u001b or [32m\n\trequire.NotRegexp(t, `\\[\\d*m`, out, \"Output should not contain color escape codes\")\n}\n"
  },
  {
    "path": "modules/terraform/opa_check.go",
    "content": "package terraform\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/hashicorp/go-multierror\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/tmccombs/hcl2json/convert\"\n\n\t\"github.com/gruntwork-io/terratest/modules/files\"\n\t\"github.com/gruntwork-io/terratest/modules/opa\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n)\n\n// OPAEval runs `opa eval` with the given option on the terraform files identified in the TerraformDir directory of the\n// Options struct. Note that since OPA does not natively support parsing HCL code, we first convert all the files to\n// JSON prior to passing it through OPA. This function fails the test if there is an error.\nfunc OPAEval(\n\tt testing.TestingT,\n\ttfOptions *Options,\n\topaEvalOptions *opa.EvalOptions,\n\tresultQuery string,\n) {\n\trequire.NoError(t, OPAEvalE(t, tfOptions, opaEvalOptions, resultQuery))\n}\n\n// OPAEvalE runs `opa eval` with the given option on the terraform files identified in the TerraformDir directory of the\n// Options struct. Note that since OPA does not natively support parsing HCL code, we first convert all the files to\n// JSON prior to passing it through OPA.\nfunc OPAEvalE(\n\tt testing.TestingT,\n\ttfOptions *Options,\n\topaEvalOptions *opa.EvalOptions,\n\tresultQuery string,\n) error {\n\ttfOptions.Logger.Logf(t, \"Running terraform files in %s through `opa eval` on policy %s\", tfOptions.TerraformDir, opaEvalOptions.RulePath)\n\n\t// Find all the tf files in the terraform dir to process.\n\ttfFiles, err := files.FindTerraformSourceFilesInDir(tfOptions.TerraformDir)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Create a temporary dir to store all the json files\n\ttmpDir, err := os.MkdirTemp(\"\", \"terratest-opa-hcl2json-*\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !opaEvalOptions.DebugKeepTempFiles {\n\t\tdefer os.RemoveAll(tmpDir)\n\t}\n\ttfOptions.Logger.Logf(t, \"Using temporary folder %s for json representation of terraform module %s\", tmpDir, tfOptions.TerraformDir)\n\n\t// Convert all the found tf files to json format so OPA works.\n\tjsonFiles := make([]string, len(tfFiles))\n\terrorsOccurred := new(multierror.Error)\n\tfor i, tfFile := range tfFiles {\n\t\ttfFileBase := filepath.Base(tfFile)\n\t\ttfFileBaseName := strings.TrimSuffix(tfFileBase, filepath.Ext(tfFileBase))\n\t\toutPath := filepath.Join(tmpDir, tfFileBaseName+\".json\")\n\t\ttfOptions.Logger.Logf(t, \"Converting %s to json %s\", tfFile, outPath)\n\t\tif err := HCLFileToJSONFile(tfFile, outPath); err != nil {\n\t\t\terrorsOccurred = multierror.Append(errorsOccurred, err)\n\t\t}\n\t\tjsonFiles[i] = outPath\n\t}\n\tif err := errorsOccurred.ErrorOrNil(); err != nil {\n\t\treturn err\n\t}\n\n\t// Run OPA checks on each of the converted json files.\n\treturn opa.EvalE(t, opaEvalOptions, jsonFiles, resultQuery)\n}\n\n// HCLFileToJSONFile is a function that takes a path containing HCL code, and converts it to JSON representation and\n// writes out the contents to the given path.\nfunc HCLFileToJSONFile(hclPath, jsonOutPath string) error {\n\tfileBytes, err := os.ReadFile(hclPath)\n\tif err != nil {\n\t\treturn err\n\t}\n\tconverted, err := convert.Bytes(fileBytes, hclPath, convert.Options{})\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn os.WriteFile(jsonOutPath, converted, 0600)\n}\n"
  },
  {
    "path": "modules/terraform/options.go",
    "content": "package terraform\n\nimport (\n\t\"io\"\n\t\"time\"\n\n\t\"github.com/gruntwork-io/terratest/modules/logger\"\n\t\"github.com/gruntwork-io/terratest/modules/ssh\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/jinzhu/copier\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nvar (\n\tDefaultRetryableTerraformErrors = map[string]string{\n\t\t// Helm related terraform calls may fail when too many tests run in parallel. While the exact cause is unknown,\n\t\t// this is presumably due to all the network contention involved. Usually a retry resolves the issue.\n\t\t\".*read: connection reset by peer.*\": \"Failed to reach helm charts repository.\",\n\t\t\".*transport is closing.*\":           \"Failed to reach Kubernetes API.\",\n\n\t\t// `terraform init` frequently fails in CI due to network issues accessing plugins. The reason is unknown, but\n\t\t// eventually these succeed after a few retries.\n\t\t\".*unable to verify signature.*\":                  \"Failed to retrieve plugin due to transient network error.\",\n\t\t\".*unable to verify checksum.*\":                   \"Failed to retrieve plugin due to transient network error.\",\n\t\t\".*no provider exists with the given name.*\":      \"Failed to retrieve plugin due to transient network error.\",\n\t\t\".*registry service is unreachable.*\":             \"Failed to retrieve plugin due to transient network error.\",\n\t\t\".*Error installing provider.*\":                   \"Failed to retrieve plugin due to transient network error.\",\n\t\t\".*Failed to query available provider packages.*\": \"Failed to retrieve plugin due to transient network error.\",\n\t\t\".*timeout while waiting for plugin to start.*\":   \"Failed to retrieve plugin due to transient network error.\",\n\t\t\".*timed out waiting for server handshake.*\":      \"Failed to retrieve plugin due to transient network error.\",\n\t\t\"could not query provider registry for\":           \"Failed to retrieve plugin due to transient network error.\",\n\n\t\t// Provider bugs where the data after apply is not propagated. This is usually an eventual consistency issue, so\n\t\t// retrying should self resolve it.\n\t\t// See https://github.com/terraform-providers/terraform-provider-aws/issues/12449 for an example.\n\t\t\".*Provider produced inconsistent result after apply.*\": \"Provider eventual consistency error.\",\n\t}\n)\n\n// Options for running Terraform commands\ntype Options struct {\n\tTerraformBinary string // Name of the binary that will be used\n\tTerraformDir    string // The path to the folder where the Terraform code is defined.\n\n\t// The vars to pass to Terraform commands using the -var option. Note that terraform does not support passing `null`\n\t// as a variable value through the command line. That is, if you use `map[string]interface{}{\"foo\": nil}` as `Vars`,\n\t// this will translate to the string literal `\"null\"` being assigned to the variable `foo`. However, nulls in\n\t// lists and maps/objects are supported. E.g., the following var will be set as expected (`{ bar = null }`:\n\t// map[string]interface{}{\n\t//     \"foo\": map[string]interface{}{\"bar\": nil},\n\t// }\n\tVars map[string]interface{}\n\n\tVarFiles                 []string               // The var file paths to pass to Terraform commands using -var-file option.\n\tMixedVars                []Var                  // Mix of `-var` and `-var-file` in arbritrary order, use `VarInline()` `VarFile()` to set the value.\n\tTargets                  []string               // The target resources to pass to the terraform command with -target\n\tLock                     bool                   // The lock option to pass to the terraform command with -lock\n\tLockTimeout              string                 // The lock timeout option to pass to the terraform command with -lock-timeout\n\tEnvVars                  map[string]string      // Environment variables to set when running Terraform\n\tBackendConfig            map[string]interface{} // The vars to pass to the terraform init command for extra configuration for the backend. If a var is nil, it will be formated as `--backend-config=var` instead of `--backend-config=var=null`\n\tRetryableTerraformErrors map[string]string      // If Terraform apply fails with one of these (transient) errors, retry. The keys are a regexp to match against the error and the message is what to display to a user if that error is matched.\n\tMaxRetries               int                    // Maximum number of times to retry errors matching RetryableTerraformErrors\n\tTimeBetweenRetries       time.Duration          // The amount of time to wait between retries\n\tUpgrade                  bool                   // Whether the -upgrade flag of the terraform init command should be set to true or not\n\tReconfigure              bool                   // Set the -reconfigure flag to the terraform init command\n\tMigrateState             bool                   // Set the -migrate-state and -force-copy (suppress 'yes' answer prompt) flag to the terraform init command\n\tNoColor                  bool                   // Whether the -no-color flag will be set for any Terraform command or not\n\tSshAgent                 *ssh.SshAgent          // Overrides local SSH agent with the given in-process agent\n\tNoStderr                 bool                   // Disable stderr redirection\n\tOutputMaxLineSize        int                    // The max size of one line in stdout and stderr (in bytes)\n\tLogger                   *logger.Logger         // Set a non-default logger that should be used. See the logger package for more info.\n\tParallelism              int                    // Set the parallelism setting for Terraform\n\tPlanFilePath             string                 // The path to output a plan file to (for the plan command) or read one from (for the apply command)\n\tPluginDir                string                 // The path of downloaded plugins to pass to the terraform init command (-plugin-dir)\n\tSetVarsAfterVarFiles     bool                   // Pass -var options after -var-file options to Terraform commands\n\tWarningsAsErrors         map[string]string      // Terraform warning messages that should be treated as errors. The keys are a regexp to match against the warning and the value is what to display to a user if that warning is matched.\n\tExtraArgs                ExtraArgs              // Extra arguments passed to Terraform commands\n\tStdin                    io.Reader              // Optional stdin to pass to Terraform commands\n}\n\ntype ExtraArgs struct {\n\tApply           []string\n\tDestroy         []string\n\tGet             []string\n\tInit            []string\n\tPlan            []string\n\tValidate        []string\n\tWorkspaceDelete []string\n\tWorkspaceSelect []string\n\tWorkspaceNew    []string\n\tOutput          []string\n\tShow            []string\n}\n\nfunc prepend(args []string, arg ...string) []string {\n\treturn append(arg, args...)\n}\n\n// Clone makes a deep copy of most fields on the Options object and returns it.\n//\n// NOTE: options.SshAgent and options.Logger CANNOT be deep copied (e.g., the SshAgent struct contains channels and\n// listeners that can't be meaningfully copied), so the original values are retained.\nfunc (options *Options) Clone() (*Options, error) {\n\tnewOptions := &Options{}\n\tif err := copier.Copy(newOptions, options); err != nil {\n\t\treturn nil, err\n\t}\n\t// copier does not deep copy maps, so we have to do it manually.\n\tnewOptions.EnvVars = make(map[string]string)\n\tfor key, val := range options.EnvVars {\n\t\tnewOptions.EnvVars[key] = val\n\t}\n\tnewOptions.Vars = make(map[string]interface{})\n\tfor key, val := range options.Vars {\n\t\tnewOptions.Vars[key] = val\n\t}\n\tnewOptions.BackendConfig = make(map[string]interface{})\n\tfor key, val := range options.BackendConfig {\n\t\tnewOptions.BackendConfig[key] = val\n\t}\n\tnewOptions.RetryableTerraformErrors = make(map[string]string)\n\tfor key, val := range options.RetryableTerraformErrors {\n\t\tnewOptions.RetryableTerraformErrors[key] = val\n\t}\n\tnewOptions.WarningsAsErrors = make(map[string]string)\n\tfor key, val := range options.WarningsAsErrors {\n\t\tnewOptions.WarningsAsErrors[key] = val\n\t}\n\n\tnewOptions.MixedVars = append(newOptions.MixedVars, options.MixedVars...)\n\n\treturn newOptions, nil\n}\n\n// WithDefaultRetryableErrors makes a copy of the Options object and returns an updated object with sensible defaults\n// for retryable errors. The included retryable errors are typical errors that most terraform modules encounter during\n// testing, and are known to self resolve upon retrying.\n// This will fail the test if there are any errors in the cloning process.\nfunc WithDefaultRetryableErrors(t testing.TestingT, originalOptions *Options) *Options {\n\tnewOptions, err := originalOptions.Clone()\n\trequire.NoError(t, err)\n\n\tif newOptions.RetryableTerraformErrors == nil {\n\t\tnewOptions.RetryableTerraformErrors = map[string]string{}\n\t}\n\tfor k, v := range DefaultRetryableTerraformErrors {\n\t\tnewOptions.RetryableTerraformErrors[k] = v\n\t}\n\n\t// These defaults for retry configuration are arbitrary, but have worked well in practice across Gruntwork\n\t// modules.\n\tnewOptions.MaxRetries = 3\n\tnewOptions.TimeBetweenRetries = 5 * time.Second\n\n\treturn newOptions\n}\n"
  },
  {
    "path": "modules/terraform/options_test.go",
    "content": "package terraform\n\nimport (\n\t\"fmt\"\n\t\"regexp\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestOptionsCloneDeepClonesEnvVars(t *testing.T) {\n\tt.Parallel()\n\n\tunique := random.UniqueId()\n\toriginal := Options{\n\t\tEnvVars: map[string]string{\n\t\t\t\"unique\":   unique,\n\t\t\t\"original\": unique,\n\t\t},\n\t}\n\tcopied, err := original.Clone()\n\trequire.NoError(t, err)\n\tcopied.EnvVars[\"unique\"] = \"nullified\"\n\tassert.Equal(t, unique, original.EnvVars[\"unique\"])\n\tassert.Equal(t, unique, copied.EnvVars[\"original\"])\n}\n\nfunc TestOptionsCloneDeepClonesVars(t *testing.T) {\n\tt.Parallel()\n\n\tunique := random.UniqueId()\n\toriginal := Options{\n\t\tVars: map[string]interface{}{\n\t\t\t\"unique\":   unique,\n\t\t\t\"original\": unique,\n\t\t},\n\t}\n\tcopied, err := original.Clone()\n\trequire.NoError(t, err)\n\tcopied.Vars[\"unique\"] = \"nullified\"\n\tassert.Equal(t, unique, original.Vars[\"unique\"])\n\tassert.Equal(t, unique, copied.Vars[\"original\"])\n}\n\nfunc TestExtraArgsHelp(t *testing.T) {\n\tt.Parallel()\n\n\ttesttable := []struct {\n\t\tname string\n\t\tfn   func() (string, error)\n\t}{\n\t\t{\n\t\t\tname: \"apply\",\n\t\t\tfn:   func() (string, error) { return ApplyE(t, &Options{ExtraArgs: ExtraArgs{Apply: []string{\"-help\"}}}) },\n\t\t},\n\t\t{\n\t\t\tname: \"destroy\",\n\t\t\tfn:   func() (string, error) { return DestroyE(t, &Options{ExtraArgs: ExtraArgs{Destroy: []string{\"-help\"}}}) },\n\t\t},\n\t\t{\n\t\t\tname: \"get\",\n\t\t\tfn:   func() (string, error) { return GetE(t, &Options{ExtraArgs: ExtraArgs{Get: []string{\"-help\"}}}) },\n\t\t},\n\t\t{\n\t\t\tname: \"init\",\n\t\t\tfn:   func() (string, error) { return InitE(t, &Options{ExtraArgs: ExtraArgs{Init: []string{\"-help\"}}}) },\n\t\t},\n\t\t{\n\t\t\tname: \"plan\",\n\t\t\tfn:   func() (string, error) { return PlanE(t, &Options{ExtraArgs: ExtraArgs{Plan: []string{\"-help\"}}}) },\n\t\t},\n\t\t{\n\t\t\tname: \"validate\",\n\t\t\tfn: func() (string, error) {\n\t\t\t\treturn ValidateE(t, &Options{ExtraArgs: ExtraArgs{Validate: []string{\"-help\"}}})\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range testtable {\n\t\tout, err := tt.fn()\n\t\trequire.NoError(t, err)\n\t\tassert.Regexp(t, regexp.MustCompile(fmt.Sprintf(`(Usage|USAGE):\\s+\\S+\\s+(\\[global options\\]\\s+)?%s`, tt.name)), out)\n\t}\n}\n\nfunc TestExtraArgsWorkspace(t *testing.T) {\n\tname := t.Name()\n\n\tt.Run(\"New\", func(t *testing.T) {\n\t\t// set to default\n\t\tWorkspaceSelectOrNew(t, &Options{}, \"default\")\n\n\t\t// after adding -help, the function did not create the workspace\n\t\tout, err := WorkspaceSelectOrNewE(t, &Options{ExtraArgs: ExtraArgs{\n\t\t\tWorkspaceNew: []string{\"-help\"},\n\t\t}}, random.UniqueId())\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"default\", out)\n\t})\n\n\tout, err := WorkspaceSelectOrNewE(t, &Options{}, name)\n\trequire.NoError(t, err)\n\trequire.Equal(t, name, out)\n\tt.Run(\"Select\", func(t *testing.T) {\n\t\t// set to default\n\t\tWorkspaceSelectOrNew(t, &Options{}, \"default\")\n\n\t\t// after adding -help to select, the function did not select the workspace\n\t\tout, err := WorkspaceSelectOrNewE(t, &Options{ExtraArgs: ExtraArgs{\n\t\t\tWorkspaceSelect: []string{\"-help\"},\n\t\t}}, name)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"default\", out)\n\t})\n\n\tt.Run(\"Delete\", func(t *testing.T) {\n\t\t// after adding -help to select, the function did not delete the workspace\n\t\t_, err := WorkspaceDeleteE(t, &Options{ExtraArgs: ExtraArgs{\n\t\t\tWorkspaceDelete: []string{\"-help\"},\n\t\t}}, name)\n\t\trequire.NoError(t, err)\n\n\t\t// the workspace should still exist\n\t\tout, err := RunTerraformCommandE(t, &Options{}, \"workspace\", \"list\")\n\t\trequire.NoError(t, err)\n\t\tassert.Contains(t, out, name)\n\t})\n}\n\nfunc TestOptionsCloneDeepClonesMixedVars(t *testing.T) {\n\tt.Parallel()\n\n\tunique := random.UniqueId()\n\toriginal := Options{\n\t\tMixedVars: []Var{VarFile(unique), VarInline(\"unique\", unique)},\n\t}\n\tcopied, err := original.Clone()\n\trequire.NoError(t, err)\n\tcopied.MixedVars[1] = VarInline(\"unique\", \"nullified\")\n\tassert.Equal(t, VarFile(unique), copied.MixedVars[0])\n\tassert.Equal(t, VarInline(\"unique\", unique), original.MixedVars[1])\n}\n"
  },
  {
    "path": "modules/terraform/output.go",
    "content": "package terraform\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"reflect\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst skipJsonLogLine = \" msg=\"\n\nvar (\n\t// ansiLineRegex matches lines starting with ANSI escape codes for text formatting (e.g., colors, styles).\n\tansiLineRegex = regexp.MustCompile(`(?m)^\\x1b\\[[0-9;]*m.*`)\n\t// tgLogLevel matches log lines containing fields for time, level, prefix, binary, and message, each with non-whitespace values.\n\ttgLogLevel = regexp.MustCompile(`.*time=\\S+ level=\\S+ prefix=\\S+ binary=\\S+ msg=.*`)\n)\n\n// Output calls terraform output for the given variable and return its string value representation.\n// It only designed to work with primitive terraform types: string, number and bool.\n// Please use OutputStruct for anything else.\nfunc Output(t testing.TestingT, options *Options, key string) string {\n\tout, err := OutputE(t, options, key)\n\trequire.NoError(t, err)\n\treturn out\n}\n\n// OutputE calls terraform output for the given variable and return its string value representation.\n// It only designed to work with primitive terraform types: string, number and bool.\n// Please use OutputStructE for anything else.\nfunc OutputE(t testing.TestingT, options *Options, key string) (string, error) {\n\tvar val interface{}\n\terr := OutputStructE(t, options, key, &val)\n\treturn fmt.Sprintf(\"%v\", val), err\n}\n\n// OutputRequired calls terraform output for the given variable and return its value. If the value is empty, fail the test.\nfunc OutputRequired(t testing.TestingT, options *Options, key string) string {\n\tout, err := OutputRequiredE(t, options, key)\n\trequire.NoError(t, err)\n\treturn out\n}\n\n// OutputRequiredE calls terraform output for the given variable and return its value. If the value is empty, return an error.\nfunc OutputRequiredE(t testing.TestingT, options *Options, key string) (string, error) {\n\tout, err := OutputE(t, options, key)\n\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tif out == \"\" {\n\t\treturn \"\", EmptyOutput(key)\n\t}\n\n\treturn out, nil\n}\n\n// parseMap takes a map of interfaces and parses the types.\n// It is recursive which allows it to support complex nested structures.\n// At this time, this function uses https://golang.org/pkg/strconv/#ParseInt\n// to determine if a number should be a float or an int. For this reason, if you are\n// expecting a float with a zero as the \"tenth\" you will need to manually convert\n// the return value to a float.\n//\n// This function exists to map return values of the terraform outputs to intuitive\n// types. ie, if you are expecting a value of \"1\" you are implicitly expecting an int.\n//\n// This also allows the work to be executed recursively to support complex data types.\nfunc parseMap(m map[string]interface{}) (map[string]interface{}, error) {\n\n\tresult := make(map[string]interface{})\n\n\tfor k, v := range m {\n\t\tswitch vt := v.(type) {\n\t\tcase map[string]interface{}:\n\t\t\tnestedMap, err := parseMap(vt)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tresult[k] = nestedMap\n\t\tcase []interface{}:\n\t\t\tnestedList, err := parseList(vt)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tresult[k] = nestedList\n\t\tcase float64:\n\t\t\tresult[k] = parseFloat(vt)\n\t\tdefault:\n\t\t\tresult[k] = vt\n\t\t}\n\n\t}\n\n\treturn result, nil\n}\n\nfunc parseList(items []interface{}) (_ []interface{}, err error) {\n\tfor i, v := range items {\n\t\trv := reflect.ValueOf(v)\n\t\tswitch rv.Kind() {\n\t\tcase reflect.Map:\n\t\t\titems[i], err = parseMap(rv.Interface().(map[string]interface{}))\n\t\tcase reflect.Slice, reflect.Array:\n\t\t\titems[i], err = parseList(rv.Interface().([]interface{}))\n\t\tcase reflect.Float64:\n\t\t\titems[i] = parseFloat(v)\n\t\t}\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\treturn items, nil\n}\n\nfunc parseFloat(v interface{}) interface{} {\n\ttestInt, err := strconv.ParseInt((fmt.Sprintf(\"%v\", v)), 10, 0)\n\tif err == nil {\n\t\treturn int(testInt)\n\t}\n\treturn v\n}\n\n// OutputMapOfObjects calls terraform output for the given variable and returns its value as a map of lists/maps.\n// If the output value is not a map of lists/maps, then it fails the test.\nfunc OutputMapOfObjects(t testing.TestingT, options *Options, key string) map[string]interface{} {\n\tout, err := OutputMapOfObjectsE(t, options, key)\n\trequire.NoError(t, err)\n\treturn out\n}\n\n// OutputMapOfObjectsE calls terraform output for the given variable and returns its value as a map of lists/maps.\n// Also returns an error object if an error was generated.\n// If the output value is not a map of lists/maps, then it fails the test.\nfunc OutputMapOfObjectsE(t testing.TestingT, options *Options, key string) (map[string]interface{}, error) {\n\tout, err := OutputJsonE(t, options, key)\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar output map[string]interface{}\n\n\tif err := json.Unmarshal([]byte(out), &output); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn parseMap(output)\n}\n\n// OutputListOfObjects calls terraform output for the given variable and returns its value as a list of maps/lists.\n// If the output value is not a list of maps/lists, then it fails the test.\nfunc OutputListOfObjects(t testing.TestingT, options *Options, key string) []map[string]interface{} {\n\tout, err := OutputListOfObjectsE(t, options, key)\n\trequire.NoError(t, err)\n\treturn out\n}\n\n// OutputListOfObjectsE calls terraform output for the given variable and returns its value as a list of maps/lists.\n// Also returns an error object if an error was generated.\n// If the output value is not a list of maps/lists, then it fails the test.\nfunc OutputListOfObjectsE(t testing.TestingT, options *Options, key string) ([]map[string]interface{}, error) {\n\tout, err := OutputJsonE(t, options, key)\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar output []map[string]interface{}\n\n\tif err := json.Unmarshal([]byte(out), &output); err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar result []map[string]interface{}\n\n\tfor _, m := range output {\n\t\tnewMap, err := parseMap(m)\n\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tresult = append(result, newMap)\n\t}\n\n\treturn result, nil\n}\n\n// OutputList calls terraform output for the given variable and returns its value as a list.\n// If the output value is not a list type, then it fails the test.\nfunc OutputList(t testing.TestingT, options *Options, key string) []string {\n\tout, err := OutputListE(t, options, key)\n\trequire.NoError(t, err)\n\treturn out\n}\n\n// OutputListE calls terraform output for the given variable and returns its value as a list.\n// If the output value is not a list type, then it returns an error.\nfunc OutputListE(t testing.TestingT, options *Options, key string) ([]string, error) {\n\tout, err := OutputJsonE(t, options, key)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar output interface{}\n\tif err := json.Unmarshal([]byte(out), &output); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif outputList, isList := output.([]interface{}); isList {\n\t\treturn parseListOutputTerraform(outputList, key)\n\t}\n\n\treturn nil, UnexpectedOutputType{Key: key, ExpectedType: \"map or list\", ActualType: reflect.TypeOf(output).String()}\n}\n\n// Parse a list output in the format it is returned by Terraform 0.12 and newer versions\nfunc parseListOutputTerraform(outputList []interface{}, key string) ([]string, error) {\n\tlist := []string{}\n\n\tfor _, item := range outputList {\n\t\tlist = append(list, fmt.Sprintf(\"%v\", item))\n\t}\n\n\treturn list, nil\n}\n\n// OutputMap calls terraform output for the given variable and returns its value as a map.\n// If the output value is not a map type, then it fails the test.\nfunc OutputMap(t testing.TestingT, options *Options, key string) map[string]string {\n\tout, err := OutputMapE(t, options, key)\n\trequire.NoError(t, err)\n\treturn out\n}\n\n// OutputMapE calls terraform output for the given variable and returns its value as a map.\n// If the output value is not a map type, then it returns an error.\nfunc OutputMapE(t testing.TestingT, options *Options, key string) (map[string]string, error) {\n\tout, err := OutputJsonE(t, options, key)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\toutputMap := map[string]interface{}{}\n\tif err := json.Unmarshal([]byte(out), &outputMap); err != nil {\n\t\treturn nil, err\n\t}\n\n\tresultMap := make(map[string]string)\n\tfor k, v := range outputMap {\n\t\tresultMap[k] = fmt.Sprintf(\"%v\", v)\n\t}\n\treturn resultMap, nil\n}\n\n// OutputForKeys calls terraform output for the given key list and returns values as a map.\n// If keys not found in the output, fails the test\nfunc OutputForKeys(t testing.TestingT, options *Options, keys []string) map[string]interface{} {\n\tout, err := OutputForKeysE(t, options, keys)\n\trequire.NoError(t, err)\n\treturn out\n}\n\n// OutputJson calls terraform output for the given variable and returns the\n// result as the json string.\n// If key is an empty string, it will return all the output variables.\nfunc OutputJson(t testing.TestingT, options *Options, key string) string {\n\tstr, err := OutputJsonE(t, options, key)\n\trequire.NoError(t, err)\n\treturn str\n}\n\n// OutputJsonE calls terraform output for the given variable and returns the\n// result as the json string.\n// If key is an empty string, it will return all the output variables.\nfunc OutputJsonE(t testing.TestingT, options *Options, key string) (string, error) {\n\targs := []string{\"output\", \"-no-color\", \"-json\"}\n\targs = append(args, options.ExtraArgs.Output...)\n\tif key != \"\" {\n\t\targs = append(args, key)\n\t}\n\n\trawJson, err := RunTerraformCommandAndGetStdoutE(t, options, args...)\n\tif err != nil {\n\t\treturn rawJson, err\n\t}\n\treturn cleanJson(rawJson)\n}\n\n// OutputStruct calls terraform output for the given variable and stores the\n// result in the value pointed to by v. If v is nil or not a pointer, or if\n// the value returned by Terraform is not appropriate for a given target type,\n// it fails the test.\nfunc OutputStruct(t testing.TestingT, options *Options, key string, v interface{}) {\n\terr := OutputStructE(t, options, key, v)\n\trequire.NoError(t, err)\n}\n\n// OutputStructE calls terraform output for the given variable and stores the\n// result in the value pointed to by v. If v is nil or not a pointer, or if\n// the value returned by Terraform is not appropriate for a given target type,\n// it returns an error.\nfunc OutputStructE(t testing.TestingT, options *Options, key string, v interface{}) error {\n\tout, err := OutputJsonE(t, options, key)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn json.Unmarshal([]byte(out), &v)\n}\n\n// OutputForKeysE calls terraform output for the given key list and returns values as a map.\n// The returned values are of type interface{} and need to be type casted as necessary. Refer to output_test.go\nfunc OutputForKeysE(t testing.TestingT, options *Options, keys []string) (map[string]interface{}, error) {\n\tout, err := OutputJsonE(t, options, \"\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\toutputMap := map[string]map[string]interface{}{}\n\tif err := json.Unmarshal([]byte(out), &outputMap); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif keys == nil {\n\t\toutputKeys := make([]string, 0, len(outputMap))\n\t\tfor k := range outputMap {\n\t\t\toutputKeys = append(outputKeys, k)\n\t\t}\n\t\tkeys = outputKeys\n\t}\n\n\tresultMap := make(map[string]interface{})\n\tfor _, key := range keys {\n\t\tvalue, containsValue := outputMap[key][\"value\"]\n\t\tif !containsValue {\n\t\t\treturn nil, OutputKeyNotFound(string(key))\n\t\t}\n\t\tresultMap[key] = value\n\t}\n\treturn resultMap, nil\n}\n\n// OutputAll calls terraform output returns all values as a map.\n// If there is error fetching the output, fails the test\nfunc OutputAll(t testing.TestingT, options *Options) map[string]interface{} {\n\tout, err := OutputAllE(t, options)\n\trequire.NoError(t, err)\n\treturn out\n}\n\n// OutputAllE calls terraform and returns all the outputs as a map\nfunc OutputAllE(t testing.TestingT, options *Options) (map[string]interface{}, error) {\n\treturn OutputForKeysE(t, options, nil)\n}\n\n// clean the ANSI characters from the JSON and update formating\nfunc cleanJson(input string) (string, error) {\n\t// Remove ANSI escape codes\n\tcleaned := ansiLineRegex.ReplaceAllString(input, \"\")\n\tcleaned = tgLogLevel.ReplaceAllString(cleaned, \"\")\n\n\tlines := strings.Split(cleaned, \"\\n\")\n\tvar result []string\n\tfor _, line := range lines {\n\t\ttrimmed := strings.TrimSpace(line)\n\t\tif trimmed != \"\" && !strings.Contains(trimmed, skipJsonLogLine) {\n\t\t\tresult = append(result, trimmed)\n\t\t}\n\t}\n\tansiClean := strings.Join(result, \"\\n\")\n\n\tvar jsonObj interface{}\n\tif err := json.Unmarshal([]byte(ansiClean), &jsonObj); err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// Format JSON output with indentation\n\tnormalized, err := json.MarshalIndent(jsonObj, \"\", \"  \")\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn string(normalized), nil\n}\n"
  },
  {
    "path": "modules/terraform/output_test.go",
    "content": "package terraform\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\n\t\"github.com/gruntwork-io/terratest/modules/files\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestOutputString(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerraformFolderToTemp(\"../../test/fixtures/terraform-output\", t.Name())\n\trequire.NoError(t, err)\n\n\toptions := &Options{\n\t\tTerraformDir: testFolder,\n\t}\n\n\tInitAndApply(t, options)\n\n\tb := Output(t, options, \"bool\")\n\trequire.Equal(t, b, \"true\", \"Bool %q should match %q\", \"true\", b)\n\n\tstr := Output(t, options, \"string\")\n\trequire.Equal(t, str, \"This is a string.\", \"String %q should match %q\", \"This is a string.\", str)\n\n\tnum := Output(t, options, \"number\")\n\trequire.Equal(t, num, \"3.14\", \"Number %q should match %q\", \"3.14\", num)\n\n\tnum1 := Output(t, options, \"number1\")\n\trequire.Equal(t, num1, \"3\", \"Number %q should match %q\", \"3\", num1)\n\n\tunicodeString := Output(t, options, \"unicode_string\")\n\trequire.Equal(t, \"söme chäräcter\", unicodeString)\n}\n\nfunc TestOutputList(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerraformFolderToTemp(\"../../test/fixtures/terraform-output-list\", t.Name())\n\trequire.NoError(t, err)\n\n\toptions := &Options{\n\t\tTerraformDir: testFolder,\n\t}\n\n\tInitAndApply(t, options)\n\tout := OutputList(t, options, \"giant_steps\")\n\n\texpectedLen := 4\n\texpectedItem := \"John Coltrane\"\n\texpectedArray := []string{\"John Coltrane\", \"Tommy Flanagan\", \"Paul Chambers\", \"Art Taylor\"}\n\n\trequire.Len(t, out, expectedLen, \"Output should contain %d items\", expectedLen)\n\trequire.Contains(t, out, expectedItem, \"Output should contain %q item\", expectedItem)\n\trequire.Equal(t, out[0], expectedItem, \"First item should be %q, got %q\", expectedItem, out[0])\n\trequire.Equal(t, out, expectedArray, \"Array %q should match %q\", expectedArray, out)\n}\n\nfunc TestOutputNotListError(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerraformFolderToTemp(\"../../test/fixtures/terraform-output-list\", t.Name())\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\toptions := &Options{\n\t\tTerraformDir: testFolder,\n\t}\n\n\tInitAndApply(t, options)\n\t_, err = OutputListE(t, options, \"not_a_list\")\n\n\trequire.Error(t, err)\n}\n\nfunc TestOutputMap(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerraformFolderToTemp(\"../../test/fixtures/terraform-output-map\", t.Name())\n\trequire.NoError(t, err)\n\n\toptions := &Options{\n\t\tTerraformDir: testFolder,\n\t}\n\n\tInitAndApply(t, options)\n\tout := OutputMap(t, options, \"mogwai\")\n\n\tt.Log(out)\n\n\texpectedLen := 4\n\texpectedMap := map[string]string{\n\t\t\"guitar_1\": \"Stuart Braithwaite\",\n\t\t\"guitar_2\": \"Barry Burns\",\n\t\t\"bass\":     \"Dominic Aitchison\",\n\t\t\"drums\":    \"Martin Bulloch\",\n\t}\n\n\trequire.Len(t, out, expectedLen, \"Output should contain %d items\", expectedLen)\n\trequire.Equal(t, expectedMap, out, \"Map %q should match %q\", expectedMap, out)\n}\n\nfunc TestOutputNotMapError(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerraformFolderToTemp(\"../../test/fixtures/terraform-output-map\", t.Name())\n\trequire.NoError(t, err)\n\n\toptions := &Options{\n\t\tTerraformDir: testFolder,\n\t}\n\n\tInitAndApply(t, options)\n\t_, err = OutputMapE(t, options, \"not_a_map\")\n\n\trequire.Error(t, err)\n}\n\nfunc TestOutputMapOfObjects(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerraformFolderToTemp(\"../../test/fixtures/terraform-output-mapofobjects\", t.Name())\n\trequire.NoError(t, err)\n\n\toptions := &Options{\n\t\tTerraformDir: testFolder,\n\t}\n\n\tInitAndApply(t, options)\n\tout := OutputMapOfObjects(t, options, \"map_of_objects\")\n\n\tnestedMap1 := map[string]interface{}{\n\t\t\"four\": 4,\n\t\t\"five\": \"five\",\n\t}\n\n\tnestedList1 := []interface{}{\n\t\tmap[string]interface{}{\n\t\t\t\"six\":   6,\n\t\t\t\"seven\": \"seven\",\n\t\t},\n\t}\n\n\texpectedMap1 := map[string]interface{}{\n\t\t\"somebool\":  true,\n\t\t\"somefloat\": 1.1,\n\t\t\"one\":       1,\n\t\t\"two\":       \"two\",\n\t\t\"three\":     \"three\",\n\t\t\"nest\":      nestedMap1,\n\t\t\"nest_list\": nestedList1,\n\t}\n\n\trequire.Equal(t, expectedMap1, out)\n}\n\nfunc TestOutputNotMapOfObjectsError(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerraformFolderToTemp(\"../../test/fixtures/terraform-output-mapofobjects\", t.Name())\n\trequire.NoError(t, err)\n\n\toptions := &Options{\n\t\tTerraformDir: testFolder,\n\t}\n\n\tInitAndApply(t, options)\n\t_, err = OutputMapOfObjectsE(t, options, \"not_map_of_objects\")\n\n\trequire.Error(t, err)\n}\n\nfunc TestOutputListOfObjects(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerraformFolderToTemp(\"../../test/fixtures/terraform-output-listofobjects\", t.Name())\n\trequire.NoError(t, err)\n\n\toptions := &Options{\n\t\tTerraformDir: testFolder,\n\t}\n\n\tInitAndApply(t, options)\n\tout := OutputListOfObjects(t, options, \"list_of_maps\")\n\n\texpectedLen := 3\n\tnestedMap1 := map[string]interface{}{\n\t\t\"four\": 4,\n\t\t\"five\": \"five\",\n\t}\n\tnestedList1 := []interface{}{\n\t\tmap[string]interface{}{\n\t\t\t\"four\": 4,\n\t\t\t\"five\": \"five\",\n\t\t},\n\t}\n\texpectedMap1 := map[string]interface{}{\n\t\t\"one\":   1,\n\t\t\"two\":   \"two\",\n\t\t\"three\": \"three\",\n\t\t\"more\":  nestedMap1,\n\t}\n\n\texpectedMap2 := map[string]interface{}{\n\t\t\"one\":   \"one\",\n\t\t\"two\":   2,\n\t\t\"three\": 3,\n\t\t\"more\":  nestedList1,\n\t}\n\n\texpectedMap3 := map[string]interface{}{\n\t\t\"one\":   \"one\",\n\t\t\"two\":   2,\n\t\t\"three\": 3,\n\t\t\"more\": []interface{}{\n\t\t\t\"one\",\n\t\t\t2,\n\t\t\t3.4,\n\t\t\t[]interface{}{\"one\", 2, 3.4},\n\t\t\tmap[string]interface{}{\"one\": 2, \"three\": 3.4},\n\t\t},\n\t}\n\n\trequire.Len(t, out, expectedLen, \"Output should contain %d items\", expectedLen)\n\tassert.Equal(t, out[0], expectedMap1, \"First map should be %q, got %q\", expectedMap1, out[0])\n\tassert.Equal(t, out[1], expectedMap2, \"Second map should be %q, got %q\", expectedMap2, out[1])\n\tassert.Equal(t, out[2], expectedMap3, \"Third map should be %q, got %q\", expectedMap3, out[1])\n}\n\nfunc TestOutputNotListOfObjectsError(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerraformFolderToTemp(\"../../test/fixtures/terraform-output-listofobjects\", t.Name())\n\trequire.NoError(t, err)\n\n\toptions := &Options{\n\t\tTerraformDir: testFolder,\n\t}\n\n\tInitAndApply(t, options)\n\t_, err = OutputListOfObjectsE(t, options, \"not_list_of_maps\")\n\n\trequire.Error(t, err)\n}\n\nfunc TestOutputsForKeys(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerraformFolderToTemp(\"../../test/fixtures/terraform-output-all\", t.Name())\n\trequire.NoError(t, err)\n\n\toptions := &Options{\n\t\tTerraformDir: testFolder,\n\t}\n\n\tkeys := []string{\"our_star\", \"stars\", \"magnitudes\"}\n\n\tInitAndApply(t, options)\n\tout := OutputForKeys(t, options, keys)\n\n\texpectedLen := 3\n\trequire.Len(t, out, expectedLen, \"Output should contain %d items\", expectedLen)\n\n\t//String value\n\texpectedString := \"Sun\"\n\tstr, ok := out[\"our_star\"].(string)\n\trequire.True(t, ok, fmt.Sprintf(\"Wrong data type for 'our_star', expected string, got %T\", out[\"our_star\"]))\n\trequire.Equal(t, expectedString, str, \"String %q should match %q\", expectedString, str)\n\n\t//List value\n\texpectedListLen := 3\n\toutputInterfaceList, ok := out[\"stars\"].([]interface{})\n\trequire.True(t, ok, fmt.Sprintf(\"Wrong data type for 'stars', expected [], got %T\", out[\"stars\"]))\n\texpectedListItem := \"Sirius\"\n\trequire.Len(t, outputInterfaceList, expectedListLen, \"Output list should contain %d items\", expectedListLen)\n\trequire.Equal(t, expectedListItem, outputInterfaceList[0].(string), \"List Item %q should match %q\",\n\t\texpectedListItem, outputInterfaceList[0].(string))\n\n\t//Map value\n\toutputInterfaceMap, ok := out[\"magnitudes\"].(map[string]interface{})\n\trequire.True(t, ok, fmt.Sprintf(\"Wrong data type for 'magnitudes', expected map[string], got %T\", out[\"magnitudes\"]))\n\texpectedMapLen := 3\n\texpectedMapItem := -1.46\n\trequire.Len(t, outputInterfaceMap, expectedMapLen, \"Output map should contain %d items\", expectedMapLen)\n\trequire.Equal(t, expectedMapItem, outputInterfaceMap[\"Sirius\"].(float64), \"Map Item %q should match %q\",\n\t\texpectedMapItem, outputInterfaceMap[\"Sirius\"].(float64))\n\n\t//Key not in the parameter list\n\toutputNotPresentMap, ok := out[\"constellations\"].(map[string]interface{})\n\trequire.False(t, ok)\n\trequire.Nil(t, outputNotPresentMap)\n}\n\nfunc TestOutputJson(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerraformFolderToTemp(\"../../test/fixtures/terraform-output\", t.Name())\n\trequire.NoError(t, err)\n\n\toptions := &Options{\n\t\tTerraformDir: testFolder,\n\t}\n\n\tInitAndApply(t, options)\n\n\texpected := `{\n  \"bool\": {\n    \"sensitive\": false,\n    \"type\": \"bool\",\n    \"value\": true\n  },\n  \"number\": {\n    \"sensitive\": false,\n    \"type\": \"number\",\n    \"value\": 3.14\n  },\n  \"number1\": {\n    \"sensitive\": false,\n    \"type\": \"number\",\n    \"value\": 3\n  },\n  \"string\": {\n    \"sensitive\": false,\n    \"type\": \"string\",\n    \"value\": \"This is a string.\"\n  },\n  \"unicode_string\": {\n    \"sensitive\": false,\n    \"type\": \"string\",\n    \"value\": \"söme chäräcter\"\n  }\n}`\n\n\tstr := OutputJson(t, options, \"\")\n\trequire.Equal(t, str, expected, \"JSON %q should match %q\", expected, str)\n}\n\nfunc TestOutputStruct(t *testing.T) {\n\tt.Parallel()\n\n\ttype TestStruct struct {\n\t\tSomebool    bool\n\t\tSomefloat   float64\n\t\tSomeint     int\n\t\tSomestring  string\n\t\tSomemap     map[string]interface{}\n\t\tListmaps    []map[string]interface{}\n\t\tListstrings []string\n\t}\n\n\ttestFolder, err := files.CopyTerraformFolderToTemp(\"../../test/fixtures/terraform-output-struct\", t.Name())\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\toptions := &Options{\n\t\tTerraformDir: testFolder,\n\t\t// Let's test order or ExtraArgs while we are at it\n\t\tExtraArgs: ExtraArgs{\n\t\t\tOutput: []string{\"-state=terraform.tfstate\"},\n\t\t},\n\t}\n\n\tInitAndApply(t, options)\n\n\texpectedObject := TestStruct{\n\t\tSomebool:    true,\n\t\tSomefloat:   0.1,\n\t\tSomeint:     1,\n\t\tSomestring:  \"two\",\n\t\tSomemap:     map[string]interface{}{\"three\": 3.0, \"four\": \"four\"},\n\t\tListmaps:    []map[string]interface{}{{\"five\": 5.0, \"six\": \"six\"}},\n\t\tListstrings: []string{\"seven\", \"eight\", \"nine\"},\n\t}\n\tactualObject := TestStruct{}\n\tOutputStruct(t, options, \"object\", &actualObject)\n\n\texpectedList := []TestStruct{\n\t\t{\n\t\t\tSomebool:   true,\n\t\t\tSomefloat:  0.1,\n\t\t\tSomeint:    1,\n\t\t\tSomestring: \"two\",\n\t\t},\n\t\t{\n\t\t\tSomebool:   false,\n\t\t\tSomefloat:  0.3,\n\t\t\tSomeint:    4,\n\t\t\tSomestring: \"five\",\n\t\t},\n\t}\n\tactualList := []TestStruct{}\n\tOutputStruct(t, options, \"list_of_objects\", &actualList)\n\n\trequire.Equal(t, expectedObject, actualObject, \"Object should be %q, got %q\", expectedObject, actualObject)\n\trequire.Equal(t, expectedList, actualList, \"List should be %q, got %q\", expectedList, actualList)\n}\n\nfunc TestOutputsAll(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerraformFolderToTemp(\"../../test/fixtures/terraform-output-all\", t.Name())\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\toptions := &Options{\n\t\tTerraformDir: testFolder,\n\t}\n\n\tInitAndApply(t, options)\n\tout := OutputAll(t, options)\n\n\texpectedLen := 4\n\trequire.Len(t, out, expectedLen, \"Output should contain %d items\", expectedLen)\n\n\t//String Value\n\texpectedString := \"Sun\"\n\tstr, ok := out[\"our_star\"].(string)\n\trequire.True(t, ok, fmt.Sprintf(\"Wrong data type for 'our_star', expected string, got %T\", out[\"our_star\"]))\n\trequire.Equal(t, expectedString, str, \"String %q should match %q\", expectedString, str)\n\n\t//List Value\n\texpectedListLen := 3\n\toutputInterfaceList, ok := out[\"stars\"].([]interface{})\n\trequire.True(t, ok, fmt.Sprintf(\"Wrong data type for 'stars', expected [], got %T\", out[\"stars\"]))\n\texpectedListItem := \"Betelgeuse\"\n\trequire.Len(t, outputInterfaceList, expectedListLen, \"Output list should contain %d items\", expectedListLen)\n\trequire.Equal(t, expectedListItem, outputInterfaceList[2].(string), \"List item %q should match %q\",\n\t\texpectedListItem, outputInterfaceList[0].(string))\n\n\t//Map Value\n\texpectedMapLen := 4\n\toutputInterfaceMap, ok := out[\"constellations\"].(map[string]interface{})\n\trequire.True(t, ok, fmt.Sprintf(\"Wrong data type for 'constellations', expected map[string], got %T\", out[\"constellations\"]))\n\texpectedMapItem := \"Aldebaran\"\n\trequire.Len(t, outputInterfaceMap, expectedMapLen, \"Output map should contain 4 items\")\n\trequire.Equal(t, expectedMapItem, outputInterfaceMap[\"Taurus\"].(string), \"Map item %q should match %q\",\n\t\texpectedMapItem, outputInterfaceMap[\"Taurus\"].(string))\n}\n\nfunc TestOutputsForKeysError(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerraformFolderToTemp(\"../../test/fixtures/terraform-output-map\", t.Name())\n\trequire.NoError(t, err)\n\n\toptions := &Options{\n\t\tTerraformDir: testFolder,\n\t}\n\n\tInitAndApply(t, options)\n\n\t_, err = OutputForKeysE(t, options, []string{\"random_key\"})\n\n\trequire.Error(t, err)\n}\n\nfunc TestOutputWithDebugLogLevel(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerraformFolderToTemp(\"../../test/fixtures/terraform-output-mapofobjects\", t.Name())\n\trequire.NoError(t, err)\n\n\toptions := &Options{\n\t\tTerraformDir: testFolder,\n\t}\n\n\tInitAndApply(t, options)\n\n\t_, err = OutputMapOfObjectsE(t, &Options{\n\t\tTerraformDir: options.TerraformDir,\n\t\tEnvVars:      map[string]string{\"TF_LOG\": \"DEBUG\"},\n\t}, \"map_of_objects\")\n\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "modules/terraform/plan.go",
    "content": "package terraform\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/gruntwork-io/terratest/modules/logger\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// InitAndPlan runs terraform init and plan with the given options and returns stdout/stderr from the plan command.\n// This will fail the test if there is an error in the command.\nfunc InitAndPlan(t testing.TestingT, options *Options) string {\n\tout, err := InitAndPlanE(t, options)\n\trequire.NoError(t, err)\n\treturn out\n}\n\n// InitAndPlanE runs terraform init and plan with the given options and returns stdout/stderr from the plan command.\nfunc InitAndPlanE(t testing.TestingT, options *Options) (string, error) {\n\tif _, err := InitE(t, options); err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn PlanE(t, options)\n}\n\n// Plan runs terraform plan with the given options and returns stdout/stderr.\n// This will fail the test if there is an error in the command.\nfunc Plan(t testing.TestingT, options *Options) string {\n\tout, err := PlanE(t, options)\n\trequire.NoError(t, err)\n\treturn out\n}\n\n// PlanE runs terraform plan with the given options and returns stdout/stderr.\nfunc PlanE(t testing.TestingT, options *Options) (string, error) {\n\treturn RunTerraformCommandE(t, options, FormatArgs(options, prepend(options.ExtraArgs.Plan, \"plan\", \"-input=false\", \"-lock=false\")...)...)\n}\n\n// InitAndPlanAndShow runs terraform init, then terraform plan, and then terraform show with the given options, and\n// returns the json output of the plan file. This will fail the test if there is an error in the command.\nfunc InitAndPlanAndShow(t testing.TestingT, options *Options) string {\n\tjsonOut, err := InitAndPlanAndShowE(t, options)\n\trequire.NoError(t, err)\n\treturn jsonOut\n}\n\n// InitAndPlanAndShowE runs terraform init, then terraform plan, and then terraform show with the given options, and\n// returns the json output of the plan file.\nfunc InitAndPlanAndShowE(t testing.TestingT, options *Options) (string, error) {\n\tif options.PlanFilePath == \"\" {\n\t\treturn \"\", PlanFilePathRequired\n\t}\n\n\t_, err := InitAndPlanE(t, options)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn ShowE(t, options)\n}\n\n// InitAndPlanAndShowWithStructNoLog runs InitAndPlanAndShowWithStruct without logging and also by allocating a\n// temporary plan file destination that is discarded before returning the struct.\nfunc InitAndPlanAndShowWithStructNoLogTempPlanFile(t testing.TestingT, options *Options) *PlanStruct {\n\toldLogger := options.Logger\n\toptions.Logger = logger.Discard\n\tdefer func() { options.Logger = oldLogger }()\n\n\ttmpFile, err := os.CreateTemp(\"\", \"terratest-plan-file-\")\n\trequire.NoError(t, err)\n\trequire.NoError(t, tmpFile.Close())\n\tdefer require.NoError(t, os.Remove(tmpFile.Name()))\n\n\toptions.PlanFilePath = tmpFile.Name()\n\treturn InitAndPlanAndShowWithStruct(t, options)\n}\n\n// InitAndPlanAndShowWithStruct runs terraform init, then terraform plan, and then terraform show with the given\n// options, and parses the json result into a go struct. This will fail the test if there is an error in the command.\nfunc InitAndPlanAndShowWithStruct(t testing.TestingT, options *Options) *PlanStruct {\n\tplan, err := InitAndPlanAndShowWithStructE(t, options)\n\trequire.NoError(t, err)\n\treturn plan\n}\n\n// InitAndPlanAndShowWithStructE runs terraform init, then terraform plan, and then terraform show with the given options, and\n// parses the json result into a go struct.\nfunc InitAndPlanAndShowWithStructE(t testing.TestingT, options *Options) (*PlanStruct, error) {\n\tjsonOut, err := InitAndPlanAndShowE(t, options)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn ParsePlanJSON(jsonOut)\n}\n\n// InitAndPlanWithExitCode runs terraform init and plan with the given options and returns exitcode for the plan command.\n// This will fail the test if there is an error in the command.\nfunc InitAndPlanWithExitCode(t testing.TestingT, options *Options) int {\n\texitCode, err := InitAndPlanWithExitCodeE(t, options)\n\trequire.NoError(t, err)\n\treturn exitCode\n}\n\n// InitAndPlanWithExitCodeE runs terraform init and plan with the given options and returns exitcode for the plan command.\nfunc InitAndPlanWithExitCodeE(t testing.TestingT, options *Options) (int, error) {\n\tif _, err := InitE(t, options); err != nil {\n\t\treturn DefaultErrorExitCode, err\n\t}\n\n\treturn PlanExitCodeE(t, options)\n}\n\n// PlanExitCode runs terraform plan with the given options and returns the detailed exitcode.\n// This will fail the test if there is an error in the command.\nfunc PlanExitCode(t testing.TestingT, options *Options) int {\n\texitCode, err := PlanExitCodeE(t, options)\n\trequire.NoError(t, err)\n\treturn exitCode\n}\n\n// PlanExitCodeE runs terraform plan with the given options and returns the detailed exitcode.\nfunc PlanExitCodeE(t testing.TestingT, options *Options) (int, error) {\n\treturn GetExitCodeForTerraformCommandE(t, options, FormatArgs(options, prepend(options.ExtraArgs.Plan, \"plan\", \"-input=false\", \"-detailed-exitcode\")...)...)\n}\n\n// Custom errors\n\nvar (\n\tPlanFilePathRequired = fmt.Errorf(\"You must set PlanFilePath on options struct to use this function.\")\n)\n"
  },
  {
    "path": "modules/terraform/plan_struct.go",
    "content": "package terraform\n\nimport (\n\t\"encoding/json\"\n\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\ttfjson \"github.com/hashicorp/terraform-json\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// PlanStruct is a Go Struct representation of the plan object returned from Terraform (after running `terraform show`).\n// Unlike the raw plan representation returned by terraform-json, this struct provides a map that maps the resource\n// addresses to the changes and planned values to make it easier to navigate the raw plan struct.\ntype PlanStruct struct {\n\t// The raw representation of the plan. See\n\t// https://www.terraform.io/docs/internals/json-format.html#plan-representation for details on the structure of the\n\t// plan output.\n\tRawPlan tfjson.Plan\n\n\t// A map that maps full resource addresses (e.g., module.foo.null_resource.test) to the planned values of that\n\t// resource.\n\tResourcePlannedValuesMap map[string]*tfjson.StateResource\n\n\t// A map that maps full resource addresses (e.g., module.foo.null_resource.test) to the planned actions terraform\n\t// will take on that resource.\n\tResourceChangesMap map[string]*tfjson.ResourceChange\n}\n\n// ParsePlanJSON takes in the json string representation of the terraform plan and returns a go struct representation\n// for easy introspection.\nfunc ParsePlanJSON(jsonStr string) (*PlanStruct, error) {\n\tplan := &PlanStruct{}\n\n\tif err := json.Unmarshal([]byte(jsonStr), &plan.RawPlan); err != nil {\n\t\treturn nil, err\n\t}\n\n\tplan.ResourcePlannedValuesMap = parsePlannedValues(plan)\n\tplan.ResourceChangesMap = parseResourceChanges(plan)\n\treturn plan, nil\n}\n\n// parseResourceChanges takes a plan and returns a map that maps resource addresses to the planned changes for that\n// resource. If there are no changes, this returns an empty map instead of erroring.\nfunc parseResourceChanges(plan *PlanStruct) map[string]*tfjson.ResourceChange {\n\tout := map[string]*tfjson.ResourceChange{}\n\tfor _, change := range plan.RawPlan.ResourceChanges {\n\t\tout[change.Address] = change\n\t}\n\treturn out\n}\n\n// parsePlannedValues takes a plan and walks through the planned values to return a map that maps the full resource\n// addresses to the planned resources. If there are no planned values, this returns an empty map instead of erroring.\nfunc parsePlannedValues(plan *PlanStruct) map[string]*tfjson.StateResource {\n\tplannedValues := plan.RawPlan.PlannedValues\n\tif plannedValues == nil {\n\t\t// No planned values, so return empty map.\n\t\treturn map[string]*tfjson.StateResource{}\n\t}\n\n\trootModule := plannedValues.RootModule\n\tif rootModule == nil {\n\t\t// No module resources, so return empty map.\n\t\treturn map[string]*tfjson.StateResource{}\n\t}\n\treturn parseModulePlannedValues(rootModule)\n}\n\n// parseModulePlannedValues will recursively walk through the modules in the planned_values of the plan struct to\n// construct a map that maps the full resource addresses to the planned resource.\nfunc parseModulePlannedValues(module *tfjson.StateModule) map[string]*tfjson.StateResource {\n\tout := map[string]*tfjson.StateResource{}\n\tfor _, resource := range module.Resources {\n\t\t// NOTE: the Address attribute of the module resource always returns the full address, even when the resource is\n\t\t// nested within sub modules.\n\t\tout[resource.Address] = resource\n\t}\n\n\t// NOTE: base case of recursion is when ChildModules is empty list.\n\tfor _, child := range module.ChildModules {\n\t\t// Recurse in to the child module. We take a recursive approach here despite limitations of the recursion stack\n\t\t// in golang due to the fact that it is rare to have heavily deep module calls in Terraform. So we optimize for\n\t\t// code readability as opposed to performance.\n\t\tchildMap := parseModulePlannedValues(child)\n\t\tfor k, v := range childMap {\n\t\t\tout[k] = v\n\t\t}\n\t}\n\treturn out\n}\n\n// AssertPlannedValuesMapKeyExists checks if the given key exists in the map, failing the test if it does not.\nfunc AssertPlannedValuesMapKeyExists(t testing.TestingT, plan *PlanStruct, keyQuery string) {\n\t_, hasKey := plan.ResourcePlannedValuesMap[keyQuery]\n\tassert.Truef(t, hasKey, \"Given planned values map does not have key %s\", keyQuery)\n}\n\n// RequirePlannedValuesMapKeyExists checks if the given key exists in the map, failing and halting the test if it does not.\nfunc RequirePlannedValuesMapKeyExists(t testing.TestingT, plan *PlanStruct, keyQuery string) {\n\t_, hasKey := plan.ResourcePlannedValuesMap[keyQuery]\n\trequire.Truef(t, hasKey, \"Given planned values map does not have key %s\", keyQuery)\n}\n\n// AssertResourceChangesMapKeyExists checks if the given key exists in the map, failing the test if it does not.\nfunc AssertResourceChangesMapKeyExists(t testing.TestingT, plan *PlanStruct, keyQuery string) {\n\t_, hasKey := plan.ResourceChangesMap[keyQuery]\n\tassert.Truef(t, hasKey, \"Given resource changes map does not have key %s\", keyQuery)\n}\n\n// RequireResourceChangesMapKeyExists checks if the given key exists in the map, failing the test if it does not.\nfunc RequireResourceChangesMapKeyExists(t testing.TestingT, plan *PlanStruct, keyQuery string) {\n\t_, hasKey := plan.ResourceChangesMap[keyQuery]\n\trequire.Truef(t, hasKey, \"Given resource changes map does not have key %s\", keyQuery)\n}\n"
  },
  {
    "path": "modules/terraform/plan_struct_test.go",
    "content": "package terraform\n\nimport (\n\t\"testing\"\n\n\thttp_helper \"github.com/gruntwork-io/terratest/modules/http-helper\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst (\n\t// NOTE: We pull down the json files from github during test runtime as opposed to checking it in as these source\n\t// files are licensed under MPL and we want to avoid a dual license scenario where some source files in terratest\n\t// are licensed under a different license.\n\tbasicJsonUrl      = \"https://raw.githubusercontent.com/hashicorp/terraform-json/v0.8.0/testdata/basic/plan.json\"\n\tdeepModuleJsonUrl = \"https://raw.githubusercontent.com/hashicorp/terraform-json/v0.8.0/testdata/deep_module/plan.json\"\n\n\tchangesJsonUrl = \"https://raw.githubusercontent.com/hashicorp/terraform-json/v0.8.0/testdata/has_changes/plan.json\"\n)\n\nfunc TestPlannedValuesMapWithBasicJson(t *testing.T) {\n\tt.Parallel()\n\n\t// Retrieve test data from the terraform-json project.\n\t_, jsonData := http_helper.HttpGet(t, basicJsonUrl, nil)\n\tplan, err := ParsePlanJSON(jsonData)\n\trequire.NoError(t, err)\n\n\tquery := []string{\n\t\t\"data.null_data_source.baz\",\n\t\t\"null_resource.bar\",\n\t\t\"null_resource.baz[0]\",\n\t\t\"null_resource.baz[1]\",\n\t\t\"null_resource.baz[2]\",\n\t\t\"null_resource.foo\",\n\t\t\"module.foo.null_resource.aliased\",\n\t\t\"module.foo.null_resource.foo\",\n\t}\n\tfor _, key := range query {\n\t\tRequirePlannedValuesMapKeyExists(t, plan, key)\n\t\tresource := plan.ResourcePlannedValuesMap[key]\n\t\tassert.Equal(t, resource.Address, key)\n\t}\n}\n\nfunc TestPlannedValuesMapWithDeepModuleJson(t *testing.T) {\n\tt.Parallel()\n\n\t// Retrieve test data from the terraform-json project.\n\t_, jsonData := http_helper.HttpGet(t, deepModuleJsonUrl, nil)\n\tplan, err := ParsePlanJSON(jsonData)\n\trequire.NoError(t, err)\n\n\tquery := []string{\n\t\t\"module.foo.module.bar.null_resource.baz\",\n\t}\n\tfor _, key := range query {\n\t\tAssertPlannedValuesMapKeyExists(t, plan, key)\n\t}\n}\n\nfunc TestResourceChangesJson(t *testing.T) {\n\tt.Parallel()\n\n\t// Retrieve test data from the terraform-json project.\n\t_, jsonData := http_helper.HttpGet(t, changesJsonUrl, nil)\n\tplan, err := ParsePlanJSON(jsonData)\n\trequire.NoError(t, err)\n\n\t// Spot check a few changes to make sure the right address was registered\n\tRequireResourceChangesMapKeyExists(t, plan, \"module.foo.null_resource.foo\")\n\tfooChanges := plan.ResourceChangesMap[\"module.foo.null_resource.foo\"]\n\trequire.NotNil(t, fooChanges.Change)\n\tassert.Equal(t, fooChanges.Change.After.(map[string]interface{})[\"triggers\"].(map[string]interface{})[\"foo\"].(string), \"bar\")\n\n\tRequireResourceChangesMapKeyExists(t, plan, \"null_resource.bar\")\n\tbarChanges := plan.ResourceChangesMap[\"null_resource.bar\"]\n\trequire.NotNil(t, barChanges.Change)\n\tassert.Equal(t, barChanges.Change.After.(map[string]interface{})[\"triggers\"].(map[string]interface{})[\"foo_id\"].(string), \"424881806176056736\")\n\n}\n"
  },
  {
    "path": "modules/terraform/plan_test.go",
    "content": "package terraform\n\nimport (\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/files\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestInitAndPlanWithError(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerraformFolderToTemp(\"../../test/fixtures/terraform-with-plan-error\", t.Name())\n\trequire.NoError(t, err)\n\n\toptions := &Options{\n\t\tTerraformDir: testFolder,\n\t}\n\n\t_, err = InitAndPlanE(t, options)\n\trequire.Error(t, err)\n}\n\nfunc TestInitAndPlanWithNoError(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerraformFolderToTemp(\"../../test/fixtures/terraform-no-error\", t.Name())\n\trequire.NoError(t, err)\n\n\toptions := &Options{\n\t\tTerraformDir: testFolder,\n\t}\n\n\t// In Terraform 0.12 and below, if there were no resources to create, update, or destroy, 'plan' command would\n\t// report \"No changes. Infrastructure is up-to-date.\" However, with 0.13 and above, if the Terraform configuration\n\t// has never been applied at all, 'plan' always shows changes. So we have to run 'apply' first, and can then\n\t// check that 'plan' returns the message we expect.\n\tInitAndApply(t, options)\n\tout, err := PlanE(t, options)\n\trequire.NoError(t, err)\n\trequire.Contains(t, out, \"No changes.\")\n}\n\nfunc TestInitAndPlanWithOutput(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerraformFolderToTemp(\"../../test/fixtures/terraform-basic-configuration\", t.Name())\n\trequire.NoError(t, err)\n\n\toptions := &Options{\n\t\tTerraformDir: testFolder,\n\t\tVars: map[string]interface{}{\n\t\t\t\"cnt\": 1,\n\t\t},\n\t}\n\n\tout, err := InitAndPlanE(t, options)\n\trequire.NoError(t, err)\n\trequire.Contains(t, out, \"1 to add, 0 to change, 0 to destroy.\")\n}\n\nfunc TestInitAndPlanWithPlanFile(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerraformFolderToTemp(\"../../test/fixtures/terraform-basic-configuration\", t.Name())\n\trequire.NoError(t, err)\n\tplanFilePath := filepath.Join(testFolder, \"plan.out\")\n\n\toptions := &Options{\n\t\tTerraformDir: testFolder,\n\t\tVars: map[string]interface{}{\n\t\t\t\"cnt\": 1,\n\t\t},\n\t\tPlanFilePath: planFilePath,\n\t}\n\n\tout, err := InitAndPlanE(t, options)\n\trequire.NoError(t, err)\n\t// clean output to be consistent in checks\n\tout = strings.ReplaceAll(out, \"\\n\", \"\")\n\tassert.Contains(t, out, \"1 to add, 0 to change, 0 to destroy.\")\n\tassert.Contains(t, out, fmt.Sprintf(\"Saved the plan to:%s\", planFilePath))\n\tassert.FileExists(t, planFilePath, \"Plan file was not saved to expected location:\", planFilePath)\n}\n\nfunc TestInitAndPlanAndShowWithStructNoLogTempPlanFile(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerraformFolderToTemp(\"../../test/fixtures/terraform-basic-configuration\", t.Name())\n\trequire.NoError(t, err)\n\n\toptions := &Options{\n\t\tTerraformDir: testFolder,\n\t\tVars: map[string]interface{}{\n\t\t\t\"cnt\": 1,\n\t\t},\n\t}\n\tplanStruct := InitAndPlanAndShowWithStructNoLogTempPlanFile(t, options)\n\tassert.Equal(t, 1, len(planStruct.ResourceChangesMap))\n}\n\nfunc TestPlanWithExitCodeWithNoChanges(t *testing.T) {\n\tt.Parallel()\n\ttestFolder, err := files.CopyTerraformFolderToTemp(\"../../test/fixtures/terraform-no-error\", t.Name())\n\trequire.NoError(t, err)\n\n\toptions := &Options{\n\t\tTerraformDir: testFolder,\n\t}\n\n\t// In Terraform 0.12 and below, if there were no resources to create, update, or destroy, the -detailed-exitcode\n\t// would return a code of 0. However, with 0.13 and above, if the Terraform configuration has never been applied\n\t// at all, -detailed-exitcode always returns an exit code of 2. So we have to run 'apply' first, and can then\n\t// check that 'plan' returns the exit code we expect.\n\tInitAndApply(t, options)\n\texitCode := PlanExitCode(t, options)\n\trequire.Equal(t, DefaultSuccessExitCode, exitCode)\n}\n\nfunc TestPlanWithExitCodeWithChanges(t *testing.T) {\n\tt.Parallel()\n\ttestFolder, err := files.CopyTerraformFolderToTemp(\"../../test/fixtures/terraform-basic-configuration\", t.Name())\n\trequire.NoError(t, err)\n\n\toptions := &Options{\n\t\tTerraformDir: testFolder,\n\t\tVars: map[string]interface{}{\n\t\t\t\"cnt\": 1,\n\t\t},\n\t}\n\texitCode := InitAndPlanWithExitCode(t, options)\n\trequire.Equal(t, TerraformPlanChangesPresentExitCode, exitCode)\n}\n\nfunc TestPlanWithExitCodeWithFailure(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerraformFolderToTemp(\"../../test/fixtures/terraform-with-plan-error\", t.Name())\n\trequire.NoError(t, err)\n\n\toptions := &Options{\n\t\tTerraformDir: testFolder,\n\t}\n\n\texitCode, getExitCodeErr := InitAndPlanWithExitCodeE(t, options)\n\trequire.NoError(t, getExitCodeErr)\n\trequire.Equal(t, exitCode, 1)\n}\n"
  },
  {
    "path": "modules/terraform/show.go",
    "content": "package terraform\n\nimport (\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// Show calls terraform show in json mode with the given options and returns stdout from the command. If\n// PlanFilePath is set on the options, this will show the plan file. Otherwise, this will show the current state of the\n// terraform module at options.TerraformDir. This will fail the test if there is an error in the command.\nfunc Show(t testing.TestingT, options *Options) string {\n\tout, err := ShowE(t, options)\n\trequire.NoError(t, err)\n\treturn out\n}\n\n// ShowE calls terraform show in json mode with the given options and returns stdout from the command. If\n// PlanFilePath is set on the options, this will show the plan file. Otherwise, this will show the current state of the\n// terraform module at options.TerraformDir.\nfunc ShowE(t testing.TestingT, options *Options) (string, error) {\n\t// We manually construct the args here instead of using `FormatArgs`, because show only accepts a limited set of\n\t// args.\n\targs := []string{\"show\", \"-no-color\", \"-json\"}\n\n\t// Attach plan file path if specified.\n\tif options.PlanFilePath != \"\" {\n\t\targs = append(args, options.PlanFilePath)\n\t}\n\treturn RunTerraformCommandAndGetStdoutE(t, options, prepend(options.ExtraArgs.Show, args...)...)\n}\n\nfunc ShowWithStruct(t testing.TestingT, options *Options) *PlanStruct {\n\tout, err := ShowWithStructE(t, options)\n\trequire.NoError(t, err)\n\treturn out\n}\n\nfunc ShowWithStructE(t testing.TestingT, options *Options) (*PlanStruct, error) {\n\tjson, err := ShowE(t, options)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tplanStruct, err := ParsePlanJSON(json)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn planStruct, nil\n}\n"
  },
  {
    "path": "modules/terraform/show_test.go",
    "content": "package terraform\n\nimport (\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/files\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestShowWithInlinePlan(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerraformFolderToTemp(\"../../test/fixtures/terraform-basic-configuration\", t.Name())\n\trequire.NoError(t, err)\n\tplanFilePath := filepath.Join(testFolder, \"plan.out\")\n\n\toptions := &Options{\n\t\tTerraformDir: testFolder,\n\t\tPlanFilePath: planFilePath,\n\t\tVars: map[string]interface{}{\n\t\t\t\"cnt\": 1,\n\t\t},\n\t}\n\n\tout := InitAndPlan(t, options)\n\tout = strings.ReplaceAll(out, \"\\n\", \"\")\n\trequire.Contains(t, out, fmt.Sprintf(\"Saved the plan to:%s\", planFilePath))\n\trequire.FileExists(t, planFilePath, \"Plan file was not saved to expected location:\", planFilePath)\n\n\t// show command does not accept Vars\n\tshowOptions := &Options{\n\t\tTerraformDir: testFolder,\n\t\tPlanFilePath: planFilePath,\n\t}\n\n\t// Test the JSON string\n\tplanJSON := Show(t, showOptions)\n\trequire.Contains(t, planJSON, \"null_resource.test[0]\")\n}\n\nfunc TestShowWithStructInlinePlan(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerraformFolderToTemp(\"../../test/fixtures/terraform-basic-configuration\", t.Name())\n\trequire.NoError(t, err)\n\tplanFilePath := filepath.Join(testFolder, \"plan.out\")\n\n\toptions := &Options{\n\t\tTerraformDir: testFolder,\n\t\tPlanFilePath: planFilePath,\n\t\tVars: map[string]interface{}{\n\t\t\t\"cnt\": 1,\n\t\t},\n\t}\n\n\tout := InitAndPlan(t, options)\n\tout = strings.ReplaceAll(out, \"\\n\", \"\")\n\trequire.Contains(t, out, fmt.Sprintf(\"Saved the plan to:%s\", planFilePath))\n\trequire.FileExists(t, planFilePath, \"Plan file was not saved to expected location:\", planFilePath)\n\n\t// show command does not accept Vars\n\tshowOptions := &Options{\n\t\tTerraformDir: testFolder,\n\t\tPlanFilePath: planFilePath,\n\t}\n\n\t// Test the JSON string\n\tplan := ShowWithStruct(t, showOptions)\n\trequire.Contains(t, plan.ResourcePlannedValuesMap, \"null_resource.test[0]\")\n}\n"
  },
  {
    "path": "modules/terraform/terraform.go",
    "content": "// Package terraform allows to interact with Terraform.\npackage terraform\n\n// https://www.terraform.io/docs/commands/plan.html#detailed-exitcode\n\n// TerraformPlanChangesPresentExitCode is the exit code returned by terraform plan detailed exitcode when changes are present\nconst TerraformPlanChangesPresentExitCode = 2\n\n// DefaultSuccessExitCode is the exit code returned when terraform command succeeds\nconst DefaultSuccessExitCode = 0\n\n// DefaultErrorExitCode is the exit code returned when terraform command fails\nconst DefaultErrorExitCode = 1\n"
  },
  {
    "path": "modules/terraform/validate.go",
    "content": "package terraform\n\nimport (\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// Validate calls terraform validate and returns stdout/stderr.\nfunc Validate(t testing.TestingT, options *Options) string {\n\tout, err := ValidateE(t, options)\n\trequire.NoError(t, err)\n\treturn out\n}\n\n// ValidateE calls terraform validate and returns stdout/stderr.\nfunc ValidateE(t testing.TestingT, options *Options) (string, error) {\n\treturn RunTerraformCommandE(t, options, FormatArgs(options, prepend(options.ExtraArgs.Validate, \"validate\")...)...)\n}\n\n// InitAndValidate runs terraform init and validate with the given options and returns stdout/stderr from the validate command.\n// This will fail the test if there is an error in the command.\nfunc InitAndValidate(t testing.TestingT, options *Options) string {\n\tout, err := InitAndValidateE(t, options)\n\trequire.NoError(t, err)\n\treturn out\n}\n\n// InitAndValidateE runs terraform init and validate with the given options and returns stdout/stderr from the validate command.\nfunc InitAndValidateE(t testing.TestingT, options *Options) (string, error) {\n\tif _, err := InitE(t, options); err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn ValidateE(t, options)\n}\n"
  },
  {
    "path": "modules/terraform/validate_test.go",
    "content": "package terraform\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/files\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestInitAndValidateWithNoError(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerraformFolderToTemp(\"../../test/fixtures/terraform-basic-configuration\", t.Name())\n\trequire.NoError(t, err)\n\n\toptions := &Options{\n\t\tTerraformDir: testFolder,\n\t}\n\n\tout := InitAndValidate(t, options)\n\trequire.Contains(t, out, \"The configuration is valid\")\n}\n\nfunc TestInitAndValidateWithError(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerraformFolderToTemp(\"../../test/fixtures/terraform-with-plan-error\", t.Name())\n\trequire.NoError(t, err)\n\n\toptions := &Options{\n\t\tTerraformDir: testFolder,\n\t}\n\n\tout, err := InitAndValidateE(t, options)\n\trequire.Error(t, err)\n\trequire.Contains(t, out, \"Reference to undeclared input variable\")\n}\n"
  },
  {
    "path": "modules/terraform/var-file.go",
    "content": "package terraform\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"reflect\"\n\t\"strings\"\n\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\n\t\"github.com/hashicorp/hcl/v2\"\n\t\"github.com/hashicorp/hcl/v2/hclparse\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/zclconf/go-cty/cty\"\n\t\"github.com/zclconf/go-cty/cty/gocty\"\n\tctyjson \"github.com/zclconf/go-cty/cty/json\"\n)\n\n// GetVariableAsStringFromVarFile Gets the string representation of a variable from a provided input file found in VarFile\n// For list or map, use GetVariableAsListFromVarFile or GetVariableAsMapFromVarFile, respectively.\nfunc GetVariableAsStringFromVarFile(t testing.TestingT, fileName string, key string) string {\n\tresult, err := GetVariableAsStringFromVarFileE(t, fileName, key)\n\trequire.NoError(t, err)\n\n\treturn result\n}\n\n// GetVariableAsStringFromVarFileE Gets the string representation of a variable from a provided input file found in VarFile\n// Will return an error if GetAllVariablesFromVarFileE returns an error or the key provided does not exist in the file.\n// For list or map, use GetVariableAsListFromVarFile or GetVariableAsMapFromVarFile, respectively.\nfunc GetVariableAsStringFromVarFileE(t testing.TestingT, fileName string, key string) (string, error) {\n\tvar variables map[string]interface{}\n\terr := GetAllVariablesFromVarFileE(t, fileName, &variables)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tvariable, exists := variables[key]\n\n\tif !exists {\n\t\treturn \"\", InputFileKeyNotFound{FilePath: fileName, Key: key}\n\t}\n\n\treturn fmt.Sprintf(\"%v\", variable), nil\n}\n\n// GetVariableAsMapFromVarFile Gets the map representation of a variable from a provided input file found in VarFile\n// Note that this returns a map of strings. For maps containing complex types, use GetAllVariablesFromVarFile.\nfunc GetVariableAsMapFromVarFile(t testing.TestingT, fileName string, key string) map[string]string {\n\tresult, err := GetVariableAsMapFromVarFileE(t, fileName, key)\n\trequire.NoError(t, err)\n\treturn result\n}\n\n// GetVariableAsMapFromVarFileE Gets the map representation of a variable from a provided input file found in VarFile.\n// Note that this returns a map of strings. For maps containing complex types, use GetAllVariablesFromVarFile\n// Returns an error if GetAllVariablesFromVarFileE returns an error, the key provided does not exist, or the value associated with the key is not a map\nfunc GetVariableAsMapFromVarFileE(t testing.TestingT, fileName string, key string) (map[string]string, error) {\n\tvar variables map[string]interface{}\n\terr := GetAllVariablesFromVarFileE(t, fileName, &variables)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvariable, exists := variables[key]\n\n\tif !exists {\n\t\treturn nil, InputFileKeyNotFound{FilePath: fileName, Key: key}\n\t}\n\n\tif reflect.TypeOf(variable).String() != \"map[string]interface {}\" {\n\t\treturn nil, UnexpectedOutputType{Key: key, ExpectedType: \"map[string]interface {}\", ActualType: reflect.TypeOf(variable).String()}\n\t}\n\n\tresultMap := make(map[string]string)\n\tfor mapKey, mapVal := range variable.(map[string]interface{}) {\n\t\tresultMap[mapKey] = fmt.Sprintf(\"%v\", mapVal)\n\t}\n\treturn resultMap, nil\n}\n\n// GetVariableAsListFromVarFile Gets the string list representation of a variable from a provided input file found in VarFile\n// Note that this returns a list of strings. For lists containing complex types, use GetAllVariablesFromVarFile.\nfunc GetVariableAsListFromVarFile(t testing.TestingT, fileName string, key string) []string {\n\tresult, err := GetVariableAsListFromVarFileE(t, fileName, key)\n\trequire.NoError(t, err)\n\n\treturn result\n}\n\n// GetVariableAsListFromVarFileE Gets the string list representation of a variable from a provided input file found in VarFile\n// Note that this returns a list of strings. For lists containing complex types, use GetAllVariablesFromVarFile.\n// Will return error if GetAllVariablesFromVarFileE returns an error, the key provided does not exist, or the value associated with the key is not a list\nfunc GetVariableAsListFromVarFileE(t testing.TestingT, fileName string, key string) ([]string, error) {\n\tvar variables map[string]interface{}\n\terr := GetAllVariablesFromVarFileE(t, fileName, &variables)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvariable, exists := variables[key]\n\tif !exists {\n\t\treturn nil, InputFileKeyNotFound{FilePath: fileName, Key: key}\n\t}\n\n\tif reflect.TypeOf(variable).String() != \"[]interface {}\" {\n\t\treturn nil, UnexpectedOutputType{Key: key, ExpectedType: \"[]interface {}\", ActualType: reflect.TypeOf(variable).String()}\n\t}\n\n\tresultArray := []string{}\n\tfor _, item := range variable.([]interface{}) {\n\t\tresultArray = append(resultArray, fmt.Sprintf(\"%v\", item))\n\t}\n\n\treturn resultArray, nil\n}\n\n// GetAllVariablesFromVarFile Parses all data from a provided input file found ind in VarFile and stores the result in\n// the value pointed to by out.\nfunc GetAllVariablesFromVarFile(t testing.TestingT, fileName string, out interface{}) {\n\terr := GetAllVariablesFromVarFileE(t, fileName, out)\n\trequire.NoError(t, err)\n}\n\n// GetAllVariablesFromVarFileE Parses all data from a provided input file found ind in VarFile and stores the result in\n// the value pointed to by out. Returns an error if the specified file does not exist, the specified file is not\n// readable, or the specified file cannot be decoded from HCL.\nfunc GetAllVariablesFromVarFileE(t testing.TestingT, fileName string, out interface{}) error {\n\tfileContents, err := os.ReadFile(fileName)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn parseAndDecodeVarFile(string(fileContents), fileName, out)\n}\n\n// parseAndDecodeVarFile uses the HCL2 parser to parse the given varfile string into an HCL or HCL JSON file body, and then decode it\n// into a map that maps var names to values.\nfunc parseAndDecodeVarFile(fileContents string, filename string, out interface{}) (err error) {\n\t// The HCL2 parser and especially cty conversions will panic in many types of errors, so we have to recover from\n\t// those panics here and convert them to normal errors\n\tdefer func() {\n\t\tif recovered := recover(); recovered != nil {\n\t\t\terr = PanicWhileParsingVarFile{RecoveredValue: recovered, ConfigFile: filename}\n\t\t}\n\t}()\n\n\tparser := hclparse.NewParser()\n\n\tvar file *hcl.File\n\tvar parseDiagnostics hcl.Diagnostics\n\n\t// determine if a JSON variables file is submitted and parse accordingly\n\tif strings.HasSuffix(filename, \".json\") {\n\t\tfile, parseDiagnostics = parser.ParseJSON([]byte(fileContents), filename)\n\t} else {\n\t\tfile, parseDiagnostics = parser.ParseHCL([]byte(fileContents), filename)\n\t}\n\n\tif parseDiagnostics != nil && parseDiagnostics.HasErrors() {\n\t\treturn parseDiagnostics\n\t}\n\n\t// VarFiles should only have attributes, so extract the attributes and decode the expressions into the return map.\n\tattrs, hclDiags := file.Body.JustAttributes()\n\tif hclDiags != nil && hclDiags.HasErrors() {\n\t\treturn hclDiags\n\t}\n\n\tvalMap := map[string]cty.Value{}\n\tfor name, attr := range attrs {\n\t\tval, hclDiags := attr.Expr.Value(nil) // nil because no function calls or variable references are allowed here\n\t\tif hclDiags != nil && hclDiags.HasErrors() {\n\t\t\treturn hclDiags\n\t\t}\n\t\tvalMap[name] = val\n\t}\n\n\tctyVal, err := convertValuesMapToCtyVal(valMap)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\ttypedOut, hasType := out.(*map[string]interface{})\n\tif hasType {\n\t\tgenericMap, err := parseCtyValueToMap(ctyVal)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t*typedOut = genericMap\n\t\treturn nil\n\t}\n\treturn gocty.FromCtyValue(ctyVal, out)\n}\n\n// This is a hacky workaround to convert a cty Value to a Go map[string]interface{}. cty does not support this directly\n// (https://github.com/hashicorp/hcl2/issues/108) and doing it with gocty.FromCtyValue is nearly impossible, as cty\n// requires you to specify all the output types and will error out when it hits interface{}. So, as an ugly workaround,\n// we convert the given value to JSON using cty's JSON library and then convert the JSON back to a\n// map[string]interface{} using the Go json library.\nfunc parseCtyValueToMap(value cty.Value) (map[string]interface{}, error) {\n\tjsonBytes, err := ctyjson.Marshal(value, cty.DynamicPseudoType)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar ctyJsonOutput CtyJsonOutput\n\tif err := json.Unmarshal(jsonBytes, &ctyJsonOutput); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn ctyJsonOutput.Value, nil\n}\n\n// When you convert a cty value to JSON, if any of that types are not yet known (i.e., are labeled as\n// DynamicPseudoType), cty's Marshall method will write the type information to a type field and the actual value to\n// a value field. This struct is used to capture that information so when we parse the JSON back into a Go struct, we\n// can pull out just the Value field we need.\ntype CtyJsonOutput struct {\n\tValue map[string]interface{}\n\tType  interface{}\n}\n\n// convertValuesMapToCtyVal takes a map of name - cty.Value pairs and converts to a single cty.Value object that can\n// then be converted to other go types.\nfunc convertValuesMapToCtyVal(valMap map[string]cty.Value) (cty.Value, error) {\n\tvalMapAsCty := cty.NilVal\n\tif valMap != nil && len(valMap) > 0 {\n\t\tvar err error\n\t\tvalMapAsCty, err = gocty.ToCtyValue(valMap, generateTypeFromValuesMap(valMap))\n\t\tif err != nil {\n\t\t\treturn valMapAsCty, err\n\t\t}\n\t}\n\treturn valMapAsCty, nil\n}\n\n// generateTypeFromValuesMap takes a values map and returns an object type that has the same number of fields, but\n// bound to each type of the underlying evaluated expression. This is the only way the HCL decoder will be happy, as\n// object type is the only map type that allows different types for each attribute (cty.Map requires all attributes to\n// have the same type.\nfunc generateTypeFromValuesMap(valMap map[string]cty.Value) cty.Type {\n\toutType := map[string]cty.Type{}\n\tfor k, v := range valMap {\n\t\toutType[k] = v.Type()\n\t}\n\treturn cty.Object(outType)\n}\n"
  },
  {
    "path": "modules/terraform/var-file_test.go",
    "content": "package terraform\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestGetVariablesFromVarFilesAsString(t *testing.T) {\n\trandomFileName := fmt.Sprintf(\"./%s.tfvars\", random.UniqueId())\n\n\ttestHcl := []byte(`\n\t\taws_region     = \"us-east-2\"\n\t\taws_account_id = \"111111111111\"\n\t\tnumber_type = 2\n\t\tboolean_type = true\n\t\ttags = {\n\t\t\tfoo = \"bar\"\n\t\t}\n\t\tlist = [\"item1\"]`)\n\n\tWriteFile(t, randomFileName, testHcl)\n\tdefer os.Remove(randomFileName)\n\n\tstringVal := GetVariableAsStringFromVarFile(t, randomFileName, \"aws_region\")\n\n\tboolString := GetVariableAsStringFromVarFile(t, randomFileName, \"boolean_type\")\n\n\tnumString := GetVariableAsStringFromVarFile(t, randomFileName, \"number_type\")\n\n\trequire.Equal(t, \"us-east-2\", stringVal)\n\trequire.Equal(t, \"true\", boolString)\n\trequire.Equal(t, \"2\", numString)\n\n}\n\nfunc TestGetVariablesFromVarFilesAsStringKeyDoesNotExist(t *testing.T) {\n\trandomFileName := fmt.Sprintf(\"./%s.tfvars\", random.UniqueId())\n\n\ttestHcl := []byte(`\n\t\taws_region     = \"us-east-2\"\n\t\taws_account_id = \"111111111111\"\n\t\ttags = {\n\t\t\tfoo = \"bar\"\n\t\t}\n\t\tlist = [\"item1\"]`)\n\n\tWriteFile(t, randomFileName, testHcl)\n\tdefer os.Remove(randomFileName)\n\n\t_, err := GetVariableAsStringFromVarFileE(t, randomFileName, \"badkey\")\n\n\trequire.Error(t, err)\n}\n\nfunc TestGetVariableAsMapFromVarFile(t *testing.T) {\n\trandomFileName := fmt.Sprintf(\"./%s.tfvars\", random.UniqueId())\n\texpected := make(map[string]string)\n\texpected[\"foo\"] = \"bar\"\n\n\ttestHcl := []byte(`\n\t\taws_region     = \"us-east-2\"\n\t\taws_account_id = \"111111111111\"\n\t\ttags = {\n\t\t\tfoo = \"bar\"\n\t\t}\n\t\tlist = [\"item1\"]`)\n\n\tWriteFile(t, randomFileName, testHcl)\n\tdefer os.Remove(randomFileName)\n\n\tval := GetVariableAsMapFromVarFile(t, randomFileName, \"tags\")\n\trequire.Equal(t, expected, val)\n}\n\nfunc TestGetVariableAsMapFromVarFileNotMap(t *testing.T) {\n\trandomFileName := fmt.Sprintf(\"./%s.tfvars\", random.UniqueId())\n\n\ttestHcl := []byte(`\n\t\taws_region     = \"us-east-2\"\n\t\taws_account_id = \"111111111111\"\n\t\ttags = {\n\t\t\tfoo = \"bar\"\n\t\t}\n\t\tlist = [\"item1\"]`)\n\n\tWriteFile(t, randomFileName, testHcl)\n\tdefer os.Remove(randomFileName)\n\n\t_, err := GetVariableAsMapFromVarFileE(t, randomFileName, \"aws_region\")\n\n\trequire.Error(t, err)\n}\n\nfunc TestGetVariableAsMapFromVarFileKeyDoesNotExist(t *testing.T) {\n\trandomFileName := fmt.Sprintf(\"./%s.tfvars\", random.UniqueId())\n\n\ttestHcl := []byte(`\n\t\taws_region     = \"us-east-2\"\n\t\taws_account_id = \"111111111111\"\n\t\ttags = {\n\t\t\tfoo = \"bar\"\n\t\t}\n\t\tlist = [\"item1\"]`)\n\n\tWriteFile(t, randomFileName, testHcl)\n\tdefer os.Remove(randomFileName)\n\n\t_, err := GetVariableAsMapFromVarFileE(t, randomFileName, \"badkey\")\n\n\trequire.Error(t, err)\n}\n\nfunc TestGetVariableAsListFromVarFile(t *testing.T) {\n\trandomFileName := fmt.Sprintf(\"./%s.tfvars\", random.UniqueId())\n\texpected := []string{\"item1\"}\n\n\ttestHcl := []byte(`\n\t\taws_region     = \"us-east-2\"\n\t\taws_account_id = \"111111111111\"\n\t\ttags = {\n\t\t\tfoo = \"bar\"\n\t\t}\n\t\tlist = [\"item1\"]`)\n\n\tWriteFile(t, randomFileName, testHcl)\n\tdefer os.Remove(randomFileName)\n\n\tval := GetVariableAsListFromVarFile(t, randomFileName, \"list\")\n\n\trequire.Equal(t, expected, val)\n}\n\nfunc TestGetVariableAsListNotList(t *testing.T) {\n\trandomFileName := fmt.Sprintf(\"./%s.tfvars\", random.UniqueId())\n\n\ttestHcl := []byte(`\n\t\taws_region     = \"us-east-2\"\n\t\taws_account_id = \"111111111111\"\n\t\ttags = {\n\t\t\tfoo = \"bar\"\n\t\t}\n\t\tlist = [\"item1\"]`)\n\n\tWriteFile(t, randomFileName, testHcl)\n\tdefer os.Remove(randomFileName)\n\n\t_, err := GetVariableAsListFromVarFileE(t, randomFileName, \"tags\")\n\n\trequire.Error(t, err)\n}\n\nfunc TestGetVariableAsListKeyDoesNotExist(t *testing.T) {\n\trandomFileName := fmt.Sprintf(\"./%s.tfvars\", random.UniqueId())\n\n\ttestHcl := []byte(`\n\t\taws_region     = \"us-east-2\"\n\t\taws_account_id = \"111111111111\"\n\t\ttags = {\n\t\t\tfoo = \"bar\"\n\t\t}\n\t\tlist = [\"item1\"]`)\n\n\tWriteFile(t, randomFileName, testHcl)\n\tdefer os.Remove(randomFileName)\n\n\t_, err := GetVariableAsListFromVarFileE(t, randomFileName, \"badkey\")\n\n\trequire.Error(t, err)\n}\nfunc TestGetAllVariablesFromVarFileEFileDoesNotExist(t *testing.T) {\n\tvar variables map[string]interface{}\n\terr := GetAllVariablesFromVarFileE(t, \"filea\", &variables)\n\trequire.Equal(t, \"open filea: no such file or directory\", err.Error())\n}\n\nfunc TestGetAllVariablesFromVarFileBadFile(t *testing.T) {\n\trandomFileName := fmt.Sprintf(\"./%s.tfvars\", random.UniqueId())\n\ttestHcl := []byte(`\n\t\tthiswillnotwork`)\n\n\tWriteFile(t, randomFileName, testHcl)\n\tdefer os.Remove(randomFileName)\n\n\tvar variables map[string]interface{}\n\terr := GetAllVariablesFromVarFileE(t, randomFileName, &variables)\n\trequire.Error(t, err)\n\n\t// HCL library could change their error string, so we are only testing the error string contains what we add to it\n\trequire.Regexp(t, fmt.Sprintf(\"^%s:2,3-18: \", randomFileName), err.Error())\n}\n\nfunc TestGetAllVariablesFromVarFile(t *testing.T) {\n\trandomFileName := fmt.Sprintf(\"./%s.tfvars\", random.UniqueId())\n\ttestHcl := []byte(`\n\taws_region     = \"us-east-2\"\n\t`)\n\n\tWriteFile(t, randomFileName, testHcl)\n\tdefer os.Remove(randomFileName)\n\n\tvar variables map[string]interface{}\n\terr := GetAllVariablesFromVarFileE(t, randomFileName, &variables)\n\trequire.NoError(t, err)\n\n\texpected := make(map[string]interface{})\n\texpected[\"aws_region\"] = \"us-east-2\"\n\n\trequire.Equal(t, expected, variables)\n}\n\nfunc TestGetAllVariablesFromVarFileStructOut(t *testing.T) {\n\trandomFileName := fmt.Sprintf(\"./%s.tfvars\", random.UniqueId())\n\ttestHcl := []byte(`\n\taws_region     = \"us-east-2\"\n\t`)\n\n\tWriteFile(t, randomFileName, testHcl)\n\tdefer os.Remove(randomFileName)\n\n\tvar region struct {\n\t\tAwsRegion string `cty:\"aws_region\"`\n\t}\n\terr := GetAllVariablesFromVarFileE(t, randomFileName, &region)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"us-east-2\", region.AwsRegion)\n}\n\nfunc TestGetVariablesFromVarFilesAsStringJSON(t *testing.T) {\n\trandomFileName := fmt.Sprintf(\"./%s.tfvars.json\", random.UniqueId())\n\n\ttestJSON := []byte(`\n\t\t{\n\t\t\t\"aws_region\": \"us-east-2\",\n\t\t\t\"aws_account_id\": \"111111111111\",\n\t\t\t\"number_type\": 2,\n\t\t\t\"boolean_type\": true,\n\t\t\t\"tags\": {\n\t\t\t\t\"foo\": \"bar\"\n\t\t\t},\n\t\t\t\"list\": [\"item1\"]\n\t\t}`)\n\n\tWriteFile(t, randomFileName, testJSON)\n\tdefer os.Remove(randomFileName)\n\n\tstringVal := GetVariableAsStringFromVarFile(t, randomFileName, \"aws_region\")\n\n\tboolString := GetVariableAsStringFromVarFile(t, randomFileName, \"boolean_type\")\n\n\tnumString := GetVariableAsStringFromVarFile(t, randomFileName, \"number_type\")\n\n\trequire.Equal(t, \"us-east-2\", stringVal)\n\trequire.Equal(t, \"true\", boolString)\n\trequire.Equal(t, \"2\", numString)\n\n}\n\nfunc TestGetVariablesFromVarFilesAsStringKeyDoesNotExistJSON(t *testing.T) {\n\trandomFileName := fmt.Sprintf(\"./%s.tfvars.json\", random.UniqueId())\n\n\ttestJSON := []byte(`\n\t\t{\n\t\t\t\"aws_region\": \"us-east-2\",\n\t\t\t\"aws_account_id\": \"111111111111\",\n\t\t\t\"tags\": {\n\t\t\t\t\"foo\": \"bar\"\n\t\t\t},\n\t\t\t\"list\": [\"item1\"]\n\t\t}`)\n\n\tWriteFile(t, randomFileName, testJSON)\n\tdefer os.Remove(randomFileName)\n\n\t_, err := GetVariableAsStringFromVarFileE(t, randomFileName, \"badkey\")\n\n\trequire.Error(t, err)\n}\n\nfunc TestGetVariableAsMapFromVarFileJSON(t *testing.T) {\n\trandomFileName := fmt.Sprintf(\"./%s.tfvars.json\", random.UniqueId())\n\texpected := make(map[string]string)\n\texpected[\"foo\"] = \"bar\"\n\n\ttestJSON := []byte(`\n\t\t{\n\t\t\t\"aws_region\": \"us-east-2\",\n\t\t\t\"aws_account_id\": \"111111111111\",\n\t\t\t\"tags\": {\n\t\t\t\t\"foo\": \"bar\"\n\t\t\t},\n\t\t\t\"list\": [\"item1\"]\n\t\t}`)\n\n\tWriteFile(t, randomFileName, testJSON)\n\tdefer os.Remove(randomFileName)\n\n\tval := GetVariableAsMapFromVarFile(t, randomFileName, \"tags\")\n\trequire.Equal(t, expected, val)\n}\n\nfunc TestGetVariableAsMapFromVarFileNotMapJSON(t *testing.T) {\n\trandomFileName := fmt.Sprintf(\"./%s.tfvars.json\", random.UniqueId())\n\n\ttestJSON := []byte(`\n\t\t{\n\t\t\t\"aws_region\": \"us-east-2\",\n\t\t\t\"aws_account_id\": \"111111111111\",\n\t\t\t\"tags\": {\n\t\t\t\t\"foo\": \"bar\"\n\t\t\t},\n\t\t\t\"list\": [\"item1\"]\n\t\t}`)\n\n\tWriteFile(t, randomFileName, testJSON)\n\tdefer os.Remove(randomFileName)\n\n\t_, err := GetVariableAsMapFromVarFileE(t, randomFileName, \"aws_region\")\n\n\trequire.Error(t, err)\n}\n\nfunc TestGetVariableAsMapFromVarFileKeyDoesNotExistJSON(t *testing.T) {\n\trandomFileName := fmt.Sprintf(\"./%s.tfvars.json\", random.UniqueId())\n\n\ttestJSON := []byte(`\n\t\t{\n\t\t\t\"aws_region\": \"us-east-2\",\n\t\t\t\"aws_account_id\": \"111111111111\",\n\t\t\t\"tags\": {\n\t\t\t\t\"foo\": \"bar\"\n\t\t\t},\n\t\t\t\"list\": [\"item1\"]\n\t\t}`)\n\n\tWriteFile(t, randomFileName, testJSON)\n\tdefer os.Remove(randomFileName)\n\n\t_, err := GetVariableAsMapFromVarFileE(t, randomFileName, \"badkey\")\n\n\trequire.Error(t, err)\n}\n\nfunc TestGetVariableAsListFromVarFileJSON(t *testing.T) {\n\trandomFileName := fmt.Sprintf(\"./%s.tfvars.json\", random.UniqueId())\n\texpected := []string{\"item1\"}\n\n\ttestJSON := []byte(`\n\t\t{\n\t\t\t\"aws_region\": \"us-east-2\",\n\t\t\t\"aws_account_id\": \"111111111111\",\n\t\t\t\"tags\": {\n\t\t\t\t\"foo\": \"bar\"\n\t\t\t},\n\t\t\t\"list\": [\"item1\"]\n\t\t}`)\n\n\tWriteFile(t, randomFileName, testJSON)\n\tdefer os.Remove(randomFileName)\n\n\tval := GetVariableAsListFromVarFile(t, randomFileName, \"list\")\n\n\trequire.Equal(t, expected, val)\n}\n\nfunc TestGetVariableAsListNotListJSON(t *testing.T) {\n\trandomFileName := fmt.Sprintf(\"./%s.tfvars.json\", random.UniqueId())\n\n\ttestJSON := []byte(`\n\t\t{\n\t\t\t\"aws_region\": \"us-east-2\",\n\t\t\t\"aws_account_id\": \"111111111111\",\n\t\t\t\"tags\": {\n\t\t\t\t\"foo\": \"bar\"\n\t\t\t},\n\t\t\t\"list\": [\"item1\"]\n\t\t}`)\n\n\tWriteFile(t, randomFileName, testJSON)\n\tdefer os.Remove(randomFileName)\n\n\t_, err := GetVariableAsListFromVarFileE(t, randomFileName, \"tags\")\n\n\trequire.Error(t, err)\n}\n\nfunc TestGetVariableAsListKeyDoesNotExistJSON(t *testing.T) {\n\trandomFileName := fmt.Sprintf(\"./%s.tfvars.json\", random.UniqueId())\n\n\ttestJSON := []byte(`\n\t\t{\n\t\t\t\"aws_region\": \"us-east-2\",\n\t\t\t\"aws_account_id\": \"111111111111\",\n\t\t\t\"tags\": {\n\t\t\t\t\"foo\": \"bar\"\n\t\t\t},\n\t\t\t\"list\": [\"item1\"]\n\t\t}`)\n\n\tWriteFile(t, randomFileName, testJSON)\n\tdefer os.Remove(randomFileName)\n\n\t_, err := GetVariableAsListFromVarFileE(t, randomFileName, \"badkey\")\n\n\trequire.Error(t, err)\n}\n\nfunc TestGetAllVariablesFromVarFileBadFileJSON(t *testing.T) {\n\trandomFileName := fmt.Sprintf(\"./%s.tfvars.json\", random.UniqueId())\n\ttestJSON := []byte(`\n\t\t{\n\t\t\tthiswillnotwork\n\t\t}`)\n\n\tWriteFile(t, randomFileName, testJSON)\n\tdefer os.Remove(randomFileName)\n\n\tvar variables map[string]interface{}\n\terr := GetAllVariablesFromVarFileE(t, randomFileName, &variables)\n\trequire.Error(t, err)\n\n\t// HCL library could change their error string, so we are only testing the error string contains what we add to it\n\trequire.Regexp(t, fmt.Sprintf(\"^%s:3,7-22: \", randomFileName), err.Error())\n}\n\nfunc TestGetAllVariablesFromVarFileJSON(t *testing.T) {\n\trandomFileName := fmt.Sprintf(\"./%s.tfvars.json\", random.UniqueId())\n\ttestJSON := []byte(`\n\t{\n\t\t\"aws_region\": \"us-east-2\"\n\t}\n\t`)\n\n\tWriteFile(t, randomFileName, testJSON)\n\tdefer os.Remove(randomFileName)\n\n\tvar variables map[string]interface{}\n\terr := GetAllVariablesFromVarFileE(t, randomFileName, &variables)\n\trequire.NoError(t, err)\n\n\texpected := make(map[string]interface{})\n\texpected[\"aws_region\"] = \"us-east-2\"\n\n\trequire.Equal(t, expected, variables)\n}\n\nfunc TestGetAllVariablesFromVarFileStructOutJSON(t *testing.T) {\n\trandomFileName := fmt.Sprintf(\"./%s.tfvars.json\", random.UniqueId())\n\ttestJSON := []byte(`\n\t{\n\t\t\"aws_region\": \"us-east-2\"\n\t}\n\t`)\n\n\tWriteFile(t, randomFileName, testJSON)\n\tdefer os.Remove(randomFileName)\n\n\tvar region struct {\n\t\tAwsRegion string `cty:\"aws_region\"`\n\t}\n\terr := GetAllVariablesFromVarFileE(t, randomFileName, &region)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"us-east-2\", region.AwsRegion)\n}\n\n// Helper function to write a file to the filesystem\n// Will immediately fail the test if it could not write the file\nfunc WriteFile(t *testing.T, fileName string, bytes []byte) {\n\terr := os.WriteFile(fileName, bytes, 0644)\n\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "modules/terraform/var.go",
    "content": "package terraform\n\ntype Var interface {\n\tArgs() []string\n\tinternal()\n}\n\nfunc VarInline(name string, value interface{}) Var {\n\treturn varInline{name: name, value: value}\n}\n\ntype varInline struct {\n\tname  string\n\tvalue interface{}\n}\n\nfunc (vi varInline) Args() []string {\n\tm := map[string]interface{}{vi.name: vi.value}\n\treturn formatTerraformArgs(m, \"-var\", true, false)\n}\nfunc (vi varInline) internal() {}\n\nfunc VarFile(path string) Var {\n\treturn varFile(path)\n}\n\ntype varFile string\n\nfunc (vf varFile) Args() []string {\n\treturn []string{\"-var-file\", string(vf)}\n}\nfunc (vi varFile) internal() {}\n"
  },
  {
    "path": "modules/terraform/workspace.go",
    "content": "package terraform\n\nimport (\n\t\"strings\"\n\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// WorkspaceSelectOrNew runs terraform workspace with the given options and the workspace name\n// and returns a name of the current workspace. It tries to select a workspace with the given\n// name, or it creates a new one if it doesn't exist.\nfunc WorkspaceSelectOrNew(t testing.TestingT, options *Options, name string) string {\n\tout, err := WorkspaceSelectOrNewE(t, options, name)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\treturn out\n}\n\n// WorkspaceSelectOrNewE runs terraform workspace with the given options and the workspace name\n// and returns a name of the current workspace. It tries to select a workspace with the given\n// name, or it creates a new one if it doesn't exist.\nfunc WorkspaceSelectOrNewE(t testing.TestingT, options *Options, name string) (string, error) {\n\tout, err := RunTerraformCommandE(t, options, \"workspace\", \"list\")\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif isExistingWorkspace(out, name) {\n\t\t_, err = RunTerraformCommandE(t, options, prepend(options.ExtraArgs.WorkspaceSelect, \"workspace\", \"select\", name)...)\n\t} else {\n\t\t_, err = RunTerraformCommandE(t, options, prepend(options.ExtraArgs.WorkspaceNew, \"workspace\", \"new\", name)...)\n\t}\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn RunTerraformCommandE(t, options, \"workspace\", \"show\")\n}\n\nfunc isExistingWorkspace(out string, name string) bool {\n\tworkspaces := strings.Split(out, \"\\n\")\n\tfor _, ws := range workspaces {\n\t\tif strings.HasSuffix(ws, name) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// WorkspaceDelete removes the specified terraform workspace with the given options.\n// It returns the name of the current workspace AFTER deletion, and the returned error (that can be nil).\n// If the workspace to delete is the current one, then it tries to switch to the \"default\" workspace.\n// Deleting the workspace \"default\" is not supported.\nfunc WorkspaceDeleteE(t testing.TestingT, options *Options, name string) (string, error) {\n\tcurrentWorkspace, err := RunTerraformCommandE(t, options, \"workspace\", \"show\")\n\tif err != nil {\n\t\treturn currentWorkspace, err\n\t}\n\n\tif name == \"default\" {\n\t\treturn currentWorkspace, &UnsupportedDefaultWorkspaceDeletion{}\n\t}\n\n\tout, err := RunTerraformCommandE(t, options, \"workspace\", \"list\")\n\tif err != nil {\n\t\treturn currentWorkspace, err\n\t}\n\tif !isExistingWorkspace(out, name) {\n\t\treturn currentWorkspace, WorkspaceDoesNotExist(name)\n\t}\n\n\t// Switch workspace before deleting if it is the current\n\tif currentWorkspace == name {\n\t\tcurrentWorkspace, err = WorkspaceSelectOrNewE(t, options, \"default\")\n\t\tif err != nil {\n\t\t\treturn currentWorkspace, err\n\t\t}\n\t}\n\n\t// delete workspace\n\t_, err = RunTerraformCommandE(t, options, prepend(options.ExtraArgs.WorkspaceDelete, \"workspace\", \"delete\", name)...)\n\n\treturn currentWorkspace, err\n}\n\n// WorkspaceDelete removes the specified terraform workspace with the given options.\n// It returns the name of the current workspace AFTER deletion.\n// If the workspace to delete is the current one, then it tries to switch to the \"default\" workspace.\n// Deleting the workspace \"default\" is not supported and only return an empty string (to avoid a fatal error).\nfunc WorkspaceDelete(t testing.TestingT, options *Options, name string) string {\n\tout, err := WorkspaceDeleteE(t, options, name)\n\trequire.NoError(t, err)\n\treturn out\n}\n"
  },
  {
    "path": "modules/terraform/workspace_test.go",
    "content": "package terraform\n\nimport (\n\t\"errors\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/files\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestWorkspaceNew(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerraformFolderToTemp(\"../../test/fixtures/terraform-workspace\", t.Name())\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\toptions := &Options{\n\t\tTerraformDir: testFolder,\n\t}\n\n\tout := WorkspaceSelectOrNew(t, options, \"terratest\")\n\n\tassert.Equal(t, \"terratest\", out)\n}\n\nfunc TestWorkspaceIllegalName(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerraformFolderToTemp(\"../../test/fixtures/terraform-workspace\", t.Name())\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\toptions := &Options{\n\t\tTerraformDir: testFolder,\n\t}\n\n\tout, err := WorkspaceSelectOrNewE(t, options, \"###@@@&&&\")\n\n\tassert.Error(t, err)\n\tassert.Equal(t, \"\", out, \"%q should be an empty string\", out)\n}\n\nfunc TestWorkspaceSelect(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerraformFolderToTemp(\"../../test/fixtures/terraform-workspace\", t.Name())\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\toptions := &Options{\n\t\tTerraformDir: testFolder,\n\t}\n\n\tout := WorkspaceSelectOrNew(t, options, \"terratest\")\n\tassert.Equal(t, \"terratest\", out)\n\n\tout = WorkspaceSelectOrNew(t, options, \"default\")\n\tassert.Equal(t, \"default\", out)\n}\n\nfunc TestWorkspaceApply(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerraformFolderToTemp(\"../../test/fixtures/terraform-workspace\", t.Name())\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\toptions := &Options{\n\t\tTerraformDir: testFolder,\n\t}\n\n\tWorkspaceSelectOrNew(t, options, \"Terratest\")\n\tout := InitAndApply(t, options)\n\n\tassert.Contains(t, out, \"Hello, Terratest\")\n}\n\nfunc TestIsExistingWorkspace(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tout      string\n\t\tname     string\n\t\texpected bool\n\t}{\n\t\t{\"  default\\n* foo\\n\", \"default\", true},\n\t\t{\"* default\\n  foo\\n\", \"default\", true},\n\t\t{\"  foo\\n* default\\n\", \"default\", true},\n\t\t{\"* foo\\n  default\\n\", \"default\", true},\n\t\t{\"  foo\\n* bar\\n\", \"default\", false},\n\t\t{\"* foo\\n  bar\\n\", \"default\", false},\n\t\t{\"  default\\n* foobar\\n\", \"foo\", false},\n\t\t{\"* default\\n  foobar\\n\", \"foo\", false},\n\t\t{\"  default\\n* foo\\n\", \"foobar\", false},\n\t\t{\"* default\\n  foo\\n\", \"foobar\", false},\n\t\t{\"* default\\n  foo\\n\", \"foo\", true},\n\t}\n\n\tfor _, testCase := range testCases {\n\t\tactual := isExistingWorkspace(testCase.out, testCase.name)\n\t\tassert.Equal(t, testCase.expected, actual, \"Out: %q, Name: %q\", testCase.out, testCase.name)\n\t}\n}\n\nfunc TestWorkspaceDeleteE(t *testing.T) {\n\tt.Parallel()\n\n\t// state describes an expected status when a given testCase begins\n\ttype state struct {\n\t\tworkspaces []string\n\t\tcurrent    string\n\t}\n\n\t// testCase describes a named test case with a state, args and expcted results\n\ttype testCase struct {\n\t\tname              string\n\t\tinitialState      state\n\t\ttoDeleteWorkspace string\n\t\texpectedCurrent   string\n\t\texpectedError     error\n\t}\n\n\ttestCases := []testCase{\n\t\t{\n\t\t\tname: \"delete another existing workspace and stay on current\",\n\t\t\tinitialState: state{\n\t\t\t\tworkspaces: []string{\"staging\", \"production\"},\n\t\t\t\tcurrent:    \"staging\",\n\t\t\t},\n\t\t\ttoDeleteWorkspace: \"production\",\n\t\t\texpectedCurrent:   \"staging\",\n\t\t\texpectedError:     nil,\n\t\t},\n\t\t{\n\t\t\tname: \"delete current workspace and switch to a specified\",\n\t\t\tinitialState: state{\n\t\t\t\tworkspaces: []string{\"staging\", \"production\"},\n\t\t\t\tcurrent:    \"production\",\n\t\t\t},\n\t\t\ttoDeleteWorkspace: \"production\",\n\t\t\texpectedCurrent:   \"default\",\n\t\t\texpectedError:     nil,\n\t\t},\n\t\t{\n\t\t\tname: \"delete a non existing workspace should trigger an error\",\n\t\t\tinitialState: state{\n\t\t\t\tworkspaces: []string{\"staging\", \"production\"},\n\t\t\t\tcurrent:    \"staging\",\n\t\t\t},\n\t\t\ttoDeleteWorkspace: \"hellothere\",\n\t\t\texpectedCurrent:   \"staging\",\n\t\t\texpectedError:     WorkspaceDoesNotExist(\"hellothere\"),\n\t\t},\n\t\t{\n\t\t\tname: \"delete the default workspace triggers an error\",\n\t\t\tinitialState: state{\n\t\t\t\tworkspaces: []string{\"staging\", \"production\"},\n\t\t\t\tcurrent:    \"staging\",\n\t\t\t},\n\t\t\ttoDeleteWorkspace: \"default\",\n\t\t\texpectedCurrent:   \"staging\",\n\t\t\texpectedError:     &UnsupportedDefaultWorkspaceDeletion{},\n\t\t},\n\t}\n\n\tfor _, testCase := range testCases {\n\t\ttestCase := testCase\n\t\tt.Run(testCase.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\ttestFolder, err := files.CopyTerraformFolderToTemp(\"../../test/fixtures/terraform-workspace\", testCase.name)\n\t\t\trequire.NoError(t, err)\n\n\t\t\toptions := &Options{\n\t\t\t\tTerraformDir: testFolder,\n\t\t\t}\n\n\t\t\t// Set up pre-existing environment based on test case description\n\t\t\tfor _, existingWorkspace := range testCase.initialState.workspaces {\n\t\t\t\t_, err = RunTerraformCommandE(t, options, \"workspace\", \"new\", existingWorkspace)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\t\t\t// Switch to the specified workspace\n\t\t\t_, err = RunTerraformCommandE(t, options, \"workspace\", \"select\", testCase.initialState.current)\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// Testing time, wooohoooo\n\t\t\tgotResult, gotErr := WorkspaceDeleteE(t, options, testCase.toDeleteWorkspace)\n\n\t\t\t// Check for errors\n\t\t\tif testCase.expectedError != nil {\n\t\t\t\tif !errors.Is(gotErr, testCase.expectedError) {\n\t\t\t\t\tt.Errorf(\"expected error: %v, got error: %v\", testCase.expectedError, gotErr)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, gotErr)\n\t\t\t\t// Check for results\n\t\t\t\tassert.Equal(t, testCase.expectedCurrent, gotResult)\n\t\t\t\tassert.False(t, isExistingWorkspace(RunTerraformCommand(t, options, \"workspace\", \"list\"), testCase.toDeleteWorkspace))\n\t\t\t}\n\t\t})\n\n\t}\n}\n"
  },
  {
    "path": "modules/terragrunt/README.md",
    "content": "# Terragrunt Module\n\nTesting library for Terragrunt configurations in Go. Provides helpers for running Terragrunt commands for single units, across multiple modules (run-all), and stack-based workflows.\n\n## Requirements\n\n- **Terragrunt** binary in PATH\n- **OpenTofu** or **Terraform** binary in PATH (Terragrunt is a wrapper and requires one of these)\n\nTo specify which binary to use (terraform vs opentofu):\n```go\n// Option 1: Via environment variable\noptions := &terragrunt.Options{\n    TerragruntDir: \"/path/to/config\",\n    EnvVars: map[string]string{\n        \"TERRAGRUNT_TFPATH\": \"/usr/local/bin/tofu\",  // or \"TG_TF_PATH\"\n    },\n}\n\n// Option 2: Via command-line flag\noptions := &terragrunt.Options{\n    TerragruntDir:  \"/path/to/config\",\n    TerragruntArgs: []string{\"--tf-path\", \"/usr/local/bin/tofu\"},\n}\n```\n\n## Quick Start\n\n### Single Unit\n\n```go\nimport (\n    \"testing\"\n    \"github.com/gruntwork-io/terratest/modules/terragrunt\"\n    \"github.com/stretchr/testify/assert\"\n)\n\nfunc TestSingleUnit(t *testing.T) {\n    t.Parallel()\n\n    options := &terragrunt.Options{\n        TerragruntDir: \"../path/to/terragrunt/unit\",\n    }\n\n    defer terragrunt.Destroy(t, options)\n    terragrunt.InitAndApply(t, options)\n\n    // Get a specific output as JSON\n    vpcOutput := terragrunt.OutputJson(t, options, \"vpc_id\")\n    assert.Contains(t, vpcOutput, \"vpc-\")\n}\n```\n\n### Multiple Modules (--all)\n\n```go\nfunc TestTerragruntApply(t *testing.T) {\n    t.Parallel()\n\n    options := &terragrunt.Options{\n        TerragruntDir: \"../path/to/terragrunt/config\",\n    }\n\n    defer terragrunt.DestroyAll(t, options)\n    terragrunt.ApplyAll(t, options)\n}\n```\n\n## Key Concepts\n\n### Options Struct\n\nThe `Options` struct has two distinct parts:\n\n1. **Test Framework Configuration** (NOT passed to terragrunt CLI):\n   - `TerragruntDir` - where to run terragrunt (required)\n   - `TerragruntBinary` - binary name (default: \"terragrunt\")\n   - `EnvVars` - environment variables\n   - `Logger` - custom logger for output\n   - `MaxRetries`, `TimeBetweenRetries` - retry settings\n   - `RetryableTerraformErrors` - map of error patterns to retry messages\n   - `WarningsAsErrors` - map of warning patterns to treat as errors\n   - `BackendConfig` - backend configuration passed to `init`\n   - `PluginDir` - plugin directory passed to `init`\n   - `Stdin` - stdin reader for commands\n\n2. **Command-Line Arguments** (passed to terragrunt):\n   - `TerragruntArgs` - global terragrunt flags (e.g., `--log-level`, `--no-color`)\n   - `TerraformArgs` - command-specific OpenTofu/Terraform flags (e.g., `-upgrade`)\n\n### Error-Returning Variants (E-suffix)\n\nEvery function has an `E`-suffix variant that returns an error instead of calling `t.Fatal` on failure. For example:\n\n- `Apply(t, options)` calls `t.Fatal` on error\n- `ApplyE(t, options)` returns `(string, error)` for custom error handling\n\nUse `E` variants when you need to test error cases or handle failures gracefully:\n```go\n_, err := terragrunt.ApplyE(t, options)\nrequire.Error(t, err)\n```\n\n### TerragruntArgs vs TerraformArgs\n\nArguments are passed in this order:\n```\nterragrunt [TerragruntArgs] --non-interactive run -- <command> [TerraformArgs]\n```\n\n**Example:**\n```go\noptions := &terragrunt.Options{\n    TerragruntDir:  \"/path/to/config\",\n    TerragruntArgs: []string{\"--log-level\", \"error\"},  // Global TG flags\n    TerraformArgs:  []string{\"-upgrade\"},              // OpenTofu/Terraform flags\n}\n// Executes: terragrunt --log-level error --non-interactive run -- init -upgrade\n```\n\n## Functions\n\n### Single-Unit Commands\n\nRun terragrunt commands against a single unit (one `terragrunt.hcl` directory):\n\n- `Init(t, options)` - Initialize configuration\n- `Apply(t, options)` - Apply changes\n- `Destroy(t, options)` - Destroy resources\n- `Plan(t, options)` - Generate and show execution plan\n- `PlanExitCode(t, options)` - Plan and return exit code (0=no changes, 2=changes, other=error)\n- `Validate(t, options)` - Validate configuration\n- `OutputJson(t, options, key)` - Get output as JSON (specific key or all outputs)\n\n### Convenience Wrappers\n\nRun init + command in a single call:\n\n- `InitAndApply(t, options)` - Init then apply\n- `InitAndPlan(t, options)` - Init then plan\n- `InitAndValidate(t, options)` - Init then validate\n\n### Run Command\n\n- `Run(t, options, tgArgs, tfArgs)` - Run any OpenTofu/Terraform command via `terragrunt run [tgArgs...] -- [tfArgs...]`\n\nThe `--` separator disambiguates Terragrunt flags (like `--all`) from OpenTofu/Terraform flags. The OpenTofu/Terraform command (e.g. `\"apply\"`) should be the first element of `tfArgs`.\n\n### Run --all Commands\n\nWork with [implicit stacks](https://terragrunt.gruntwork.io/docs/features/stacks/#implicit-stacks) (multiple units in a directory):\n\n- `ApplyAll(t, options)` - Apply all modules with dependencies\n- `DestroyAll(t, options)` - Destroy all modules with dependencies\n- `PlanAllExitCode(t, options)` - Plan all and return exit code (0=no changes, 2=changes, other=error)\n- `ValidateAll(t, options)` - Validate all modules\n- `RunAll(t, options, command)` - *Deprecated: use `Run` with `--all` in tgArgs instead.* Run any OpenTofu/Terraform command with --all flag\n- `OutputAllJson(t, options)` - Get all outputs as raw JSON string (note: returns separate JSON objects per module)\n\n### HCL Commands\n\nTerragrunt HCL tooling commands:\n\n- `FormatAll(t, options)` - Format all terragrunt.hcl files (`terragrunt hcl format`)\n- `HclValidate(t, options)` - Validate terragrunt.hcl syntax and configuration (`terragrunt hcl validate`)\n\n### Configuration Commands\n\n- `Render(t, options)` - Render resolved terragrunt configuration as HCL\n- `RenderJson(t, options)` - Render resolved terragrunt configuration as JSON\n- `Graph(t, options)` - Output dependency graph in DOT format\n\n### Stack Commands\n\nWork with [explicit stacks](https://terragrunt.gruntwork.io/docs/features/stacks/#explicit-stacks) (a directory with a `terragrunt.stack.hcl` file):\n\n- `StackGenerate(t, options)` - Generate stack from stack.hcl\n- `StackRun(t, options)` - Run command on generated stack\n- `StackClean(t, options)` - Remove .terragrunt-stack directory\n- `StackOutput(t, options, key)` - Get stack output value\n- `StackOutputJson(t, options, key)` - Get stack output as JSON\n- `StackOutputAll(t, options)` - Get all stack outputs as map\n- `StackOutputListAll(t, options)` - Get list of all output variable names\n\n## Examples\n\nSee the [examples directory](../../examples/) for complete working examples:\n- [terragrunt-example](../../examples/terragrunt-example/) - Single unit testing\n- [terragrunt-multi-module-example](../../examples/terragrunt-multi-module-example/) - Multi-module testing\n- [terragrunt-second-example](../../examples/terragrunt-second-example/) - Additional patterns\n\n### Testing with Dependencies\n\n```go\nfunc TestStack(t *testing.T) {\n    t.Parallel()\n\n    options := &terragrunt.Options{\n        TerragruntDir: \"../live/prod\",\n    }\n\n    // Apply respects dependency order\n    terragrunt.ApplyAll(t, options)\n    defer terragrunt.DestroyAll(t, options)\n\n    // Verify infrastructure\n    // ... your assertions here\n}\n```\n\n### Using Custom Arguments\n\n```go\nfunc TestWithCustomArgs(t *testing.T) {\n    t.Parallel()\n\n    options := &terragrunt.Options{\n        TerragruntDir:  \"../config\",\n        TerragruntArgs: []string{\"--log-level\", \"error\", \"--no-color\"},\n        TerraformArgs:  []string{\"-upgrade\"},\n    }\n\n    terragrunt.Init(t, options)\n}\n```\n\n### Testing Stack Outputs\n\n```go\nfunc TestStackOutput(t *testing.T) {\n    t.Parallel()\n\n    options := &terragrunt.Options{\n        TerragruntDir: \"../stack\",\n    }\n\n    applyOpts := &terragrunt.Options{\n        TerragruntDir: \"../stack\",\n        TerraformArgs: []string{\"apply\"},\n    }\n    destroyOpts := &terragrunt.Options{\n        TerragruntDir: \"../stack\",\n        TerraformArgs: []string{\"destroy\"},\n    }\n\n    terragrunt.StackRun(t, applyOpts)\n    defer terragrunt.StackRun(t, destroyOpts)\n\n    // Get specific output\n    vpcID := terragrunt.StackOutput(t, options, \"vpc_id\")\n    assert.NotEmpty(t, vpcID)\n\n    // Get all outputs\n    outputs := terragrunt.StackOutputAll(t, options)\n    assert.Contains(t, outputs, \"vpc_id\")\n}\n```\n\n### Checking Plan Exit Code\n\n```go\nfunc TestInfrastructureUpToDate(t *testing.T) {\n    t.Parallel()\n\n    options := &terragrunt.Options{\n        TerragruntDir: \"../prod\",\n    }\n\n    // First apply\n    terragrunt.ApplyAll(t, options)\n    defer terragrunt.DestroyAll(t, options)\n\n    // Plan should show no changes (exit code 0)\n    exitCode := terragrunt.PlanAllExitCode(t, options)\n    assert.Equal(t, 0, exitCode, \"No changes expected\")\n}\n```\n\n### Using Run for Flexibility\n\n```go\nfunc TestCustomCommand(t *testing.T) {\n    t.Parallel()\n\n    options := &terragrunt.Options{\n        TerragruntDir: \"../modules\",\n    }\n\n    // Run any OpenTofu/Terraform command with --all\n    terragrunt.Run(t, options, []string{\"--all\"}, []string{\"refresh\"})\n\n    // Verify state is current\n    output := terragrunt.Run(t, options, []string{\"--all\"}, []string{\"show\"})\n    assert.Contains(t, output, \"expected-resource\")\n}\n```\n\n### Validating Stack Output Keys\n\n```go\nfunc TestStackOutputKeys(t *testing.T) {\n    t.Parallel()\n\n    options := &terragrunt.Options{\n        TerragruntDir: \"../stack\",\n    }\n\n    applyOpts := &terragrunt.Options{\n        TerragruntDir: \"../stack\",\n        TerraformArgs: []string{\"apply\"},\n    }\n    destroyOpts := &terragrunt.Options{\n        TerragruntDir: \"../stack\",\n        TerraformArgs: []string{\"destroy\"},\n    }\n\n    terragrunt.StackRun(t, applyOpts)\n    defer terragrunt.StackRun(t, destroyOpts)\n\n    // Get list of all output keys\n    keys := terragrunt.StackOutputListAll(t, options)\n\n    // Verify required outputs exist\n    assert.Contains(t, keys, \"vpc_id\")\n    assert.Contains(t, keys, \"subnet_ids\")\n}\n```\n\n### Using Filters (v0.97.0+)\n\n```go\noptions := &terragrunt.Options{\n    TerragruntDir:  \"../live/prod\",\n    TerragruntArgs: []string{\"--filter\", \"{./vpc}\"},  // Only apply vpc\n}\nterragrunt.ApplyAll(t, options)\n```\n\n## Not Supported\n\nThis module does **NOT** have dedicated helpers for:\n- `import`, `refresh`, `show`, `state`, `test` commands\n- `backend`, `exec`, `catalog`, `scaffold` commands\n- Discovery commands (`find`, `list`)\n- Configuration commands (`info`)\n\nFor these commands, use `Run`/`RunE` or run terragrunt directly via the `shell` module.\n\n## Compatibility\n\nTested with Terragrunt v0.80.4+, v0.93.5+, and v0.99.x. Earlier versions may work but are not guaranteed.\n\n### Migration from terraform Module\n\nThe following functions were previously in the `terraform` module and have been moved here. The deprecated versions have been removed from the `terraform` module.\n\n| Removed (terraform module) | Replacement (terragrunt module) |\n|----------------------------|----------------------------------|\n| `TgApplyAll` / `TgApplyAllE` | `ApplyAll` / `ApplyAllE` |\n| `TgDestroyAll` / `TgDestroyAllE` | `DestroyAll` / `DestroyAllE` |\n| `TgPlanAllExitCode` / `TgPlanAllExitCodeE` | `PlanAllExitCode` / `PlanAllExitCodeE` |\n| `ValidateInputs` / `ValidateInputsE` | `HclValidate` / `HclValidateE` |\n\n> **Note:** `ValidateInputs` specifically checked input alignment. For equivalent behavior, pass `TerraformArgs: []string{\"--inputs\"}` to `HclValidate`.\n\n## More Info\n\n- [Terragrunt Documentation](https://terragrunt.gruntwork.io/)\n- [Terratest Documentation](https://terratest.gruntwork.io/)\n"
  },
  {
    "path": "modules/terragrunt/apply.go",
    "content": "package terragrunt\n\nimport (\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// ApplyAll runs terragrunt run --all apply with the given options and returns stdout/stderr. Note that this method does NOT call destroy and\n// assumes the caller is responsible for cleaning up any resources created by running apply.\nfunc ApplyAll(t testing.TestingT, options *Options) string {\n\tout, err := ApplyAllE(t, options)\n\trequire.NoError(t, err)\n\treturn out\n}\n\n// ApplyAllE runs terragrunt run --all -- apply with the given options and returns stdout/stderr. Note that this method does NOT call destroy and\n// assumes the caller is responsible for cleaning up any resources created by running apply.\nfunc ApplyAllE(t testing.TestingT, options *Options) (string, error) {\n\targs := buildRunArgs([]string{\"--all\"}, []string{\"apply\", \"-input=false\", \"-auto-approve\"})\n\treturn runTerragruntCommandE(t, options, \"run\", args...)\n}\n\n// Apply runs terragrunt run apply for a single unit and returns stdout/stderr.\nfunc Apply(t testing.TestingT, options *Options) string {\n\tout, err := ApplyE(t, options)\n\trequire.NoError(t, err)\n\treturn out\n}\n\n// ApplyE runs terragrunt run -- apply for a single unit and returns stdout/stderr.\nfunc ApplyE(t testing.TestingT, options *Options) (string, error) {\n\targs := buildRunArgs([]string{}, []string{\"apply\", \"-input=false\", \"-auto-approve\"})\n\treturn runTerragruntCommandE(t, options, \"run\", args...)\n}\n\n// InitAndApply runs terragrunt init followed by apply for a single unit and returns the apply stdout/stderr.\nfunc InitAndApply(t testing.TestingT, options *Options) string {\n\tout, err := InitAndApplyE(t, options)\n\trequire.NoError(t, err)\n\treturn out\n}\n\n// InitAndApplyE runs terragrunt init followed by apply for a single unit and returns the apply stdout/stderr.\nfunc InitAndApplyE(t testing.TestingT, options *Options) (string, error) {\n\tif _, err := InitE(t, options); err != nil {\n\t\treturn \"\", err\n\t}\n\treturn ApplyE(t, options)\n}\n"
  },
  {
    "path": "modules/terragrunt/apply_test.go",
    "content": "package terragrunt\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/files\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestApplyAll(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerragruntFolderToTemp(\"testdata/terragrunt-no-error\", t.Name())\n\trequire.NoError(t, err)\n\n\toptions := &Options{\n\t\tTerragruntDir:    testFolder,\n\t\tTerragruntBinary: \"terragrunt\",\n\t}\n\n\tdefer DestroyAll(t, options)\n\tout := ApplyAll(t, options)\n\trequire.Contains(t, out, \"Hello, World\")\n}\n\nfunc TestApply(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerragruntFolderToTemp(\"testdata/terragrunt-no-error\", t.Name())\n\trequire.NoError(t, err)\n\n\toptions := &Options{\n\t\tTerragruntDir:    testFolder,\n\t\tTerragruntBinary: \"terragrunt\",\n\t}\n\n\tdefer Destroy(t, options)\n\tout := Apply(t, options)\n\trequire.Contains(t, out, \"Hello, World\")\n}\n\nfunc TestInitAndApply(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerragruntFolderToTemp(\"testdata/terragrunt-no-error\", t.Name())\n\trequire.NoError(t, err)\n\n\toptions := &Options{\n\t\tTerragruntDir:    testFolder,\n\t\tTerragruntBinary: \"terragrunt\",\n\t}\n\n\tdefer Destroy(t, options)\n\tout := InitAndApply(t, options)\n\trequire.Contains(t, out, \"Hello, World\")\n}\n\n// TestInitAndApplyE_InitFailure verifies that when init fails, apply is skipped\n// and the init error is propagated.\nfunc TestInitAndApplyE_InitFailure(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerraformFolderToTemp(\n\t\t\"testdata/terragrunt-stack-init-error\", t.Name())\n\trequire.NoError(t, err)\n\n\toptions := &Options{\n\t\tTerragruntDir:    testFolder,\n\t\tTerragruntBinary: \"terragrunt\",\n\t}\n\n\tout, err := InitAndApplyE(t, options)\n\trequire.Error(t, err, \"InitAndApplyE should propagate init failure\")\n\trequire.Empty(t, out, \"Output should be empty when init fails\")\n\trequire.Contains(t, err.Error(), \"Missing expression\",\n\t\t\"Error should be from init, not apply\")\n}\n"
  },
  {
    "path": "modules/terragrunt/cmd.go",
    "content": "package terragrunt\n\nimport (\n\t\"fmt\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/gruntwork-io/terratest/modules/retry\"\n\t\"github.com/gruntwork-io/terratest/modules/shell\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n)\n\n// runTerragruntStackCommandE executes terragrunt stack commands\n// It handles argument construction, retry logic, and error handling for all stack commands\nfunc runTerragruntStackCommandE(\n\tt testing.TestingT,\n\topts *Options,\n\tsubCommand string,\n\tadditionalArgs ...string,\n) (string, error) {\n\t// Build the base command arguments starting with \"stack\"\n\tcommandArgs := []string{\"stack\"}\n\tif subCommand != \"\" {\n\t\tcommandArgs = append(commandArgs, subCommand)\n\t}\n\n\treturn executeTerragruntCommand(t, opts, commandArgs, additionalArgs...)\n}\n\n// runTerragruntCommandE is the core function that executes regular tg commands\n// It handles argument construction, retry logic, and error handling for non-stack commands\nfunc runTerragruntCommandE(\n\tt testing.TestingT,\n\topts *Options,\n\tcommand string,\n\tadditionalArgs ...string,\n) (string, error) {\n\t// Build the base command arguments starting with the command\n\tcommandArgs := []string{command}\n\n\treturn executeTerragruntCommand(t, opts, commandArgs, additionalArgs...)\n}\n\n// executeTerragruntCommand is the common execution function for all tg commands\n// It handles validation, argument construction, retry logic, and error handling\nfunc executeTerragruntCommand(t testing.TestingT, opts *Options, baseCommandArgs []string,\n\tadditionalArgs ...string) (string, error) {\n\t// Validate and prepare options\n\tif err := prepareOptions(opts); err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// Build args and generate command\n\tfinalArgs := buildTerragruntArgs(opts, append(baseCommandArgs, additionalArgs...)...)\n\texecCommand := generateCommand(opts, finalArgs...)\n\tcommandDescription := fmt.Sprintf(\"%s %v\", opts.TerragruntBinary, finalArgs)\n\n\t// Execute the command with retry logic and error handling\n\treturn retry.DoWithRetryableErrorsE(\n\t\tt,\n\t\tcommandDescription,\n\t\topts.RetryableTerraformErrors,\n\t\topts.MaxRetries,\n\t\topts.TimeBetweenRetries,\n\t\tfunc() (string, error) {\n\t\t\toutput, err := shell.RunCommandAndGetOutputE(t, execCommand)\n\t\t\tif err != nil {\n\t\t\t\treturn output, err\n\t\t\t}\n\n\t\t\t// Check for warnings that should be treated as errors\n\t\t\tif warningErr := hasWarning(opts, output); warningErr != nil {\n\t\t\t\treturn output, warningErr\n\t\t\t}\n\n\t\t\treturn output, nil\n\t\t},\n\t)\n}\n\n// hasWarning checks if the command output contains any warnings that should be treated as errors\n// It uses regex patterns defined in opts.WarningsAsErrors to match warning messages\nfunc hasWarning(opts *Options, commandOutput string) error {\n\tfor warningPattern, errorMessage := range opts.WarningsAsErrors {\n\t\t// Create a regex pattern to match warnings with the specified pattern\n\t\tregexPattern := fmt.Sprintf(\"\\nWarning: %s[^\\n]*\\n\", warningPattern)\n\t\tcompiledRegex, err := regexp.Compile(regexPattern)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"cannot compile regex for warning detection: %w\", err)\n\t\t}\n\n\t\t// Find all matches of the warning pattern in the output\n\t\tmatches := compiledRegex.FindAllString(commandOutput, -1)\n\t\tif len(matches) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\t// If warnings are found, return an error with the specified message\n\t\treturn fmt.Errorf(\"warning(s) were found: %s:\\n%s\", errorMessage, strings.Join(matches, \"\"))\n\t}\n\treturn nil\n}\n\n// prepareOptions validates options and sets defaults\nfunc prepareOptions(opts *Options) error {\n\tif err := validateOptions(opts); err != nil {\n\t\treturn err\n\t}\n\tif opts.TerragruntBinary == \"\" {\n\t\topts.TerragruntBinary = DefaultTerragruntBinary\n\t}\n\tsetTerragruntLogFormatting(opts)\n\treturn nil\n}\n\n// buildTerragruntArgs constructs the final argument list for a terragrunt command\n// Arguments are ordered as: TerragruntArgs → --non-interactive → commandArgs → TerraformArgs\nfunc buildTerragruntArgs(opts *Options, commandArgs ...string) []string {\n\tvar args []string\n\targs = append(args, opts.TerragruntArgs...)\n\targs = append(args, NonInteractiveFlag)\n\targs = append(args, commandArgs...)\n\n\tif len(opts.TerraformArgs) > 0 {\n\t\targs = append(args, opts.TerraformArgs...)\n\t}\n\n\treturn args\n}\n\n// validateOptions validates that required options are provided\nfunc validateOptions(opts *Options) error {\n\tif opts == nil {\n\t\treturn fmt.Errorf(\"options cannot be nil\")\n\t}\n\tif opts.TerragruntDir == \"\" {\n\t\treturn fmt.Errorf(\"TerragruntDir is required\")\n\t}\n\treturn nil\n}\n\n// defaultSuccessExitCode is the exit code returned when the OpenTofu/Terraform command succeeds\nconst defaultSuccessExitCode = 0\n\n// defaultErrorExitCode is the exit code returned when the OpenTofu/Terraform command fails\nconst defaultErrorExitCode = 1\n\n// getExitCodeForTerragruntCommandE runs terragrunt with the given arguments and options and returns exit code\nfunc getExitCodeForTerragruntCommandE(t testing.TestingT, additionalOptions *Options, additionalArgs ...string) (int, error) {\n\t// Validate and prepare options\n\tif err := prepareOptions(additionalOptions); err != nil {\n\t\treturn defaultErrorExitCode, err\n\t}\n\n\t// Build args and generate command\n\targs := buildTerragruntArgs(additionalOptions, additionalArgs...)\n\tadditionalOptions.Logger.Logf(t, \"Running terragrunt with args %v\", args)\n\tcmd := generateCommand(additionalOptions, args...)\n\t_, err := shell.RunCommandAndGetOutputE(t, cmd)\n\tif err == nil {\n\t\treturn defaultSuccessExitCode, nil\n\t}\n\texitCode, getExitCodeErr := shell.GetExitCodeForRunCommandError(err)\n\tif getExitCodeErr == nil {\n\t\treturn exitCode, nil\n\t}\n\treturn defaultErrorExitCode, getExitCodeErr\n}\n\n// buildRunArgs constructs the argument list for a terragrunt run command.\n// The -- separator disambiguates Terragrunt flags from OpenTofu/Terraform flags:\n//\n//\trun [tgArgs...] -- [tfArgs...]\nfunc buildRunArgs(tgArgs []string, tfArgs []string) []string {\n\tvar args []string\n\targs = append(args, tgArgs...)\n\targs = append(args, \"--\")\n\targs = append(args, tfArgs...)\n\treturn args\n}\n\n// generateCommand creates a shell.Command with the specified tg options and arguments\n// This function encapsulates the command creation logic for consistency\nfunc generateCommand(terragruntOptions *Options, commandArgs ...string) shell.Command {\n\treturn shell.Command{\n\t\tCommand:    terragruntOptions.TerragruntBinary,\n\t\tArgs:       commandArgs,\n\t\tWorkingDir: terragruntOptions.TerragruntDir,\n\t\tEnv:        terragruntOptions.EnvVars,\n\t\tLogger:     terragruntOptions.Logger,\n\t\tStdin:      terragruntOptions.Stdin,\n\t}\n}\n"
  },
  {
    "path": "modules/terragrunt/cmd_args_test.go",
    "content": "package terragrunt\n\nimport (\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/files\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// TestTerragruntArgsIncluded verifies that TerragruntArgs are actually passed to terragrunt (issue #1609).\n// This test uses a real terragrunt command to ensure the args are properly included.\nfunc TestTerragruntArgsIncluded(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerraformFolderToTemp(\n\t\t\"testdata/terragrunt-stack-init\", t.Name())\n\trequire.NoError(t, err)\n\n\toptions := &Options{\n\t\tTerragruntDir:    filepath.Join(testFolder, \"live\"),\n\t\tTerragruntBinary: \"terragrunt\",\n\t\t// Use --log-level which should affect the output\n\t\tTerragruntArgs: []string{\"--log-level\", \"error\"},\n\t}\n\n\t// Run init - if TerragruntArgs work, we should only see error-level logs\n\toutput, err := InitE(t, options)\n\trequire.NoError(t, err)\n\n\t// With --log-level error, we shouldn't see info-level messages\n\t// (Without the fix, --log-level would be ignored and we'd see info logs)\n\trequire.NotContains(t, output, \"level=info\",\n\t\t\"With --log-level error, info logs should not appear. If they do, TerragruntArgs are being ignored.\")\n}\n\n// TestTerraformArgsIncluded verifies that TerraformArgs are passed to the terraform command (issue #1609).\nfunc TestTerraformArgsIncluded(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerraformFolderToTemp(\n\t\t\"testdata/terragrunt-stack-init\", t.Name())\n\trequire.NoError(t, err)\n\n\toptions := &Options{\n\t\tTerragruntDir:    filepath.Join(testFolder, \"live\"),\n\t\tTerragruntBinary: \"terragrunt\",\n\t\t// Use -backend=false to disable backend initialization\n\t\t// This is a distinct terraform flag we can verify\n\t\tTerraformArgs: []string{\"-backend=false\"},\n\t}\n\n\t// Run init with -backend=false flag\n\toutput, err := InitE(t, options)\n\trequire.NoError(t, err)\n\n\t// With -backend=false, we should NOT see backend initialization messages\n\t// (Without the fix, -backend=false would be ignored and we'd see backend init)\n\trequire.NotContains(t, output, \"Initializing the backend\",\n\t\t\"With -backend=false, should not see backend initialization. If we do, TerraformArgs are being ignored.\")\n}\n\n// TestPlanExitCodeIncludesArgs verifies that PlanAllExitCodeE properly includes TerragruntArgs and TerraformArgs (issue #1609).\n// This test specifically checks the exit code functions which use getExitCodeForTerragruntCommandE.\nfunc TestPlanExitCodeIncludesArgs(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerragruntFolderToTemp(\"testdata/terragrunt-multi-plan\", t.Name())\n\trequire.NoError(t, err)\n\n\t// First apply so we have state\n\tbaseOptions := &Options{\n\t\tTerragruntDir:    testFolder,\n\t\tTerragruntBinary: \"terragrunt\",\n\t}\n\tdefer DestroyAll(t, baseOptions)\n\tApplyAll(t, baseOptions)\n\n\t// Now run plan with exit code AND TerragruntArgs\n\toptions := &Options{\n\t\tTerragruntDir:    testFolder,\n\t\tTerragruntBinary: \"terragrunt\",\n\t\t// Use --log-level to verify TerragruntArgs are included in exit code functions\n\t\tTerragruntArgs: []string{\"--log-level\", \"error\"},\n\t}\n\n\t// This should return exit code 0 (no changes) and should respect the log level\n\texitCode, err := PlanAllExitCodeE(t, options)\n\trequire.NoError(t, err)\n\trequire.Equal(t, 0, exitCode)\n\n\t// The key verification: If TerragruntArgs were ignored, we'd see info-level logs in the output.\n\t// Since we can't easily capture the output from the exit code function, we rely on the fact\n\t// that if the args were ignored, the function would have failed due to unexpected log output\n\t// affecting terragrunt's behavior. The fact that it succeeded with exit code 0 demonstrates\n\t// that --log-level error was properly passed.\n}\n\n// TestCombinedArgsOrdering verifies that both TerragruntArgs and TerraformArgs work together\n// in the correct order: TerragruntArgs → --non-interactive → command → TerraformArgs\nfunc TestCombinedArgsOrdering(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerraformFolderToTemp(\n\t\t\"testdata/terragrunt-stack-init\", t.Name())\n\trequire.NoError(t, err)\n\n\toptions := &Options{\n\t\tTerragruntDir:    filepath.Join(testFolder, \"live\"),\n\t\tTerragruntBinary: \"terragrunt\",\n\t\t// Combine both TerragruntArgs and TerraformArgs\n\t\tTerragruntArgs: []string{\"--log-level\", \"error\"},\n\t\tTerraformArgs:  []string{\"-backend=false\"},\n\t}\n\n\t// Run init - both args should be passed in the correct order\n\toutput, err := InitE(t, options)\n\trequire.NoError(t, err)\n\n\t// Verify TerragruntArgs effect: should not see info-level logs\n\trequire.NotContains(t, output, \"level=info\",\n\t\t\"With --log-level error, info logs should not appear\")\n\n\t// Verify TerraformArgs effect: should not see backend initialization\n\trequire.NotContains(t, output, \"Initializing the backend\",\n\t\t\"With -backend=false, should not see backend initialization\")\n}\n\n// TestValidateOptions verifies that validateOptions properly catches invalid configurations\nfunc TestValidateOptions(t *testing.T) {\n\tt.Parallel()\n\n\t// Test nil options\n\terr := validateOptions(nil)\n\trequire.Error(t, err)\n\trequire.Contains(t, err.Error(), \"options cannot be nil\")\n\n\t// Test missing TerragruntDir\n\terr = validateOptions(&Options{})\n\trequire.Error(t, err)\n\trequire.Contains(t, err.Error(), \"TerragruntDir is required\")\n\n\t// Test valid options\n\terr = validateOptions(&Options{\n\t\tTerragruntDir: \"/some/path\",\n\t})\n\trequire.NoError(t, err)\n}\n\n// TestBuildTerragruntArgs verifies the argument construction logic\nfunc TestBuildTerragruntArgs(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname         string\n\t\topts         *Options\n\t\tcommandArgs  []string\n\t\texpectedArgs []string\n\t\tdescription  string\n\t}{\n\t\t{\n\t\t\tname:         \"empty args\",\n\t\t\topts:         &Options{},\n\t\t\tcommandArgs:  []string{\"init\"},\n\t\t\texpectedArgs: []string{\"--non-interactive\", \"init\"},\n\t\t\tdescription:  \"Should add --non-interactive even with no custom args\",\n\t\t},\n\t\t{\n\t\t\tname: \"only terragrunt args\",\n\t\t\topts: &Options{\n\t\t\t\tTerragruntArgs: []string{\"--log-level\", \"error\"},\n\t\t\t},\n\t\t\tcommandArgs:  []string{\"init\"},\n\t\t\texpectedArgs: []string{\"--log-level\", \"error\", \"--non-interactive\", \"init\"},\n\t\t\tdescription:  \"Should place TerragruntArgs before --non-interactive\",\n\t\t},\n\t\t{\n\t\t\tname: \"only terraform args\",\n\t\t\topts: &Options{\n\t\t\t\tTerraformArgs: []string{\"-upgrade\"},\n\t\t\t},\n\t\t\tcommandArgs:  []string{\"init\"},\n\t\t\texpectedArgs: []string{\"--non-interactive\", \"init\", \"-upgrade\"},\n\t\t\tdescription:  \"Should place TerraformArgs after command\",\n\t\t},\n\t\t{\n\t\t\tname: \"both arg types\",\n\t\t\topts: &Options{\n\t\t\t\tTerragruntArgs: []string{\"--log-level\", \"error\", \"--no-color\"},\n\t\t\t\tTerraformArgs:  []string{\"-upgrade\", \"-backend=false\"},\n\t\t\t},\n\t\t\tcommandArgs:  []string{\"init\"},\n\t\t\texpectedArgs: []string{\"--log-level\", \"error\", \"--no-color\", \"--non-interactive\", \"init\", \"-upgrade\", \"-backend=false\"},\n\t\t\tdescription:  \"Should maintain correct order: TerragruntArgs → --non-interactive → command → TerraformArgs\",\n\t\t},\n\t\t{\n\t\t\tname: \"stack command with args\",\n\t\t\topts: &Options{\n\t\t\t\tTerragruntArgs: []string{\"--log-level\", \"error\"},\n\t\t\t\tTerraformArgs:  []string{\"plan\"},\n\t\t\t},\n\t\t\tcommandArgs:  []string{\"stack\", \"run\"},\n\t\t\texpectedArgs: []string{\"--log-level\", \"error\", \"--non-interactive\", \"stack\", \"run\", \"plan\"},\n\t\t\tdescription:  \"Should work with multi-part commands like 'stack run'\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\ttt := tt // capture range variable\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tactualArgs := buildTerragruntArgs(tt.opts, tt.commandArgs...)\n\t\t\trequire.Equal(t, tt.expectedArgs, actualArgs, tt.description)\n\t\t})\n\t}\n}\n\n// TestPrepareOptions verifies default value setting behavior\nfunc TestPrepareOptions(t *testing.T) {\n\tt.Parallel()\n\n\t// Test that default binary is set\n\topts := &Options{\n\t\tTerragruntDir: \"/some/path\",\n\t}\n\terr := prepareOptions(opts)\n\trequire.NoError(t, err)\n\trequire.Equal(t, DefaultTerragruntBinary, opts.TerragruntBinary)\n\n\t// Test that custom binary is preserved\n\topts = &Options{\n\t\tTerragruntDir:    \"/some/path\",\n\t\tTerragruntBinary: \"custom-terragrunt\",\n\t}\n\terr = prepareOptions(opts)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"custom-terragrunt\", opts.TerragruntBinary)\n\n\t// Test that validation errors propagate\n\terr = prepareOptions(&Options{})\n\trequire.Error(t, err)\n\trequire.Contains(t, err.Error(), \"TerragruntDir is required\")\n}\n\n// TestHasWarning verifies warning detection in command output\nfunc TestHasWarning(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname             string\n\t\twarningsAsErrors map[string]string\n\t\toutput           string\n\t\texpectError      bool\n\t\terrorContains    string\n\t}{\n\t\t{\n\t\t\tname:             \"nil map returns no error\",\n\t\t\twarningsAsErrors: nil,\n\t\t\toutput:           \"\\nWarning: something bad\\n\",\n\t\t\texpectError:      false,\n\t\t},\n\t\t{\n\t\t\tname:             \"empty map returns no error\",\n\t\t\twarningsAsErrors: map[string]string{},\n\t\t\toutput:           \"\\nWarning: something bad\\n\",\n\t\t\texpectError:      false,\n\t\t},\n\t\t{\n\t\t\tname:             \"matching warning returns error\",\n\t\t\twarningsAsErrors: map[string]string{\"something bad\": \"found a bad warning\"},\n\t\t\toutput:           \"some output\\nWarning: something bad happened\\nmore output\",\n\t\t\texpectError:      true,\n\t\t\terrorContains:    \"found a bad warning\",\n\t\t},\n\t\t{\n\t\t\tname:             \"no match returns no error\",\n\t\t\twarningsAsErrors: map[string]string{\"something bad\": \"found a bad warning\"},\n\t\t\toutput:           \"some output\\nno warnings here\\nmore output\",\n\t\t\texpectError:      false,\n\t\t},\n\t\t{\n\t\t\tname:             \"invalid regex returns error\",\n\t\t\twarningsAsErrors: map[string]string{\"(?P<\": \"bad regex\"},\n\t\t\toutput:           \"\\nWarning: anything\\n\",\n\t\t\texpectError:      true,\n\t\t\terrorContains:    \"cannot compile regex\",\n\t\t},\n\t\t{\n\t\t\tname: \"first matching pattern wins\",\n\t\t\twarningsAsErrors: map[string]string{\n\t\t\t\t\"alpha\": \"alpha error\",\n\t\t\t\t\"beta\":  \"beta error\",\n\t\t\t},\n\t\t\toutput:      \"\\nWarning: alpha problem\\n\\nWarning: beta problem\\n\",\n\t\t\texpectError: true,\n\t\t\t// We can't predict map iteration order, but one of these should match\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\ttt := tt\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\topts := &Options{WarningsAsErrors: tt.warningsAsErrors}\n\t\t\terr := hasWarning(opts, tt.output)\n\t\t\tif tt.expectError {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\tif tt.errorContains != \"\" {\n\t\t\t\t\trequire.Contains(t, err.Error(), tt.errorContains)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// TestEnvVarsPropagation verifies environment variables are passed through\nfunc TestEnvVarsPropagation(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerraformFolderToTemp(\n\t\t\"testdata/terragrunt-stack-init\", t.Name())\n\trequire.NoError(t, err)\n\n\t// Detect which IaC binary is available (terraform or tofu)\n\ttfBinary := \"terraform\"\n\tif _, err := exec.LookPath(\"terraform\"); err != nil {\n\t\t// terraform not found, try tofu\n\t\tif _, err := exec.LookPath(\"tofu\"); err != nil {\n\t\t\tt.Skip(\"Neither terraform nor tofu found in PATH\")\n\t\t}\n\t\ttfBinary = \"tofu\"\n\t}\n\n\toptions := &Options{\n\t\tTerragruntDir: filepath.Join(testFolder, \"live\"),\n\t\tEnvVars: map[string]string{\n\t\t\t\"TERRAGRUNT_TFPATH\": tfBinary, // Use whichever binary is available\n\t\t\t\"TG_LOG_LEVEL\":      \"error\",  // Alternative to --log-level flag\n\t\t},\n\t}\n\n\t// Run init - should succeed with env vars set\n\toutput, err := InitE(t, options)\n\trequire.NoError(t, err)\n\trequire.NotEmpty(t, output)\n\t// With TG_LOG_LEVEL=error, should not see info logs\n\trequire.NotContains(t, output, \"level=info\")\n}\n"
  },
  {
    "path": "modules/terragrunt/destroy.go",
    "content": "package terragrunt\n\nimport (\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// DestroyAll runs terragrunt run --all destroy with the given options and returns stdout.\nfunc DestroyAll(t testing.TestingT, options *Options) string {\n\tout, err := DestroyAllE(t, options)\n\trequire.NoError(t, err)\n\treturn out\n}\n\n// DestroyAllE runs terragrunt run --all -- destroy with the given options and returns stdout.\nfunc DestroyAllE(t testing.TestingT, options *Options) (string, error) {\n\targs := buildRunArgs([]string{\"--all\"}, []string{\"destroy\", \"-auto-approve\", \"-input=false\"})\n\treturn runTerragruntCommandE(t, options, \"run\", args...)\n}\n\n// Destroy runs terragrunt run destroy for a single unit and returns stdout/stderr.\nfunc Destroy(t testing.TestingT, options *Options) string {\n\tout, err := DestroyE(t, options)\n\trequire.NoError(t, err)\n\treturn out\n}\n\n// DestroyE runs terragrunt run -- destroy for a single unit and returns stdout/stderr.\nfunc DestroyE(t testing.TestingT, options *Options) (string, error) {\n\targs := buildRunArgs([]string{}, []string{\"destroy\", \"-auto-approve\", \"-input=false\"})\n\treturn runTerragruntCommandE(t, options, \"run\", args...)\n}\n"
  },
  {
    "path": "modules/terragrunt/destroy_test.go",
    "content": "package terragrunt\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/files\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestDestroyAll(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerragruntFolderToTemp(\"testdata/terragrunt-no-error\", t.Name())\n\trequire.NoError(t, err)\n\n\toptions := &Options{\n\t\tTerragruntDir:    testFolder,\n\t\tTerragruntBinary: \"terragrunt\",\n\t}\n\n\tApplyAll(t, options)\n\tdestroyOut := DestroyAll(t, options)\n\trequire.NotEmpty(t, destroyOut)\n}\n\nfunc TestDestroy(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerragruntFolderToTemp(\"testdata/terragrunt-no-error\", t.Name())\n\trequire.NoError(t, err)\n\n\toptions := &Options{\n\t\tTerragruntDir:    testFolder,\n\t\tTerragruntBinary: \"terragrunt\",\n\t}\n\n\tApply(t, options)\n\tdestroyOut := Destroy(t, options)\n\trequire.NotEmpty(t, destroyOut)\n}\n\n// TestDestroyAllWithArgs verifies DestroyAll respects TerragruntArgs\nfunc TestDestroyAllWithArgs(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerragruntFolderToTemp(\"testdata/terragrunt-multi-plan\", t.Name())\n\trequire.NoError(t, err)\n\n\t// Apply first\n\tApplyAll(t, &Options{\n\t\tTerragruntDir:    testFolder,\n\t\tTerragruntBinary: \"terragrunt\",\n\t})\n\n\t// Destroy with TerragruntArgs\n\toptions := &Options{\n\t\tTerragruntDir:    testFolder,\n\t\tTerragruntBinary: \"terragrunt\",\n\t\tTerragruntArgs:   []string{\"--log-level\", \"error\"},\n\t}\n\n\tdestroyOut := DestroyAll(t, options)\n\trequire.NotEmpty(t, destroyOut)\n\t// With --log-level error, should not see info logs\n\trequire.NotContains(t, destroyOut, \"level=info\")\n}\n"
  },
  {
    "path": "modules/terragrunt/format.go",
    "content": "package terragrunt\n\nimport (\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// FormatAll runs terragrunt hcl format to format all terragrunt.hcl files and returns stdout/stderr\nfunc FormatAll(t testing.TestingT, options *Options) string {\n\tout, err := FormatAllE(t, options)\n\trequire.NoError(t, err)\n\treturn out\n}\n\n// FormatAllE runs terragrunt hcl format to format all terragrunt.hcl files and returns stdout/stderr\nfunc FormatAllE(t testing.TestingT, options *Options) (string, error) {\n\treturn runTerragruntCommandE(t, options, \"hcl\", \"format\")\n}\n"
  },
  {
    "path": "modules/terragrunt/format_test.go",
    "content": "package terragrunt\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/files\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestFormatAll(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerragruntFolderToTemp(\"testdata/terragrunt-multi-plan\", t.Name())\n\trequire.NoError(t, err)\n\n\t// Create an unformatted terragrunt.hcl file in foo directory\n\tunformattedContent := `terraform {\nsource = \"git::git@github.com:foo/modules.git//app\"\n}\ninputs={\nfoo=\"bar\"\n}`\n\ttgFile := filepath.Join(testFolder, \"foo\", \"terragrunt.hcl\")\n\terr = os.WriteFile(tgFile, []byte(unformattedContent), 0644)\n\trequire.NoError(t, err)\n\n\toptions := &Options{\n\t\tTerragruntDir:    testFolder,\n\t\tTerragruntBinary: \"terragrunt\",\n\t}\n\n\t// Run format command\n\tFormatAll(t, options)\n\n\t// Read the formatted file to verify it was actually formatted\n\tformattedContent, err := os.ReadFile(tgFile)\n\trequire.NoError(t, err)\n\n\t// Verify the file was formatted (should have proper spacing now)\n\trequire.Contains(t, string(formattedContent), `source = \"git::git@github.com:foo/modules.git//app\"`,\n\t\t\"Expected file to be formatted with proper spacing\")\n\trequire.Contains(t, string(formattedContent), `inputs = {`,\n\t\t\"Expected inputs block to be formatted with spaces around =\")\n\trequire.Contains(t, string(formattedContent), `foo = \"bar\"`,\n\t\t\"Expected key-value pairs to be formatted with spaces around =\")\n}\n\nfunc TestFormatAllE(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerragruntFolderToTemp(\"testdata/terragrunt-multi-plan\", t.Name())\n\trequire.NoError(t, err)\n\n\t// Create an unformatted file to ensure the command actually does something\n\tunformattedContent := `terraform {\nsource = \"git::git@github.com:foo/modules.git//app\"\n}\ninputs={\nfoo=\"bar\"\n}`\n\ttgFile := filepath.Join(testFolder, \"foo\", \"terragrunt.hcl\")\n\terr = os.WriteFile(tgFile, []byte(unformattedContent), 0644)\n\trequire.NoError(t, err)\n\n\toptions := &Options{\n\t\tTerragruntDir:    testFolder,\n\t\tTerragruntBinary: \"terragrunt\",\n\t}\n\n\t// Run format command - should succeed\n\t_, err = FormatAllE(t, options)\n\trequire.NoError(t, err)\n\n\t// Verify the file was actually formatted by reading it\n\tformattedContent, err := os.ReadFile(tgFile)\n\trequire.NoError(t, err)\n\trequire.Contains(t, string(formattedContent), `inputs = {`,\n\t\t\"File should be formatted with proper spacing\")\n}\n"
  },
  {
    "path": "modules/terragrunt/graph.go",
    "content": "package terragrunt\n\nimport (\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// Graph runs terragrunt dag graph and returns the DOT-format dependency graph.\n// This is useful for verifying dependency relationships between terragrunt units.\nfunc Graph(t testing.TestingT, options *Options) string {\n\tout, err := GraphE(t, options)\n\trequire.NoError(t, err)\n\treturn out\n}\n\n// GraphE runs terragrunt dag graph and returns the DOT-format dependency graph.\n// This is useful for verifying dependency relationships between terragrunt units.\n// Log lines are stripped from the output so the result is clean DOT format.\nfunc GraphE(t testing.TestingT, options *Options) (string, error) {\n\trawOutput, err := runTerragruntCommandE(t, options, \"dag\", \"graph\")\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn filterLogLines(rawOutput), nil\n}\n"
  },
  {
    "path": "modules/terragrunt/graph_test.go",
    "content": "package terragrunt\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/files\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestGraph(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerragruntFolderToTemp(\"testdata/terragrunt-multi-plan\", t.Name())\n\trequire.NoError(t, err)\n\n\toutput := Graph(t, &Options{\n\t\tTerragruntDir:    testFolder,\n\t\tTerragruntBinary: \"terragrunt\",\n\t})\n\n\trequire.Contains(t, output, \"digraph\")\n\trequire.Contains(t, output, `\"foo\"`)\n\trequire.Contains(t, output, `\"bar\"`)\n}\n\nfunc TestGraphE_InvalidConfig(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := t.TempDir()\n\trequire.NoError(t, os.WriteFile(filepath.Join(tmpDir, \"terragrunt.hcl\"), []byte(\"not_valid!!!\"), 0644))\n\n\toutput, err := GraphE(t, &Options{TerragruntDir: tmpDir})\n\trequire.NoError(t, err)\n\trequire.Contains(t, output, \"digraph\")\n\t// Invalid config produces a minimal graph with just the current unit\n\trequire.NotContains(t, output, \"->\")\n}\n"
  },
  {
    "path": "modules/terragrunt/hcl_validate.go",
    "content": "package terragrunt\n\nimport (\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// HclValidate runs terragrunt hcl validate to check terragrunt.hcl syntax.\n// This validates Terragrunt HCL configuration and can check for mis-aligned inputs.\n// Use TerraformArgs to pass additional flags like \"--inputs\" or \"--strict\".\n//\n// Examples:\n//\n//\tHclValidate(t, options)                                        // Basic syntax check\n//\tHclValidate(t, &Options{TerraformArgs: []string{\"--inputs\"}})  // Check input alignment\nfunc HclValidate(t testing.TestingT, options *Options) string {\n\tout, err := HclValidateE(t, options)\n\trequire.NoError(t, err)\n\treturn out\n}\n\n// HclValidateE runs terragrunt hcl validate to check terragrunt.hcl syntax.\n// This validates Terragrunt HCL configuration and can check for mis-aligned inputs.\n// Use TerraformArgs to pass additional flags like \"--inputs\" or \"--strict\".\nfunc HclValidateE(t testing.TestingT, options *Options) (string, error) {\n\treturn runTerragruntCommandE(t, options, \"hcl\", \"validate\")\n}\n"
  },
  {
    "path": "modules/terragrunt/hcl_validate_test.go",
    "content": "package terragrunt\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/files\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestHclValidate(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerragruntFolderToTemp(\"testdata/terragrunt-multi-plan\", t.Name())\n\trequire.NoError(t, err)\n\n\tHclValidate(t, &Options{\n\t\tTerragruntDir:    testFolder,\n\t\tTerragruntBinary: \"terragrunt\",\n\t})\n}\n\nfunc TestHclValidateE_InvalidConfig(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := t.TempDir()\n\trequire.NoError(t, os.WriteFile(filepath.Join(tmpDir, \"terragrunt.hcl\"), []byte(\"not_valid!!!\"), 0644))\n\n\t_, err := HclValidateE(t, &Options{TerragruntDir: tmpDir})\n\trequire.Error(t, err)\n}\n"
  },
  {
    "path": "modules/terragrunt/init.go",
    "content": "package terragrunt\n\nimport (\n\t\"github.com/gruntwork-io/terratest/internal/lib/formatting\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// Init calls terragrunt run init and return stdout/stderr\nfunc Init(t testing.TestingT, options *Options) string {\n\tout, err := InitE(t, options)\n\trequire.NoError(t, err)\n\treturn out\n}\n\n// InitE calls terragrunt run -- init and return stdout/stderr\nfunc InitE(t testing.TestingT, options *Options) (string, error) {\n\targs := buildRunArgs([]string{}, append([]string{\"init\"}, initArgs(options)...))\n\treturn runTerragruntCommandE(t, options, \"run\", args...)\n}\n\n// initArgs builds the argument list for terragrunt init command.\n// This function handles complex configuration that requires special formatting.\nfunc initArgs(options *Options) []string {\n\tvar args []string\n\n\t// Add complex configuration that requires special formatting\n\t// These are OpenTofu/Terraform-specific arguments that need special formatting\n\targs = append(args, formatting.FormatBackendConfigAsArgs(options.BackendConfig)...)\n\targs = append(args, formatting.FormatPluginDirAsArgs(options.PluginDir)...)\n\treturn args\n}\n"
  },
  {
    "path": "modules/terragrunt/init_test.go",
    "content": "package terragrunt\n\nimport (\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/files\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestInit(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerraformFolderToTemp(\n\t\t\"testdata/terragrunt-no-error\", t.Name())\n\trequire.NoError(t, err)\n\n\tout := Init(t, &Options{\n\t\tTerragruntDir:    testFolder,\n\t\tTerragruntBinary: \"terragrunt\",\n\t\tTerraformArgs:    []string{\"-upgrade=true\"},\n\t})\n\t// Check for common success indicator (works with both Terraform and OpenTofu)\n\trequire.Contains(t, out, \"successfully initialized\")\n}\n\nfunc TestInitE(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerraformFolderToTemp(\n\t\t\"testdata/terragrunt-no-error\", t.Name())\n\trequire.NoError(t, err)\n\n\tout, err := InitE(t, &Options{\n\t\tTerragruntDir:    testFolder,\n\t\tTerragruntBinary: \"terragrunt\",\n\t\tTerraformArgs:    []string{\"-upgrade=true\"}, // Common terraform init flag\n\t})\n\trequire.NoError(t, err)\n\t// Check for common success indicator (works with both Terraform and OpenTofu)\n\trequire.Contains(t, out, \"successfully initialized\")\n}\n\nfunc TestInitWithInvalidConfig(t *testing.T) {\n\tt.Parallel()\n\t// Test error handling when tg.hcl has invalid HCL syntax\n\ttestFolder, err := files.CopyTerraformFolderToTemp(\n\t\t\"testdata/terragrunt-stack-init-error\", t.Name())\n\trequire.NoError(t, err)\n\n\t// This should fail due to invalid HCL syntax in tg.hcl\n\t_, err = InitE(t, &Options{\n\t\tTerragruntDir:    testFolder,\n\t\tTerragruntBinary: \"terragrunt\",\n\t\tTerraformArgs:    []string{\"-upgrade=true\"}, // Common terraform init flag\n\t})\n\trequire.Error(t, err)\n\t// The error should contain information about the HCL parsing error\n\trequire.Contains(t, err.Error(), \"Missing expression\")\n}\n\n// TestInitWithBothArgTypes verifies init works with both TerragruntArgs and TerraformArgs\nfunc TestInitWithBothArgTypes(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerraformFolderToTemp(\n\t\t\"testdata/terragrunt-stack-init\", t.Name())\n\trequire.NoError(t, err)\n\n\toptions := &Options{\n\t\tTerragruntDir:    filepath.Join(testFolder, \"live\"),\n\t\tTerragruntBinary: \"terragrunt\",\n\t\tTerragruntArgs:   []string{\"--log-level\", \"error\"},\n\t\tTerraformArgs:    []string{\"-upgrade\"},\n\t}\n\n\toutput, err := InitE(t, options)\n\trequire.NoError(t, err)\n\t// Verify TerragruntArgs: no info logs\n\trequire.NotContains(t, output, \"level=info\")\n\t// Verify TerraformArgs: -upgrade was passed (shows in terraform output)\n\trequire.Contains(t, output, \"Initializing\")\n}\n"
  },
  {
    "path": "modules/terragrunt/json_helpers.go",
    "content": "package terragrunt\n\nimport (\n\t\"encoding/json\"\n\t\"regexp\"\n\t\"strings\"\n)\n\n// removeLogLines removes terragrunt log lines and metadata from output\nfunc removeLogLines(rawOutput string) string {\n\tlines := strings.Split(rawOutput, \"\\n\")\n\tvar result []string\n\tfor _, line := range lines {\n\t\ttrimmed := strings.TrimSpace(line)\n\t\t// Skip empty lines, terragrunt log lines, and metadata lines\n\t\tif trimmed == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif isLogLine(trimmed) || isMetadataLine(trimmed) {\n\t\t\tcontinue\n\t\t}\n\t\tresult = append(result, trimmed)\n\t}\n\n\treturn strings.Join(result, \"\\n\")\n}\n\n// isMetadataLine checks if a line is terragrunt metadata (e.g., \"Group 1\", \"- Unit ./foo\")\nfunc isMetadataLine(line string) bool {\n\treturn tgMetadataPattern.MatchString(line)\n}\n\n// newLogLinePattern matches the new terragrunt log format: \"HH:MM:SS.mmm LEVEL ...\"\n// Example: \"20:41:53.564 INFO   Generating unit father...\"\nvar newLogLinePattern = regexp.MustCompile(`^\\d{2}:\\d{2}:\\d{2}\\.\\d{3}\\s+(INFO|WARN|ERROR|DEBUG|TRACE|STDOUT|STDERR)\\s+`)\n\n// tgMetadataPattern matches terragrunt metadata lines like \"Group 1\" or \"- Unit ./foo\"\nvar tgMetadataPattern = regexp.MustCompile(`^(Group \\d+|- Unit )`)\n\n// isLogLine checks if a line is a terragrunt log line\nfunc isLogLine(line string) bool {\n\t// Old format: time=... level=... msg=...\n\tif strings.HasPrefix(line, \"time=\") && strings.Contains(line, \"level=\") && strings.Contains(line, \"msg=\") {\n\t\treturn true\n\t}\n\t// New format (terragrunt 0.88+): HH:MM:SS.mmm LEVEL message\n\treturn newLogLinePattern.MatchString(line)\n}\n\n// extractJsonContent extracts only JSON objects from terragrunt output,\n// filtering out log lines and other non-JSON content like \"Group 1\" or \"- Unit ./foo\".\n// Uses json.Decoder to correctly handle braces inside JSON string values.\nfunc extractJsonContent(rawOutput string) (string, error) {\n\tlines := strings.Split(rawOutput, \"\\n\")\n\tvar filtered []string\n\tfor _, line := range lines {\n\t\ttrimmed := strings.TrimSpace(line)\n\t\tif trimmed == \"\" || isLogLine(trimmed) || isMetadataLine(trimmed) {\n\t\t\tcontinue\n\t\t}\n\t\tfiltered = append(filtered, trimmed)\n\t}\n\n\tremaining := strings.Join(filtered, \"\\n\")\n\tif remaining == \"\" {\n\t\treturn \"\", nil\n\t}\n\n\tdec := json.NewDecoder(strings.NewReader(remaining))\n\tvar results []string\n\tfor dec.More() {\n\t\tvar raw json.RawMessage\n\t\tif err := dec.Decode(&raw); err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\tresults = append(results, string(raw))\n\t}\n\treturn strings.Join(results, \"\\n\"), nil\n}\n\n// cleanTerragruntOutput extracts the actual output value from terragrunt stack's verbose output\n//\n// Example input (raw tg output):\n//\n//\ttime=2023-07-11T10:30:45Z level=info prefix=foo tf-path=terraform msg=Initializing...\n//\ttime=2023-07-11T10:30:46Z level=info prefix=foo tf-path=terraform msg=Running command...\n//\t\"my-bucket-name\"\n//\n// Example output (cleaned):\n//\n//\tmy-bucket-name\n//\n// For JSON values, it preserves the structure:\n// Input:\n//\n//\ttime=2023-07-11T10:30:45Z level=info prefix=foo tf-path=terraform msg=Running...\n//\t{\"vpc_id\": \"vpc-12345\", \"subnet_ids\": [\"subnet-1\", \"subnet-2\"]}\n//\n// Output:\n//\n//\t{\"vpc_id\": \"vpc-12345\", \"subnet_ids\": [\"subnet-1\", \"subnet-2\"]}\nfunc cleanTerragruntOutput(rawOutput string) (string, error) {\n\t// Remove terragrunt log lines and metadata\n\tfinalOutput := removeLogLines(rawOutput)\n\tif finalOutput == \"\" {\n\t\treturn \"\", nil\n\t}\n\n\t// Check if it's JSON (starts with { or [)\n\tif strings.HasPrefix(finalOutput, \"{\") || strings.HasPrefix(finalOutput, \"[\") {\n\t\t// For JSON output, return as-is\n\t\treturn finalOutput, nil\n\t}\n\n\t// For simple values, remove surrounding quotes if present\n\t// Use TrimPrefix/TrimSuffix to remove exactly one quote from each end\n\tif strings.HasPrefix(finalOutput, \"\\\"\") && strings.HasSuffix(finalOutput, \"\\\"\") {\n\t\tfinalOutput = strings.TrimPrefix(finalOutput, \"\\\"\")\n\t\tfinalOutput = strings.TrimSuffix(finalOutput, \"\\\"\")\n\t}\n\n\treturn finalOutput, nil\n}\n\n// cleanTerragruntJson cleans the JSON output from a terragrunt stack command that\n// returns a single combined JSON object. Returns an error if the output contains\n// multiple JSON objects (use extractJsonContent directly for multi-object output).\n//\n// Example input (raw tg JSON output):\n//\n//\ttime=2023-07-11T10:30:45Z level=info prefix=mother tf-path=terraform msg=Initializing...\n//\ttime=2023-07-11T10:30:46Z level=info prefix=mother tf-path=terraform msg=Running command...\n//\t{\"mother\":{\"output\":\"./test.txt\"},\"father\":{\"output\":\"./test.txt\"}}\n//\n// Example output (cleaned and formatted):\n//\n//\t{\n//\t  \"mother\": {\n//\t    \"output\": \"./test.txt\"\n//\t  },\n//\t  \"father\": {\n//\t    \"output\": \"./test.txt\"\n//\t  }\n//\t}\nfunc cleanTerragruntJson(input string) (string, error) {\n\t// Extract only JSON content, filtering out log lines and other non-JSON content\n\tcleaned, err := extractJsonContent(input)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// Parse JSON\n\tvar jsonObj interface{}\n\tif err := json.Unmarshal([]byte(cleaned), &jsonObj); err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// Format JSON output with indentation\n\tnormalized, err := json.MarshalIndent(jsonObj, \"\", \"  \")\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn string(normalized), nil\n}\n"
  },
  {
    "path": "modules/terragrunt/json_helpers_test.go",
    "content": "package terragrunt\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestIsLogLine(t *testing.T) {\n\tt.Parallel()\n\n\t// Old format (time=... level=... msg=...)\n\tassert.True(t, isLogLine(\"time=2026 level=info prefix=foo tf-path=terraform msg=Running\"))\n\n\t// New format (HH:MM:SS.mmm LEVEL ...)\n\tassert.True(t, isLogLine(\"20:41:53.564 INFO   Generating unit father\"))\n\tassert.True(t, isLogLine(\"20:41:53.564 WARN   Something is off\"))\n\tassert.True(t, isLogLine(\"20:41:53.564 DEBUG  Detailed info\"))\n\tassert.True(t, isLogLine(\"20:41:53.564 STDOUT [.terragrunt-stack/mother] terraform: output\"))\n\tassert.True(t, isLogLine(\"20:41:53.564 STDERR [foo] error message\"))\n\tassert.True(t, isLogLine(\"20:41:53.564 ERROR  Something went wrong\"))\n\tassert.True(t, isLogLine(\"20:41:53.564 TRACE  Very detailed\"))\n\n\t// Not log lines\n\tassert.False(t, isLogLine(`{\"key\": \"value\"}`))\n\tassert.False(t, isLogLine(`{\"message\": \"error msg=bad\"}`))\n\tassert.False(t, isLogLine(\"Group 1\"))\n\tassert.False(t, isLogLine(\"- Unit ./foo\"))\n}\n\nfunc TestIsMetadataLine(t *testing.T) {\n\tt.Parallel()\n\n\t// Metadata lines\n\tassert.True(t, isMetadataLine(\"Group 1\"))\n\tassert.True(t, isMetadataLine(\"Group 42\"))\n\tassert.True(t, isMetadataLine(\"- Unit ./foo\"))\n\tassert.True(t, isMetadataLine(\"- Unit ./.terragrunt-stack/mother\"))\n\n\t// Not metadata lines\n\tassert.False(t, isMetadataLine(`{\"key\": \"value\"}`))\n\tassert.False(t, isMetadataLine(\"mother = { output = \\\"./test.txt\\\" }\"))\n\tassert.False(t, isMetadataLine(\"20:41:53.564 INFO   Running\"))\n}\n\nfunc TestRemoveLogLines(t *testing.T) {\n\tt.Parallel()\n\n\t// Removes old format log lines, keeps JSON\n\tresult := removeLogLines(\"time=2026 level=info msg=Start\\n{\\\"key\\\": \\\"value\\\"}\")\n\tassert.Equal(t, `{\"key\": \"value\"}`, result)\n\n\t// Removes new format log lines\n\tresult = removeLogLines(\"20:41:53.564 INFO   Running\\n{\\\"key\\\": \\\"value\\\"}\")\n\tassert.Equal(t, `{\"key\": \"value\"}`, result)\n\n\t// Removes metadata lines (Group, Unit)\n\tresult = removeLogLines(\"Group 1\\n- Unit ./foo\\n{\\\"key\\\": \\\"value\\\"}\")\n\tassert.Equal(t, `{\"key\": \"value\"}`, result)\n\n\t// Preserves JSON with msg= in value\n\tresult = removeLogLines(\"time=2026 level=info msg=Start\\n{\\\"message\\\": \\\"error msg=bad\\\"}\")\n\tassert.Contains(t, result, \"error msg=bad\")\n}\n\nfunc TestExtractJsonContent(t *testing.T) {\n\tt.Parallel()\n\n\t// Extracts JSON with old format, filters non-JSON\n\tinput := \"time=2026 level=info msg=Running\\nGroup 1\\n- Unit ./foo\\n{\\\"a\\\": 1}\\n{\\\"b\\\": 2}\"\n\tresult, err := extractJsonContent(input)\n\trequire.NoError(t, err)\n\tassert.Contains(t, result, `\"a\": 1`)\n\tassert.Contains(t, result, `\"b\": 2`)\n\tassert.NotContains(t, result, \"Group\")\n\tassert.NotContains(t, result, \"Unit\")\n\n\t// Extracts JSON with new format logs\n\tinput = \"20:41:53.564 INFO   Running\\n20:41:53.564 STDOUT terraform: done\\n{\\\"key\\\": \\\"value\\\"}\"\n\tresult, err = extractJsonContent(input)\n\trequire.NoError(t, err)\n\tassert.Equal(t, `{\"key\": \"value\"}`, result)\n\n\t// Handles nested JSON\n\tinput = \"time=2026 level=info msg=Running\\n{\\n  \\\"outer\\\": {\\n    \\\"inner\\\": true\\n  }\\n}\"\n\tresult, err = extractJsonContent(input)\n\trequire.NoError(t, err)\n\tassert.Contains(t, result, `\"inner\": true`)\n\n\t// Empty when only logs/metadata\n\tinput = \"20:41:53.564 INFO   Running\\nGroup 1\\n- Unit ./foo\"\n\tresult, err = extractJsonContent(input)\n\trequire.NoError(t, err)\n\tassert.Equal(t, \"\", result)\n}\n\nfunc TestCleanTerragruntOutput(t *testing.T) {\n\tt.Parallel()\n\n\t// Simple quoted string value\n\tinput := \"time=2026 level=info msg=Running\\n\\\"my-bucket-name\\\"\"\n\tresult, err := cleanTerragruntOutput(input)\n\trequire.NoError(t, err)\n\tassert.Equal(t, \"my-bucket-name\", result)\n\n\t// JSON output preserved\n\tinput = \"20:41:53.564 INFO   Running\\n{\\\"key\\\": \\\"value\\\"}\"\n\tresult, err = cleanTerragruntOutput(input)\n\trequire.NoError(t, err)\n\tassert.Equal(t, `{\"key\": \"value\"}`, result)\n\n\t// Filters metadata lines\n\tinput = \"Group 1\\n- Unit ./foo\\n\\\"result\\\"\"\n\tresult, err = cleanTerragruntOutput(input)\n\trequire.NoError(t, err)\n\tassert.Equal(t, \"result\", result)\n\n\t// Empty input returns empty\n\tinput = \"20:41:53.564 INFO   Running\"\n\tresult, err = cleanTerragruntOutput(input)\n\trequire.NoError(t, err)\n\tassert.Equal(t, \"\", result)\n}\n\nfunc TestCleanTerragruntJson(t *testing.T) {\n\tt.Parallel()\n\n\t// Valid single JSON with old format logs\n\tinput := \"time=2026 level=info msg=Running\\n{\\\"mother\\\":{\\\"output\\\":\\\"test\\\"}}\"\n\tresult, err := cleanTerragruntJson(input)\n\trequire.NoError(t, err)\n\tassert.Contains(t, result, \"mother\")\n\n\t// Valid single JSON with new format logs (terragrunt 0.88+)\n\tinput = \"{\\\"a\\\": 1}\\n20:41:53.564 INFO   Generating unit\\n20:41:53.564 STDOUT terraform: done\"\n\tresult, err = cleanTerragruntJson(input)\n\trequire.NoError(t, err)\n\tassert.Contains(t, result, `\"a\": 1`)\n\n\t// Multiple JSON objects should error\n\t_, err = cleanTerragruntJson(\"{\\\"a\\\": 1}\\n{\\\"b\\\": 2}\")\n\trequire.Error(t, err)\n\n\t// Empty/no-JSON input should error (documents expected behavior)\n\t_, err = cleanTerragruntJson(\"20:41:53.564 INFO   Running\\nGroup 1\\n- Unit ./foo\")\n\trequire.Error(t, err, \"cleanTerragruntJson should error when input contains no JSON\")\n}\n\nfunc TestCleanTerragruntOutputEdgeCases(t *testing.T) {\n\tt.Parallel()\n\n\t// Empty string value (terraform outputs \"\" for empty strings)\n\tinput := \"time=2026 level=info msg=Running\\n\\\"\\\"\"\n\tresult, err := cleanTerragruntOutput(input)\n\trequire.NoError(t, err)\n\tassert.Equal(t, \"\", result, \"Empty quoted string should become empty string\")\n\n\t// Value with quotes inside (terraform outputs \"\\\"quoted\\\"\")\n\tinput = \"20:41:53.564 INFO   Running\\n\\\"\\\\\\\"quoted\\\\\\\"\\\"\"\n\tresult, err = cleanTerragruntOutput(input)\n\trequire.NoError(t, err)\n\tassert.Equal(t, \"\\\\\\\"quoted\\\\\\\"\", result, \"Escaped quotes should be preserved\")\n\n\t// Multiple lines of non-JSON content after filtering logs\n\tinput = \"20:41:53.564 INFO   Running\\nline1\\nline2\"\n\tresult, err = cleanTerragruntOutput(input)\n\trequire.NoError(t, err)\n\tassert.Equal(t, \"line1\\nline2\", result)\n\n\t// Mismatched quotes: opening quote without closing quote should be left as-is\n\tinput = \"20:41:53.564 INFO   Running\\n\\\"no-closing-quote\"\n\tresult, err = cleanTerragruntOutput(input)\n\trequire.NoError(t, err)\n\tassert.Equal(t, \"\\\"no-closing-quote\", result, \"Mismatched quotes should be preserved verbatim\")\n\n\t// Closing quote without opening quote should be left as-is\n\tinput = \"20:41:53.564 INFO   Running\\nno-opening-quote\\\"\"\n\tresult, err = cleanTerragruntOutput(input)\n\trequire.NoError(t, err)\n\tassert.Equal(t, \"no-opening-quote\\\"\", result, \"Mismatched quotes should be preserved verbatim\")\n\n\t// Array JSON output preserved\n\tinput = \"20:41:53.564 INFO   Running\\n[\\\"a\\\", \\\"b\\\"]\"\n\tresult, err = cleanTerragruntOutput(input)\n\trequire.NoError(t, err)\n\tassert.Equal(t, `[\"a\", \"b\"]`, result, \"JSON array should be preserved\")\n}\n\nfunc TestExtractJsonContentMalformedJson(t *testing.T) {\n\tt.Parallel()\n\n\t// Valid JSON followed by malformed JSON: returns error\n\tinput := \"{\\\"valid\\\": true}\\n{broken json\"\n\t_, err := extractJsonContent(input)\n\tassert.Error(t, err)\n\n\t// Malformed JSON only: returns error\n\tinput = \"{not valid json at all\"\n\t_, err = extractJsonContent(input)\n\tassert.Error(t, err)\n\n\t// Valid JSON with log lines before and after (realistic scenario)\n\tinput = \"time=2026 level=info msg=Before\\n{\\\"key\\\": 1}\\ntime=2026 level=info msg=After\"\n\tresult, err := extractJsonContent(input)\n\trequire.NoError(t, err)\n\tassert.Contains(t, result, `\"key\"`)\n\tassert.NotContains(t, result, \"Before\")\n\tassert.NotContains(t, result, \"After\")\n\n\t// Whitespace-only after filtering logs\n\tinput = \"20:41:53.564 INFO   Running\\n   \\n  \"\n\tresult, err = extractJsonContent(input)\n\trequire.NoError(t, err)\n\tassert.Equal(t, \"\", result)\n\n\t// Two valid JSON objects separated by log lines\n\tinput = \"20:41:53.564 INFO   Start\\n{\\\"a\\\": 1}\\n20:41:53.564 INFO   Middle\\n{\\\"b\\\": 2}\\n20:41:53.564 INFO   End\"\n\tresult, err = extractJsonContent(input)\n\trequire.NoError(t, err)\n\tassert.Contains(t, result, `\"a\"`)\n\tassert.Contains(t, result, `\"b\"`)\n}\n"
  },
  {
    "path": "modules/terragrunt/options.go",
    "content": "package terragrunt\n\nimport (\n\t\"io\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/gruntwork-io/terratest/modules/logger\"\n)\n\n// Key concepts:\n// - Options: Configure HOW the test framework executes tg (directories, retry logic, logging)\n// - TerragruntArgs: Global terragrunt flags (e.g., --log-level, --no-color)\n// - TerraformArgs: Command-specific OpenTofu/Terraform args (e.g., -upgrade for init, or the command itself for stack run)\n// - Use Options.TerragruntDir to specify WHERE to run tg\n//\n// Example:\n//\n//\t// For init with OpenTofu/Terraform flags\n//\tInitE(t, &Options{\n//\t    TerragruntDir: \"/path/to/config\",\n//\t    TerragruntArgs: []string{\"--log-level\", \"info\"},\n//\t    TerraformArgs: []string{\"-upgrade=true\"},\n//\t})\n//\n//\t// For run-all with global flags\n//\tApplyAllE(t, &Options{\n//\t    TerragruntDir: \"/path/to/config\",\n//\t    TerragruntArgs: []string{\"--no-color\"},\n//\t})\n//\n// Constants for test framework configuration and environment variables\nconst (\n\tDefaultTerragruntBinary = \"terragrunt\"\n\tNonInteractiveFlag      = \"--non-interactive\"\n\tTerragruntLogFormatKey  = \"TG_LOG_FORMAT\"\n\tTerragruntLogCustomKey  = \"TG_LOG_CUSTOM_FORMAT\"\n\tTerragruntNoTipsKey     = \"TG_NO_TIPS\"\n\tDefaultLogFormat        = \"key-value\"\n\tDefaultLogCustomFormat  = \"%msg(color=disable)\"\n)\n\n// Options represent the configuration options for tg test execution.\n//\n// This struct is divided into two clear categories:\n//\n// 1. TEST FRAMEWORK CONFIGURATION:\n//   - Controls HOW the test framework executes tg\n//   - Includes: binary paths, directories, retry logic, logging, environment\n//   - These are NOT passed as command-line arguments to tg\n//\n// 2. TG COMMAND ARGUMENTS:\n//   - TerragruntArgs: Global terragrunt flags (placed BEFORE the command)\n//   - TerraformArgs: Command-specific flags (placed AFTER the command)\n//   - These ARE passed directly to tg in the appropriate positions\n//\n// This separation eliminates confusion about which settings control the test\n// framework vs which become tg command-line arguments.\ntype Options struct {\n\t// Test framework configuration (NOT passed to tg command line)\n\tTerragruntBinary string            // The tg binary to use (should be \"terragrunt\")\n\tTerragruntDir    string            // The directory containing the tg configuration\n\tEnvVars          map[string]string // Environment variables for command execution\n\tLogger           *logger.Logger    // Logger for command output\n\n\t// Test framework retry and error handling (NOT passed to tg command line)\n\tMaxRetries               int               // Maximum number of retries\n\tTimeBetweenRetries       time.Duration     // Time between retries\n\tRetryableTerraformErrors map[string]string // Retryable error patterns\n\tWarningsAsErrors         map[string]string // Warnings to treat as errors\n\n\t// Complex configuration that requires special formatting (NOT raw command-line args)\n\tBackendConfig map[string]interface{} // Backend configuration (formatted specially)\n\tPluginDir     string                 // Plugin directory (formatted specially)\n\n\t// Global terragrunt command-line flags (placed BEFORE the command)\n\t// Example: []string{\"--log-level\", \"info\", \"--no-color\"}\n\tTerragruntArgs []string\n\n\t// Command-specific OpenTofu/Terraform flags (placed AFTER the command)\n\t// Example: []string{\"-upgrade=true\"} for init, or []string{\"plan\"} for stack run\n\tTerraformArgs []string\n\n\t// Optional stdin to pass to OpenTofu/Terraform commands\n\tStdin io.Reader\n}\n\n// setTerragruntLogFormatting sets default log formatting and other env vars for tg\n// if they are not already set in options.EnvVars or OS environment vars\nfunc setTerragruntLogFormatting(options *Options) {\n\tif options.EnvVars == nil {\n\t\toptions.EnvVars = make(map[string]string)\n\t}\n\n\t_, inOpts := options.EnvVars[TerragruntLogFormatKey]\n\tif !inOpts {\n\t\t_, inEnv := os.LookupEnv(TerragruntLogFormatKey)\n\t\tif !inEnv {\n\t\t\t// key-value format for tg logs to avoid colors and have plain form\n\t\t\t// https://terragrunt.gruntwork.io/docs/reference/cli-options/#terragrunt-log-format\n\t\t\toptions.EnvVars[TerragruntLogFormatKey] = DefaultLogFormat\n\t\t}\n\t}\n\n\t_, inOpts = options.EnvVars[TerragruntLogCustomKey]\n\tif !inOpts {\n\t\t_, inEnv := os.LookupEnv(TerragruntLogCustomKey)\n\t\tif !inEnv {\n\t\t\toptions.EnvVars[TerragruntLogCustomKey] = DefaultLogCustomFormat\n\t\t}\n\t}\n\n\t// Suppress tips for cleaner test output (v1.0.0+, ignored by older versions)\n\t_, inOpts = options.EnvVars[TerragruntNoTipsKey]\n\tif !inOpts {\n\t\t_, inEnv := os.LookupEnv(TerragruntNoTipsKey)\n\t\tif !inEnv {\n\t\t\toptions.EnvVars[TerragruntNoTipsKey] = \"true\"\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "modules/terragrunt/output.go",
    "content": "package terragrunt\n\nimport (\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// TODO: Add OutputAll/OutputAllE when terragrunt supports combined JSON output format.\n// Currently, `output --all -json` returns separate JSON objects per module without module prefixes,\n// making it impossible to reliably map outputs to their source modules.\n\n// OutputAllJson runs terragrunt run --all output -json and returns the raw JSON string.\n// Note: Current terragrunt versions return separate JSON objects per module, not a combined object.\nfunc OutputAllJson(t testing.TestingT, options *Options) string {\n\tout, err := OutputAllJsonE(t, options)\n\trequire.NoError(t, err)\n\treturn out\n}\n\n// OutputAllJsonE runs terragrunt run --all output -json and returns the raw JSON string.\n// Note: Current terragrunt versions return separate JSON objects per module, not a combined object.\nfunc OutputAllJsonE(t testing.TestingT, options *Options) (string, error) {\n\toptsCopy := *options\n\toptsCopy.TerragruntArgs = append([]string{\"--no-color\"}, options.TerragruntArgs...)\n\n\targs := buildRunArgs([]string{\"--all\"}, []string{\"output\", \"-json\"})\n\trawOutput, err := runTerragruntCommandE(t, &optsCopy, \"run\", args...)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// Extract only JSON content from output, filtering log lines and other terragrunt messages\n\treturn extractJsonContent(rawOutput)\n}\n\n// OutputJson runs terragrunt run output -json for a single unit and returns clean JSON.\n// If key is non-empty, returns the JSON value for that specific output.\n// If key is empty, returns all outputs as JSON.\nfunc OutputJson(t testing.TestingT, options *Options, key string) string {\n\tout, err := OutputJsonE(t, options, key)\n\trequire.NoError(t, err)\n\treturn out\n}\n\n// OutputJsonE runs terragrunt run output -json for a single unit and returns clean JSON.\n// If key is non-empty, returns the JSON value for that specific output.\n// If key is empty, returns all outputs as JSON.\nfunc OutputJsonE(t testing.TestingT, options *Options, key string) (string, error) {\n\toptsCopy := *options\n\toptsCopy.TerragruntArgs = append([]string{\"--no-color\"}, options.TerragruntArgs...)\n\n\ttfArgs := []string{\"-json\"}\n\tif key != \"\" {\n\t\ttfArgs = append(tfArgs, key)\n\t}\n\n\targs := buildRunArgs([]string{}, append([]string{\"output\"}, tfArgs...))\n\trawOutput, err := runTerragruntCommandE(t, &optsCopy, \"run\", args...)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn cleanTerragruntJson(rawOutput)\n}\n"
  },
  {
    "path": "modules/terragrunt/output_test.go",
    "content": "package terragrunt\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/files\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestOutputJson(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerragruntFolderToTemp(\"testdata/terragrunt-output\", t.Name())\n\trequire.NoError(t, err)\n\n\toptions := &Options{\n\t\tTerragruntDir:    testFolder,\n\t\tTerragruntBinary: \"terragrunt\",\n\t}\n\n\tApply(t, options)\n\tdefer Destroy(t, options)\n\n\tjson := OutputJson(t, options, \"str\")\n\tassert.Contains(t, json, \"str\")\n}\n\nfunc TestOutputJsonAllKeys(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerragruntFolderToTemp(\"testdata/terragrunt-output\", t.Name())\n\trequire.NoError(t, err)\n\n\toptions := &Options{\n\t\tTerragruntDir:    testFolder,\n\t\tTerragruntBinary: \"terragrunt\",\n\t}\n\n\tApply(t, options)\n\tdefer Destroy(t, options)\n\n\tjson := OutputJson(t, options, \"\")\n\tassert.Contains(t, json, \"str\")\n\tassert.Contains(t, json, \"list\")\n\tassert.Contains(t, json, \"map\")\n}\n\nfunc TestOutputJsonE_Error(t *testing.T) {\n\tt.Parallel()\n\n\toptions := &Options{\n\t\tTerragruntDir:    t.TempDir(),\n\t\tTerragruntBinary: \"terragrunt\",\n\t}\n\n\t_, err := OutputJsonE(t, options, \"\")\n\trequire.Error(t, err)\n}\n"
  },
  {
    "path": "modules/terragrunt/plan.go",
    "content": "package terragrunt\n\nimport (\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// PlanAllExitCode runs terragrunt run --all plan with the given options and returns the detailed exit code.\n// This will fail the test if there is an error in the command.\nfunc PlanAllExitCode(t testing.TestingT, options *Options) int {\n\texitCode, err := PlanAllExitCodeE(t, options)\n\trequire.NoError(t, err)\n\treturn exitCode\n}\n\n// PlanAllExitCodeE runs terragrunt run --all -- plan with the given options and returns the detailed exit code.\nfunc PlanAllExitCodeE(t testing.TestingT, options *Options) (int, error) {\n\targs := buildRunArgs([]string{\"--all\"}, []string{\"plan\", \"-input=false\", \"-lock=true\", \"-detailed-exitcode\"})\n\treturn getExitCodeForTerragruntCommandE(t, options, append([]string{\"run\"}, args...)...)\n}\n\n// Plan runs terragrunt run plan for a single unit and returns stdout/stderr.\nfunc Plan(t testing.TestingT, options *Options) string {\n\tout, err := PlanE(t, options)\n\trequire.NoError(t, err)\n\treturn out\n}\n\n// PlanE runs terragrunt run -- plan for a single unit and returns stdout/stderr.\n// Uses -lock=false since plan is a read-only operation that does not need state locking.\nfunc PlanE(t testing.TestingT, options *Options) (string, error) {\n\targs := buildRunArgs([]string{}, []string{\"plan\", \"-input=false\", \"-lock=false\"})\n\treturn runTerragruntCommandE(t, options, \"run\", args...)\n}\n\n// PlanExitCode runs terragrunt run plan for a single unit and returns the detailed exit code.\n// This will fail the test if there is an error in the command.\nfunc PlanExitCode(t testing.TestingT, options *Options) int {\n\texitCode, err := PlanExitCodeE(t, options)\n\trequire.NoError(t, err)\n\treturn exitCode\n}\n\n// PlanExitCodeE runs terragrunt run -- plan for a single unit and returns the detailed exit code.\nfunc PlanExitCodeE(t testing.TestingT, options *Options) (int, error) {\n\targs := buildRunArgs([]string{}, []string{\"plan\", \"-input=false\", \"-lock=true\", \"-detailed-exitcode\"})\n\treturn getExitCodeForTerragruntCommandE(t, options, append([]string{\"run\"}, args...)...)\n}\n\n// InitAndPlan runs terragrunt init followed by plan for a single unit and returns the plan stdout/stderr.\nfunc InitAndPlan(t testing.TestingT, options *Options) string {\n\tout, err := InitAndPlanE(t, options)\n\trequire.NoError(t, err)\n\treturn out\n}\n\n// InitAndPlanE runs terragrunt init followed by plan for a single unit and returns the plan stdout/stderr.\nfunc InitAndPlanE(t testing.TestingT, options *Options) (string, error) {\n\tif _, err := InitE(t, options); err != nil {\n\t\treturn \"\", err\n\t}\n\treturn PlanE(t, options)\n}\n"
  },
  {
    "path": "modules/terragrunt/plan_test.go",
    "content": "package terragrunt\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/files\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestPlanAllExitCode(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerragruntFolderToTemp(\"testdata/terragrunt-multi-plan\", t.Name())\n\trequire.NoError(t, err)\n\n\toptions := &Options{\n\t\tTerragruntDir:    testFolder,\n\t\tTerragruntBinary: \"terragrunt\",\n\t}\n\n\tdefer DestroyAll(t, options)\n\tApplyAll(t, options)\n\texitCode := PlanAllExitCode(t, options)\n\trequire.Equal(t, 0, exitCode)\n}\n\nfunc TestPlan(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerragruntFolderToTemp(\"testdata/terragrunt-no-error\", t.Name())\n\trequire.NoError(t, err)\n\n\toptions := &Options{\n\t\tTerragruntDir:    testFolder,\n\t\tTerragruntBinary: \"terragrunt\",\n\t}\n\n\tout := Plan(t, options)\n\trequire.NotEmpty(t, out)\n}\n\nfunc TestPlanExitCode(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerragruntFolderToTemp(\"testdata/terragrunt-no-error\", t.Name())\n\trequire.NoError(t, err)\n\n\toptions := &Options{\n\t\tTerragruntDir:    testFolder,\n\t\tTerragruntBinary: \"terragrunt\",\n\t}\n\n\t// Apply first so plan shows no changes (exit code 0)\n\tApply(t, options)\n\tdefer Destroy(t, options)\n\n\texitCode := PlanExitCode(t, options)\n\tassert.Equal(t, 0, exitCode)\n}\n\nfunc TestInitAndPlan(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerragruntFolderToTemp(\"testdata/terragrunt-no-error\", t.Name())\n\trequire.NoError(t, err)\n\n\toptions := &Options{\n\t\tTerragruntDir:    testFolder,\n\t\tTerragruntBinary: \"terragrunt\",\n\t}\n\n\tout := InitAndPlan(t, options)\n\trequire.NotEmpty(t, out)\n}\n\nfunc TestPlanAllWithError(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerragruntFolderToTemp(\"testdata/terragrunt-with-plan-error\", t.Name())\n\trequire.NoError(t, err)\n\n\toptions := &Options{\n\t\tTerragruntDir:    testFolder,\n\t\tTerragruntBinary: \"terragrunt\",\n\t}\n\n\tgetExitCode, errExitCode := PlanAllExitCodeE(t, options)\n\t// GetExitCodeForRunCommandError was unable to determine the exit code correctly\n\trequire.NoError(t, errExitCode)\n\n\trequire.Equal(t, 1, getExitCode)\n}\n\nfunc TestAssertPlanAllExitCodeNoError(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerragruntFolderToTemp(\"testdata/terragrunt-multi-plan\", t.Name())\n\trequire.NoError(t, err)\n\n\toptions := &Options{\n\t\tTerragruntDir:    testFolder,\n\t\tTerragruntBinary: \"terragrunt\",\n\t}\n\n\tdefer DestroyAll(t, options)\n\n\tgetExitCode, errExitCode := PlanAllExitCodeE(t, options)\n\tif errExitCode != nil {\n\t\tt.Fatal(errExitCode)\n\t}\n\n\t// since there is no state file we expect `2` to be the success exit code\n\tassert.Equal(t, 2, getExitCode)\n\tassertPlanAllExitCode(t, getExitCode, true)\n\n\tApplyAll(t, options)\n\n\tgetExitCode, errExitCode = PlanAllExitCodeE(t, options)\n\tif errExitCode != nil {\n\t\tt.Fatal(errExitCode)\n\t}\n\n\t// since there is a state file we expect `0` to be the success exit code\n\tassert.Equal(t, 0, getExitCode)\n\tassertPlanAllExitCode(t, getExitCode, true)\n}\n\nfunc TestAssertPlanAllExitCodeWithError(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerragruntFolderToTemp(\"testdata/terragrunt-with-plan-error\", t.Name())\n\trequire.NoError(t, err)\n\n\toptions := &Options{\n\t\tTerragruntDir:    testFolder,\n\t\tTerragruntBinary: \"terragrunt\",\n\t}\n\n\tgetExitCode, errExitCode := PlanAllExitCodeE(t, options)\n\trequire.NoError(t, errExitCode)\n\n\tassertPlanAllExitCode(t, getExitCode, false)\n}\n\nfunc assertPlanAllExitCode(t *testing.T, exitCode int, assertTrue bool) {\n\n\tvalidExitCodes := map[int]bool{\n\t\t0: true,\n\t\t2: true,\n\t}\n\n\t_, hasKey := validExitCodes[exitCode]\n\tif assertTrue {\n\t\tassert.True(t, hasKey)\n\t} else {\n\t\tassert.False(t, hasKey)\n\t}\n}\n"
  },
  {
    "path": "modules/terragrunt/render.go",
    "content": "package terragrunt\n\nimport (\n\t\"strings\"\n\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// Render runs terragrunt render to output the resolved terragrunt configuration as HCL.\n// This is useful for verifying merged includes, resolved dependencies, and executed functions\n// without actually applying any changes.\nfunc Render(t testing.TestingT, options *Options) string {\n\tout, err := RenderE(t, options)\n\trequire.NoError(t, err)\n\treturn out\n}\n\n// RenderE runs terragrunt render to output the resolved terragrunt configuration as HCL.\n// This is useful for verifying merged includes, resolved dependencies, and executed functions\n// without actually applying any changes. Log lines are stripped from the output.\nfunc RenderE(t testing.TestingT, options *Options) (string, error) {\n\trawOutput, err := runTerragruntCommandE(t, options, \"render\")\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn filterLogLines(rawOutput), nil\n}\n\n// filterLogLines removes terragrunt log lines while preserving original indentation.\n// Unlike removeLogLines (which trims whitespace for JSON extraction), this keeps\n// leading whitespace intact so HCL output structure is preserved.\nfunc filterLogLines(rawOutput string) string {\n\tlines := strings.Split(rawOutput, \"\\n\")\n\tvar result []string\n\tfor _, line := range lines {\n\t\ttrimmed := strings.TrimSpace(line)\n\t\tif trimmed == \"\" || isLogLine(trimmed) || isMetadataLine(trimmed) {\n\t\t\tcontinue\n\t\t}\n\t\tresult = append(result, line)\n\t}\n\treturn strings.Join(result, \"\\n\")\n}\n\n// RenderJson runs terragrunt render --format json and returns the cleaned JSON output.\n// This is useful for programmatic assertions on the resolved terragrunt configuration.\nfunc RenderJson(t testing.TestingT, options *Options) string {\n\tout, err := RenderJsonE(t, options)\n\trequire.NoError(t, err)\n\treturn out\n}\n\n// RenderJsonE runs terragrunt render --format json and returns the cleaned JSON output.\n// This is useful for programmatic assertions on the resolved terragrunt configuration.\nfunc RenderJsonE(t testing.TestingT, options *Options) (string, error) {\n\toptsCopy := *options\n\toptsCopy.TerragruntArgs = append([]string{\"--no-color\"}, options.TerragruntArgs...)\n\n\trawOutput, err := runTerragruntCommandE(t, &optsCopy, \"render\", \"--format\", \"json\")\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn cleanTerragruntJson(rawOutput)\n}\n"
  },
  {
    "path": "modules/terragrunt/render_test.go",
    "content": "package terragrunt\n\nimport (\n\t\"encoding/json\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/files\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestRender(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerragruntFolderToTemp(\"testdata/terragrunt-no-error\", t.Name())\n\trequire.NoError(t, err)\n\n\toutput := Render(t, &Options{\n\t\tTerragruntDir:    testFolder,\n\t\tTerragruntBinary: \"terragrunt\",\n\t})\n\n\trequire.Contains(t, output, `source = \"`)\n\trequire.Contains(t, output, `extra_arguments`)\n\t// Verify log lines are stripped and indentation is preserved\n\trequire.NotContains(t, output, \"level=\")\n\trequire.Contains(t, output, \"  source = \")\n}\n\nfunc TestRenderJson(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerragruntFolderToTemp(\"testdata/terragrunt-no-error\", t.Name())\n\trequire.NoError(t, err)\n\n\toutput := RenderJson(t, &Options{\n\t\tTerragruntDir:    testFolder,\n\t\tTerragruntBinary: \"terragrunt\",\n\t})\n\n\tvar parsed map[string]interface{}\n\trequire.NoError(t, json.Unmarshal([]byte(output), &parsed), \"output should be valid JSON\")\n\trequire.Contains(t, parsed, \"terraform\")\n}\n\nfunc TestFilterLogLines(t *testing.T) {\n\tt.Parallel()\n\n\tinput := \"20:41:53.564 INFO   some log message\\n  source = \\\"./modules/vpc\\\"\\n\\ntime=2023-07-11 level=info msg=hello\\n  inputs = {\\nGroup 1\\n    name = \\\"test\\\"\\n  }\"\n\tresult := filterLogLines(input)\n\n\t// Log lines and metadata lines should be stripped\n\trequire.NotContains(t, result, \"INFO\")\n\trequire.NotContains(t, result, \"level=info\")\n\trequire.NotContains(t, result, \"Group 1\")\n\n\t// Indentation should be preserved (unlike removeLogLines which trims)\n\trequire.Contains(t, result, \"  source = \")\n\trequire.Contains(t, result, \"  inputs = {\")\n\trequire.Contains(t, result, \"    name = \")\n}\n\nfunc TestRenderE_InvalidConfig(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := t.TempDir()\n\trequire.NoError(t, os.WriteFile(filepath.Join(tmpDir, \"terragrunt.hcl\"), []byte(\"not_valid!!!\"), 0644))\n\n\t_, err := RenderE(t, &Options{TerragruntDir: tmpDir})\n\trequire.Error(t, err)\n}\n"
  },
  {
    "path": "modules/terragrunt/run.go",
    "content": "package terragrunt\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// Run runs terragrunt run [tgArgs...] -- [tfArgs...] with the given options and returns stdout/stderr.\n// This is a generic wrapper that allows running any OpenTofu/Terraform command through terragrunt run.\n// The -- separator disambiguates Terragrunt flags from OpenTofu/Terraform flags.\n// The OpenTofu/Terraform command (e.g. \"apply\") should be the first element of tfArgs.\nfunc Run(t testing.TestingT, options *Options, tgArgs []string, tfArgs []string) string {\n\tout, err := RunE(t, options, tgArgs, tfArgs)\n\trequire.NoError(t, err)\n\treturn out\n}\n\n// RunE runs terragrunt run [tgArgs...] -- [tfArgs...] with the given options and returns stdout/stderr.\n// This is a generic wrapper that allows running any OpenTofu/Terraform command through terragrunt run.\n// The -- separator disambiguates Terragrunt flags from OpenTofu/Terraform flags.\n// The OpenTofu/Terraform command (e.g. \"apply\") should be the first element of tfArgs.\nfunc RunE(t testing.TestingT, options *Options, tgArgs []string, tfArgs []string) (string, error) {\n\tif len(tfArgs) == 0 {\n\t\treturn \"\", fmt.Errorf(\"tfArgs cannot be empty; at minimum, an OpenTofu/Terraform command (e.g. \\\"apply\\\") is required\")\n\t}\n\targs := buildRunArgs(tgArgs, tfArgs)\n\treturn runTerragruntCommandE(t, options, \"run\", args...)\n}\n"
  },
  {
    "path": "modules/terragrunt/run_all.go",
    "content": "package terragrunt\n\nimport (\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n)\n\n// Deprecated: Use Run with the --all flag in tgArgs instead.\n// RunAll runs terragrunt run --all -- <command> with the given options and returns stdout/stderr.\nfunc RunAll(t testing.TestingT, options *Options, command string) string {\n\treturn Run(t, options, []string{\"--all\"}, []string{command})\n}\n\n// Deprecated: Use RunE with the --all flag in tgArgs instead.\n// RunAllE runs terragrunt run --all -- <command> with the given options and returns stdout/stderr.\nfunc RunAllE(t testing.TestingT, options *Options, command string) (string, error) {\n\treturn RunE(t, options, []string{\"--all\"}, []string{command})\n}\n"
  },
  {
    "path": "modules/terragrunt/run_all_test.go",
    "content": "package terragrunt\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/files\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestRunAll(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerragruntFolderToTemp(\"testdata/terragrunt-multi-plan\", t.Name())\n\trequire.NoError(t, err)\n\n\toptions := &Options{\n\t\tTerragruntDir:    testFolder,\n\t\tTerragruntBinary: \"terragrunt\",\n\t}\n\n\t// Test with validate command\n\tout := RunAll(t, options, \"validate\")\n\trequire.NotEmpty(t, out)\n}\n\nfunc TestRunAllE(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerragruntFolderToTemp(\"testdata/terragrunt-multi-plan\", t.Name())\n\trequire.NoError(t, err)\n\n\toptions := &Options{\n\t\tTerragruntDir:    testFolder,\n\t\tTerragruntBinary: \"terragrunt\",\n\t}\n\n\t// Test with validate command\n\tout, err := RunAllE(t, options, \"validate\")\n\trequire.NoError(t, err)\n\trequire.NotEmpty(t, out)\n}\n\nfunc TestRunAllWithPlan(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerragruntFolderToTemp(\"testdata/terragrunt-multi-plan\", t.Name())\n\trequire.NoError(t, err)\n\n\toptions := &Options{\n\t\tTerragruntDir:    testFolder,\n\t\tTerragruntBinary: \"terragrunt\",\n\t}\n\n\t// Test with plan command - verify output contains expected terraform plan text\n\tout, err := RunAllE(t, options, \"plan\")\n\trequire.NoError(t, err)\n\trequire.Contains(t, out, \"Changes to Outputs\")\n}\n"
  },
  {
    "path": "modules/terragrunt/run_test.go",
    "content": "package terragrunt\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/files\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// TestBuildRunArgs verifies the argument construction logic for the run command.\nfunc TestBuildRunArgs(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname     string\n\t\ttgArgs   []string\n\t\ttfArgs   []string\n\t\texpected []string\n\t}{\n\t\t{\n\t\t\tname:     \"only tf args\",\n\t\t\ttgArgs:   []string{},\n\t\t\ttfArgs:   []string{\"apply\", \"-auto-approve\"},\n\t\t\texpected: []string{\"--\", \"apply\", \"-auto-approve\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"nil tg args with tf args\",\n\t\t\ttgArgs:   nil,\n\t\t\ttfArgs:   []string{\"plan\"},\n\t\t\texpected: []string{\"--\", \"plan\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"both tg and tf args\",\n\t\t\ttgArgs:   []string{\"--all\"},\n\t\t\ttfArgs:   []string{\"apply\", \"-input=false\", \"-auto-approve\"},\n\t\t\texpected: []string{\"--all\", \"--\", \"apply\", \"-input=false\", \"-auto-approve\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"multiple tg args\",\n\t\t\ttgArgs:   []string{\"--all\", \"--exclude-dir\", \"staging\"},\n\t\t\ttfArgs:   []string{\"plan\"},\n\t\t\texpected: []string{\"--all\", \"--exclude-dir\", \"staging\", \"--\", \"plan\"},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tactual := buildRunArgs(tt.tgArgs, tt.tfArgs)\n\t\t\trequire.Equal(t, tt.expected, actual)\n\t\t})\n\t}\n}\n\n// TestRunE_EmptyTfArgs verifies that RunE returns an error when tfArgs is empty.\nfunc TestRunE_EmptyTfArgs(t *testing.T) {\n\tt.Parallel()\n\n\toptions := &Options{\n\t\tTerragruntDir: \"/some/path\",\n\t}\n\n\t_, err := RunE(t, options, []string{}, []string{})\n\trequire.Error(t, err)\n\trequire.Contains(t, err.Error(), \"tfArgs cannot be empty\")\n\n\t_, err = RunE(t, options, []string{\"--all\"}, nil)\n\trequire.Error(t, err)\n\trequire.Contains(t, err.Error(), \"tfArgs cannot be empty\")\n}\n\n// TestRun verifies that Run executes terragrunt run -- apply successfully.\nfunc TestRun(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerragruntFolderToTemp(\"testdata/terragrunt-no-error\", t.Name())\n\trequire.NoError(t, err)\n\n\toptions := &Options{\n\t\tTerragruntDir:    testFolder,\n\t\tTerragruntBinary: \"terragrunt\",\n\t}\n\n\tdefer Run(t, options, []string{}, []string{\"destroy\", \"-auto-approve\"})\n\tout := Run(t, options, []string{}, []string{\"apply\", \"-input=false\", \"-auto-approve\"})\n\trequire.Contains(t, out, \"Hello, World\")\n}\n\n// TestRunE verifies that RunE returns an error on failure rather than calling t.Fatal.\nfunc TestRunE(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerragruntFolderToTemp(\"testdata/terragrunt-no-error\", t.Name())\n\trequire.NoError(t, err)\n\n\toptions := &Options{\n\t\tTerragruntDir:    testFolder,\n\t\tTerragruntBinary: \"terragrunt\",\n\t}\n\n\t// Run an invalid tf command to trigger an error\n\t_, err = RunE(t, options, []string{}, []string{\"not-a-real-command\"})\n\trequire.Error(t, err)\n}\n\n// TestRunWithTgArgs verifies that terragrunt-specific args are passed before the -- separator.\nfunc TestRunWithTgArgs(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerragruntFolderToTemp(\"testdata/terragrunt-no-error\", t.Name())\n\trequire.NoError(t, err)\n\n\toptions := &Options{\n\t\tTerragruntDir:    testFolder,\n\t\tTerragruntBinary: \"terragrunt\",\n\t}\n\n\tdefer Run(t, options, []string{}, []string{\"destroy\", \"-auto-approve\"})\n\n\t// Use --log-level error as a tg arg to verify it's respected\n\tout := Run(t, options, []string{\"--log-level\", \"error\"}, []string{\"apply\", \"-input=false\", \"-auto-approve\"})\n\trequire.Contains(t, out, \"Hello, World\")\n\trequire.NotContains(t, out, \"level=info\",\n\t\t\"With --log-level error, info logs should not appear\")\n}\n\n// TestRunE_ValidationError verifies that RunE returns an error for invalid options.\nfunc TestRunE_ValidationError(t *testing.T) {\n\tt.Parallel()\n\n\t// Missing TerragruntDir\n\toptions := &Options{}\n\t_, err := RunE(t, options, []string{}, []string{\"apply\"})\n\trequire.Error(t, err)\n\trequire.Contains(t, err.Error(), \"TerragruntDir is required\")\n}\n"
  },
  {
    "path": "modules/terragrunt/stack_clean.go",
    "content": "package terragrunt\n\nimport (\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// StackClean calls terragrunt stack clean to remove the .terragrunt-stack directory\n// This command cleans up the generated stack files created by stack generate or stack run\nfunc StackClean(t testing.TestingT, options *Options) string {\n\tout, err := StackCleanE(t, options)\n\trequire.NoError(t, err)\n\treturn out\n}\n\n// StackCleanE calls terragrunt stack clean to remove the .terragrunt-stack directory\n// This command cleans up the generated stack files created by stack generate or stack run\nfunc StackCleanE(t testing.TestingT, options *Options) (string, error) {\n\treturn runTerragruntStackCommandE(t, options, \"clean\")\n}\n"
  },
  {
    "path": "modules/terragrunt/stack_clean_test.go",
    "content": "package terragrunt\n\nimport (\n\t\"path\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/files\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestStackClean(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerraformFolderToTemp(\n\t\t\"testdata/terragrunt-stack-init\", t.Name())\n\trequire.NoError(t, err)\n\n\tstackDir := path.Join(testFolder, \"live\", \".terragrunt-stack\")\n\n\tStackGenerate(t, &Options{\n\t\tTerragruntDir:    path.Join(testFolder, \"live\"),\n\t\tTerragruntBinary: \"terragrunt\",\n\t})\n\n\trequire.DirExists(t, stackDir)\n\n\tout := StackClean(t, &Options{\n\t\tTerragruntDir:    path.Join(testFolder, \"live\"),\n\t\tTerragruntBinary: \"terragrunt\",\n\t})\n\n\trequire.Contains(t, out, \"Deleting stack directory\")\n\trequire.NoDirExists(t, stackDir)\n}\n\nfunc TestStackCleanE(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerraformFolderToTemp(\n\t\t\"testdata/terragrunt-stack-init\", t.Name())\n\trequire.NoError(t, err)\n\n\tstackDir := path.Join(testFolder, \"live\", \".terragrunt-stack\")\n\n\t// First generate the stack to create .terragrunt-stack directory\n\t_, err = StackGenerateE(t, &Options{\n\t\tTerragruntDir:    path.Join(testFolder, \"live\"),\n\t\tTerragruntBinary: \"terragrunt\",\n\t})\n\trequire.NoError(t, err)\n\n\t// Verify that the .terragrunt-stack directory was created\n\trequire.DirExists(t, stackDir)\n\n\t// Clean the stack\n\tout, err := StackCleanE(t, &Options{\n\t\tTerragruntDir:    path.Join(testFolder, \"live\"),\n\t\tTerragruntBinary: \"terragrunt\",\n\t})\n\trequire.NoError(t, err)\n\n\t// Verify clean command produced expected output\n\trequire.Contains(t, out, \"Deleting stack directory\")\n\n\t// Verify that the .terragrunt-stack directory was removed\n\trequire.NoDirExists(t, stackDir)\n}\n\nfunc TestStackCleanNonExistentStack(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerraformFolderToTemp(\n\t\t\"testdata/terragrunt-stack-init\", t.Name())\n\trequire.NoError(t, err)\n\n\tstackDir := path.Join(testFolder, \"live\", \".terragrunt-stack\")\n\n\t// Verify that the .terragrunt-stack directory doesn't exist\n\trequire.NoDirExists(t, stackDir)\n\n\t// Clean should succeed even if .terragrunt-stack doesn't exist\n\t_, err = StackCleanE(t, &Options{\n\t\tTerragruntDir:    path.Join(testFolder, \"live\"),\n\t\tTerragruntBinary: \"terragrunt\",\n\t})\n\trequire.NoError(t, err)\n}\n\nfunc TestStackCleanAfterRun(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerraformFolderToTemp(\n\t\t\"testdata/terragrunt-stack-init\", t.Name())\n\trequire.NoError(t, err)\n\n\tstackDir := path.Join(testFolder, \"live\", \".terragrunt-stack\")\n\n\t// Initialize the stack\n\t_, err = InitE(t, &Options{\n\t\tTerragruntDir:    path.Join(testFolder, \"live\"),\n\t\tTerragruntBinary: \"terragrunt\",\n\t\tTerraformArgs:    []string{\"-upgrade=true\"},\n\t})\n\trequire.NoError(t, err)\n\n\t// Run plan to generate the stack\n\t_, err = StackRunE(t, &Options{\n\t\tTerragruntDir:    path.Join(testFolder, \"live\"),\n\t\tTerragruntBinary: \"terragrunt\",\n\t\tTerraformArgs:    []string{\"plan\"},\n\t})\n\trequire.NoError(t, err)\n\n\t// Verify that the .terragrunt-stack directory was created\n\trequire.DirExists(t, stackDir)\n\n\t// Clean the stack\n\tout, err := StackCleanE(t, &Options{\n\t\tTerragruntDir:    path.Join(testFolder, \"live\"),\n\t\tTerragruntBinary: \"terragrunt\",\n\t})\n\trequire.NoError(t, err)\n\n\t// Verify clean command produced expected output\n\trequire.Contains(t, out, \"Deleting stack directory\")\n\n\t// Verify that the .terragrunt-stack directory was removed\n\trequire.NoDirExists(t, stackDir)\n}\n"
  },
  {
    "path": "modules/terragrunt/stack_generate.go",
    "content": "package terragrunt\n\nimport (\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// StackGenerate calls terragrunt stack generate and returns stdout/stderr\nfunc StackGenerate(t testing.TestingT, options *Options) string {\n\tout, err := StackGenerateE(t, options)\n\trequire.NoError(t, err)\n\treturn out\n}\n\n// StackGenerateE calls terragrunt stack generate and returns stdout/stderr\nfunc StackGenerateE(t testing.TestingT, options *Options) (string, error) {\n\treturn runTerragruntStackCommandE(t, options, \"generate\")\n}\n"
  },
  {
    "path": "modules/terragrunt/stack_generate_test.go",
    "content": "package terragrunt\n\nimport (\n\t\"path\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/files\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestStackGenerate(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerraformFolderToTemp(\n\t\t\"testdata/terragrunt-stack-init\", t.Name())\n\trequire.NoError(t, err)\n\n\tInit(t, &Options{\n\t\tTerragruntDir:    path.Join(testFolder, \"live\"),\n\t\tTerragruntBinary: \"terragrunt\",\n\t\tTerraformArgs:    []string{\"-upgrade=true\"},\n\t})\n\n\tout := StackGenerate(t, &Options{\n\t\tTerragruntDir:    path.Join(testFolder, \"live\"),\n\t\tTerragruntBinary: \"terragrunt\",\n\t})\n\n\trequire.True(t, containsEitherString(out, \"Processing unit\", \"Generating unit\"))\n\trequire.DirExists(t, path.Join(testFolder, \"live\", \".terragrunt-stack\"))\n}\n\nfunc TestStackGenerateE(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerraformFolderToTemp(\n\t\t\"testdata/terragrunt-stack-init\", t.Name())\n\trequire.NoError(t, err)\n\n\t// First initialize the stack\n\t_, err = InitE(t, &Options{\n\t\tTerragruntDir:    path.Join(testFolder, \"live\"),\n\t\tTerragruntBinary: \"terragrunt\",\n\t\tTerraformArgs:    []string{\"-upgrade=true\"},\n\t})\n\trequire.NoError(t, err)\n\n\t// Then generate the stack\n\tout, err := StackGenerateE(t, &Options{\n\t\tTerragruntDir:    path.Join(testFolder, \"live\"),\n\t\tTerragruntBinary: \"terragrunt\",\n\t})\n\trequire.NoError(t, err)\n\n\t// Validate that generate command produced output\n\t// Terragrunt v0.80.4+ outputs \"Processing unit\", older versions output \"Generating unit\"\n\trequire.True(t, containsEitherString(out, \"Processing unit\", \"Generating unit\"), \"Output should contain either 'Processing unit' or 'Generating unit'\")\n\n\t// Verify that the .terragrunt-stack directory was created\n\tstackDir := path.Join(testFolder, \"live\", \".terragrunt-stack\")\n\trequire.DirExists(t, stackDir)\n\n\t// Verify that the expected unit directories were created\n\texpectedUnits := []string{\"mother\", \"father\", \"chicks/chick-1\", \"chicks/chick-2\"}\n\tfor _, unit := range expectedUnits {\n\t\tunitPath := path.Join(stackDir, unit)\n\t\trequire.DirExists(t, unitPath)\n\t}\n}\n\nfunc TestStackGenerateNonExistentDir(t *testing.T) {\n\tt.Parallel()\n\n\t// Test with non-existent directory\n\t_, err := StackGenerateE(t, &Options{\n\t\tTerragruntDir:    \"/non/existent/path\",\n\t\tTerragruntBinary: \"terragrunt\",\n\t})\n\trequire.Error(t, err)\n}\n\n// containsEitherString checks if the output contains at least one of the provided strings\nfunc containsEitherString(output, str1, str2 string) bool {\n\treturn strings.Contains(output, str1) || strings.Contains(output, str2)\n}\n\n// TestStackGenerateWithArgs verifies stack commands respect TerragruntArgs\nfunc TestStackGenerateWithArgs(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerraformFolderToTemp(\n\t\t\"testdata/terragrunt-stack-init\", t.Name())\n\trequire.NoError(t, err)\n\n\t// Initialize first\n\t_, err = InitE(t, &Options{\n\t\tTerragruntDir:    path.Join(testFolder, \"live\"),\n\t\tTerragruntBinary: \"terragrunt\",\n\t})\n\trequire.NoError(t, err)\n\n\t// Generate with TerragruntArgs\n\tout, err := StackGenerateE(t, &Options{\n\t\tTerragruntDir:    path.Join(testFolder, \"live\"),\n\t\tTerragruntBinary: \"terragrunt\",\n\t\tTerragruntArgs:   []string{\"--log-level\", \"error\"},\n\t})\n\trequire.NoError(t, err)\n\t// Verify args were respected\n\trequire.NotContains(t, out, \"level=info\")\n}\n"
  },
  {
    "path": "modules/terragrunt/stack_output.go",
    "content": "package terragrunt\n\nimport (\n\t\"encoding/json\"\n\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// StackOutput calls terragrunt stack output for the given variable and returns its value as a string\nfunc StackOutput(t testing.TestingT, options *Options, key string) string {\n\tout, err := StackOutputE(t, options, key)\n\trequire.NoError(t, err)\n\treturn out\n}\n\n// StackOutputE calls terragrunt stack output for the given variable and returns its value as a string\nfunc StackOutputE(t testing.TestingT, options *Options, key string) (string, error) {\n\t// Prepare options with no-color flag for parsing\n\toptsCopy := *options\n\toptsCopy.TerragruntArgs = append([]string{\"--no-color\"}, options.TerragruntArgs...)\n\n\tvar args []string\n\tif key != \"\" {\n\t\targs = append(args, key)\n\t}\n\t// Append any user-provided TerraformArgs\n\tif len(options.TerraformArgs) > 0 {\n\t\targs = append(args, options.TerraformArgs...)\n\t}\n\n\t// Output command for stack\n\trawOutput, err := runTerragruntStackCommandE(\n\t\tt, &optsCopy, \"output\", args...)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// Extract the actual value from output\n\tcleaned, err := cleanTerragruntOutput(rawOutput)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn cleaned, nil\n}\n\n// StackOutputJson calls terragrunt stack output for the given variable and returns the result as the json string.\n// If key is an empty string, it will return all the output variables.\nfunc StackOutputJson(t testing.TestingT, options *Options, key string) string {\n\tstr, err := StackOutputJsonE(t, options, key)\n\trequire.NoError(t, err)\n\treturn str\n}\n\n// StackOutputJsonE calls terragrunt stack output for the given variable and returns the\n// result as the json string.\n// If key is an empty string, it will return all the output variables.\nfunc StackOutputJsonE(t testing.TestingT, options *Options, key string) (string, error) {\n\t// Prepare options with no-color flag\n\toptsCopy := *options\n\toptsCopy.TerragruntArgs = append([]string{\"--no-color\"}, options.TerragruntArgs...)\n\n\t// -json is an OpenTofu/Terraform flag that should go after the output command\n\targs := []string{\"-json\"}\n\tif key != \"\" {\n\t\targs = append(args, key)\n\t}\n\t// Append any user-provided TerraformArgs\n\tif len(options.TerraformArgs) > 0 {\n\t\targs = append(args, options.TerraformArgs...)\n\t}\n\n\t// Output command for stack\n\trawOutput, err := runTerragruntStackCommandE(\n\t\tt, &optsCopy, \"output\", args...)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// Parse and format JSON output\n\treturn cleanTerragruntJson(rawOutput)\n}\n\n// StackOutputAll gets all stack outputs and returns them as a map[string]interface{}\nfunc StackOutputAll(t testing.TestingT, options *Options) map[string]interface{} {\n\toutputs, err := StackOutputAllE(t, options)\n\trequire.NoError(t, err)\n\treturn outputs\n}\n\n// StackOutputAllE gets all stack outputs and returns them as a map[string]interface{}\nfunc StackOutputAllE(t testing.TestingT, options *Options) (map[string]interface{}, error) {\n\tjsonOutput, err := StackOutputJsonE(t, options, \"\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar outputs map[string]interface{}\n\tif err := json.Unmarshal([]byte(jsonOutput), &outputs); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn outputs, nil\n}\n\n// StackOutputListAll gets all stack output variable names and returns them as a slice\nfunc StackOutputListAll(t testing.TestingT, options *Options) []string {\n\tkeys, err := StackOutputListAllE(t, options)\n\trequire.NoError(t, err)\n\treturn keys\n}\n\n// StackOutputListAllE gets all stack output variable names and returns them as a slice\nfunc StackOutputListAllE(t testing.TestingT, options *Options) ([]string, error) {\n\toutputs, err := StackOutputAllE(t, options)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tkeys := make([]string, 0, len(outputs))\n\tfor key := range outputs {\n\t\tkeys = append(keys, key)\n\t}\n\n\treturn keys, nil\n}\n"
  },
  {
    "path": "modules/terragrunt/stack_output_test.go",
    "content": "package terragrunt\n\nimport (\n\t\"encoding/json\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/files\"\n\t\"github.com/gruntwork-io/terratest/modules/logger\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// Integration test using actual terragrunt stack fixture\nfunc TestStackOutputIntegration(t *testing.T) {\n\tt.Parallel()\n\n\t// Create a temporary copy of the stack fixture\n\ttestFolder, err := files.CopyTerragruntFolderToTemp(\n\t\t\"testdata/terragrunt-stack-init\", \"tg-stack-output-test\")\n\trequire.NoError(t, err)\n\n\toptions := &Options{\n\t\tTerragruntDir:    testFolder + \"/live\",\n\t\tTerragruntBinary: \"terragrunt\",\n\t\tLogger:           logger.Discard,\n\t}\n\n\t// Initialize and apply tg using stack commands\n\t_, err = InitE(t, options)\n\trequire.NoError(t, err)\n\n\tapplyOptions := &Options{\n\t\tTerragruntDir:    testFolder + \"/live\",\n\t\tTerragruntBinary: \"terragrunt\",\n\t\tLogger:           logger.Discard,\n\t\tTerraformArgs:    []string{\"apply\"}, // stack run auto-approves by default\n\t}\n\t_, err = StackRunE(t, applyOptions)\n\trequire.NoError(t, err)\n\n\t// Clean up after test\n\tdefer func() {\n\t\tdestroyOptions := &Options{\n\t\t\tTerragruntDir:    testFolder + \"/live\",\n\t\t\tTerragruntBinary: \"terragrunt\",\n\t\t\tLogger:           logger.Discard,\n\t\t\tTerraformArgs:    []string{\"destroy\"}, // stack run auto-approves by default\n\t\t}\n\t\t_, _ = StackRunE(t, destroyOptions)\n\t}()\n\n\t// Test string stack output - get output from mother unit\n\tstrOutput := StackOutput(t, options, \"mother\")\n\tassert.Contains(t, strOutput, \"./test.txt\")\n\n\t// Test getting stack output as JSON using the StackOutputJson function\n\tjsonOptions := &Options{\n\t\tTerragruntDir:    testFolder + \"/live\",\n\t\tTerragruntBinary: \"terragrunt\",\n\t\tLogger:           logger.Discard,\n\t}\n\n\tstrOutputJson := StackOutputJson(t, jsonOptions, \"mother\")\n\t// The JSON output for a single value should still be cleaned to just show the value\n\tassert.Contains(t, strOutputJson, \"./test.txt\")\n\n\t// Test getting all stack outputs as JSON\n\tallOutputsJson := StackOutputJson(t, jsonOptions, \"\")\n\trequire.NotEmpty(t, allOutputsJson)\n\n\t// For JSON output of all outputs, we should get valid JSON\n\t// But our function cleans it, so let's test it as-is\n\t// The JSON structure should be valid and contain our expected data\n\tif strings.Contains(allOutputsJson, \"{\") {\n\t\t// Parse and validate the JSON structure\n\t\tvar allOutputs map[string]interface{}\n\t\terr = json.Unmarshal([]byte(allOutputsJson), &allOutputs)\n\t\trequire.NoError(t, err)\n\n\t\t// Verify all expected stack outputs are present\n\t\trequire.Contains(t, allOutputs, \"mother\")\n\t\trequire.Contains(t, allOutputs, \"father\")\n\t\trequire.Contains(t, allOutputs, \"chick_1\")\n\t\trequire.Contains(t, allOutputs, \"chick_2\")\n\n\t\t// Verify the structure of outputs\n\t\tmotherOutputMap := allOutputs[\"mother\"].(map[string]interface{})\n\t\tassert.Equal(t, \"./test.txt\", motherOutputMap[\"output\"])\n\t} else {\n\t\t// If not JSON format, at least verify it contains our expected values\n\t\tassert.Contains(t, allOutputsJson, \"mother\")\n\t\tassert.Contains(t, allOutputsJson, \"father\")\n\t\tassert.Contains(t, allOutputsJson, \"chick_1\")\n\t\tassert.Contains(t, allOutputsJson, \"chick_2\")\n\t}\n}\n\n// Test error handling with non-existent stack output\nfunc TestStackOutputErrorHandling(t *testing.T) {\n\tt.Parallel()\n\n\t// Create a temporary copy of the stack fixture\n\ttestFolder, err := files.CopyTerragruntFolderToTemp(\n\t\t\"testdata/terragrunt-stack-init\", \"tg-stack-output-error-test\")\n\trequire.NoError(t, err)\n\n\toptions := &Options{\n\t\tTerragruntDir:    testFolder + \"/live\",\n\t\tTerragruntBinary: \"terragrunt\",\n\t\tLogger:           logger.Discard,\n\t}\n\n\t// Initialize and apply tg using stack commands\n\t_, err = InitE(t, options)\n\trequire.NoError(t, err)\n\n\tapplyOptions := &Options{\n\t\tTerragruntDir:    testFolder + \"/live\",\n\t\tTerragruntBinary: \"terragrunt\",\n\t\tLogger:           logger.Discard,\n\t\tTerraformArgs:    []string{\"apply\"}, // stack run auto-approves by default\n\t}\n\t_, err = StackRunE(t, applyOptions)\n\trequire.NoError(t, err)\n\n\t// Clean up after test\n\tdefer func() {\n\t\tdestroyOptions := &Options{\n\t\t\tTerragruntDir:    testFolder + \"/live\",\n\t\t\tTerragruntBinary: \"terragrunt\",\n\t\t\tLogger:           logger.Discard,\n\t\t\tTerraformArgs:    []string{\"destroy\"}, // stack run auto-approves by default\n\t\t}\n\t\t_, _ = StackRunE(t, destroyOptions)\n\t}()\n\n\t// Test that non-existent stack output returns error or empty string\n\toutput, err := StackOutputE(t, options, \"non_existent_output\")\n\t// Tg stack output might return empty string for non-existent outputs\n\t// rather than an error, so we need to handle both cases\n\tif err != nil {\n\t\tassert.Contains(t, strings.ToLower(err.Error()), \"output\")\n\t} else {\n\t\tassert.Empty(t, output, \"Expected empty output for non-existent stack output\")\n\t}\n}\n\n// Test StackOutputAll to get all stack outputs as a map\nfunc TestStackOutputAll(t *testing.T) {\n\tt.Parallel()\n\n\t// Create a temporary copy of the stack fixture\n\ttestFolder, err := files.CopyTerragruntFolderToTemp(\n\t\t\"testdata/terragrunt-stack-init\", \"tg-stack-output-all-test\")\n\trequire.NoError(t, err)\n\n\toptions := &Options{\n\t\tTerragruntDir:    testFolder + \"/live\",\n\t\tTerragruntBinary: \"terragrunt\",\n\t\tLogger:           logger.Discard,\n\t}\n\n\t// Initialize and apply tg using stack commands\n\t_, err = InitE(t, options)\n\trequire.NoError(t, err)\n\n\tapplyOptions := &Options{\n\t\tTerragruntDir:    testFolder + \"/live\",\n\t\tTerragruntBinary: \"terragrunt\",\n\t\tLogger:           logger.Discard,\n\t\tTerraformArgs:    []string{\"apply\"}, // stack run auto-approves by default\n\t}\n\t_, err = StackRunE(t, applyOptions)\n\trequire.NoError(t, err)\n\n\t// Clean up after test\n\tdefer func() {\n\t\tdestroyOptions := &Options{\n\t\t\tTerragruntDir:    testFolder + \"/live\",\n\t\t\tTerragruntBinary: \"terragrunt\",\n\t\t\tLogger:           logger.Discard,\n\t\t\tTerraformArgs:    []string{\"destroy\"}, // stack run auto-approves by default\n\t\t}\n\t\t_, _ = StackRunE(t, destroyOptions)\n\t}()\n\n\t// Test StackOutputAll - get all outputs as a map\n\tallOutputs := StackOutputAll(t, options)\n\trequire.NotEmpty(t, allOutputs)\n\n\t// Verify expected outputs are present\n\trequire.Contains(t, allOutputs, \"mother\")\n\trequire.Contains(t, allOutputs, \"father\")\n\trequire.Contains(t, allOutputs, \"chick_1\")\n\trequire.Contains(t, allOutputs, \"chick_2\")\n\n\t// Verify we can access specific output values\n\tmotherOutput := allOutputs[\"mother\"].(map[string]interface{})\n\tassert.Equal(t, \"./test.txt\", motherOutput[\"output\"])\n}\n\n// Test StackOutputListAll to get all stack output keys\nfunc TestStackOutputListAll(t *testing.T) {\n\tt.Parallel()\n\n\t// Create a temporary copy of the stack fixture\n\ttestFolder, err := files.CopyTerragruntFolderToTemp(\n\t\t\"testdata/terragrunt-stack-init\", \"tg-stack-output-list-test\")\n\trequire.NoError(t, err)\n\n\toptions := &Options{\n\t\tTerragruntDir:    testFolder + \"/live\",\n\t\tTerragruntBinary: \"terragrunt\",\n\t\tLogger:           logger.Discard,\n\t}\n\n\t// Initialize and apply using stack commands\n\t_, err = InitE(t, options)\n\trequire.NoError(t, err)\n\n\tapplyOptions := &Options{\n\t\tTerragruntDir:    testFolder + \"/live\",\n\t\tTerragruntBinary: \"terragrunt\",\n\t\tLogger:           logger.Discard,\n\t\tTerraformArgs:    []string{\"apply\"},\n\t}\n\t_, err = StackRunE(t, applyOptions)\n\trequire.NoError(t, err)\n\n\t// Clean up after test\n\tdefer func() {\n\t\tdestroyOptions := &Options{\n\t\t\tTerragruntDir:    testFolder + \"/live\",\n\t\t\tTerragruntBinary: \"terragrunt\",\n\t\t\tLogger:           logger.Discard,\n\t\t\tTerraformArgs:    []string{\"destroy\"},\n\t\t}\n\t\t_, _ = StackRunE(t, destroyOptions)\n\t}()\n\n\t// Test StackOutputListAll - get all output keys\n\tkeys := StackOutputListAll(t, options)\n\trequire.NotEmpty(t, keys)\n\n\t// Verify expected keys are present\n\trequire.Contains(t, keys, \"mother\")\n\trequire.Contains(t, keys, \"father\")\n\trequire.Contains(t, keys, \"chick_1\")\n\trequire.Contains(t, keys, \"chick_2\")\n\n\t// Verify we got all 4 keys\n\trequire.Len(t, keys, 4)\n}\n\n// Test StackOutputListAllE\nfunc TestStackOutputListAllE(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerragruntFolderToTemp(\n\t\t\"testdata/terragrunt-stack-init\", \"tg-stack-output-list-e-test\")\n\trequire.NoError(t, err)\n\n\toptions := &Options{\n\t\tTerragruntDir:    testFolder + \"/live\",\n\t\tTerragruntBinary: \"terragrunt\",\n\t\tLogger:           logger.Discard,\n\t}\n\n\t_, err = InitE(t, options)\n\trequire.NoError(t, err)\n\n\tapplyOptions := &Options{\n\t\tTerragruntDir:    testFolder + \"/live\",\n\t\tTerragruntBinary: \"terragrunt\",\n\t\tLogger:           logger.Discard,\n\t\tTerraformArgs:    []string{\"apply\"},\n\t}\n\t_, err = StackRunE(t, applyOptions)\n\trequire.NoError(t, err)\n\n\tdefer func() {\n\t\tdestroyOptions := &Options{\n\t\t\tTerragruntDir:    testFolder + \"/live\",\n\t\t\tTerragruntBinary: \"terragrunt\",\n\t\t\tLogger:           logger.Discard,\n\t\t\tTerraformArgs:    []string{\"destroy\"},\n\t\t}\n\t\t_, _ = StackRunE(t, destroyOptions)\n\t}()\n\n\tkeys, err := StackOutputListAllE(t, options)\n\trequire.NoError(t, err)\n\trequire.NotEmpty(t, keys)\n\trequire.Contains(t, keys, \"mother\")\n}\n"
  },
  {
    "path": "modules/terragrunt/stack_run.go",
    "content": "package terragrunt\n\nimport (\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// StackRun calls terragrunt stack run and returns stdout/stderr\nfunc StackRun(t testing.TestingT, options *Options) string {\n\tout, err := StackRunE(t, options)\n\trequire.NoError(t, err)\n\treturn out\n}\n\n// StackRunE calls terragrunt stack run and returns stdout/stderr\nfunc StackRunE(t testing.TestingT, options *Options) (string, error) {\n\treturn runTerragruntStackCommandE(t, options, \"run\")\n}\n"
  },
  {
    "path": "modules/terragrunt/stack_run_test.go",
    "content": "package terragrunt\n\nimport (\n\t\"path\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/files\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestStackRun(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerraformFolderToTemp(\n\t\t\"testdata/terragrunt-stack-init\", t.Name())\n\trequire.NoError(t, err)\n\n\tInit(t, &Options{\n\t\tTerragruntDir:    path.Join(testFolder, \"live\"),\n\t\tTerragruntBinary: \"terragrunt\",\n\t\tTerraformArgs:    []string{\"-upgrade=true\"},\n\t})\n\n\tout := StackRun(t, &Options{\n\t\tTerragruntDir:    path.Join(testFolder, \"live\"),\n\t\tTerragruntBinary: \"terragrunt\",\n\t\tTerraformArgs:    []string{\"plan\"},\n\t})\n\n\trequire.True(t, containsEitherString(out, \"Processing unit\", \"Generating unit\"))\n\trequire.DirExists(t, path.Join(testFolder, \"live\", \".terragrunt-stack\"))\n}\n\nfunc TestStackRunE(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerraformFolderToTemp(\n\t\t\"testdata/terragrunt-stack-init\", t.Name())\n\trequire.NoError(t, err)\n\n\t// First initialize the stack\n\t_, err = InitE(t, &Options{\n\t\tTerragruntDir:    path.Join(testFolder, \"live\"),\n\t\tTerragruntBinary: \"terragrunt\",\n\t\tTerraformArgs:    []string{\"-upgrade=true\"},\n\t})\n\trequire.NoError(t, err)\n\n\t// Then run plan on the stack\n\tout, err := StackRunE(t, &Options{\n\t\tTerragruntDir:    path.Join(testFolder, \"live\"),\n\t\tTerragruntBinary: \"terragrunt\",\n\t\tTerraformArgs:    []string{\"plan\"},\n\t})\n\trequire.NoError(t, err)\n\n\t// Validate that generate command produced output\n\t// Terragrunt v0.80.4+ outputs \"Processing unit\", older versions output \"Generating unit\"\n\trequire.True(t, containsEitherString(out, \"Processing unit\", \"Generating unit\"), \"Output should contain either 'Processing unit' or 'Generating unit'\")\n\n\t// Verify that the .terragrunt-stack directory was created\n\tstackDir := path.Join(testFolder, \"live\", \".terragrunt-stack\")\n\trequire.DirExists(t, stackDir)\n\n\t// Verify that the expected unit directories were created\n\texpectedUnits := []string{\"mother\", \"father\", \"chicks/chick-1\", \"chicks/chick-2\"}\n\tfor _, unit := range expectedUnits {\n\t\tunitPath := path.Join(stackDir, unit)\n\t\trequire.DirExists(t, unitPath)\n\t}\n}\n\nfunc TestStackRunPlanWithNoColor(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerraformFolderToTemp(\n\t\t\"testdata/terragrunt-stack-init\", t.Name())\n\trequire.NoError(t, err)\n\n\t// First initialize the stack\n\t_, err = InitE(t, &Options{\n\t\tTerragruntDir:    path.Join(testFolder, \"live\"),\n\t\tTerragruntBinary: \"terragrunt\",\n\t\tTerraformArgs:    []string{\"-upgrade=true\"},\n\t})\n\trequire.NoError(t, err)\n\n\t// Run plan with no-color option\n\tout, err := StackRunE(t, &Options{\n\t\tTerragruntDir:    path.Join(testFolder, \"live\"),\n\t\tTerragruntBinary: \"terragrunt\",\n\t\tTerragruntArgs:   []string{\"--no-color\"},\n\t\tTerraformArgs:    []string{\"plan\"},\n\t})\n\trequire.NoError(t, err)\n\n\t// Validate that generate command produced output\n\t// Terragrunt v0.80.4+ outputs \"Processing unit\", older versions output \"Generating unit\"\n\trequire.True(t, containsEitherString(out, \"Processing unit\", \"Generating unit\"), \"Output should contain either 'Processing unit' or 'Generating unit'\")\n\n\t// Verify that the .terragrunt-stack directory was created\n\tstackDir := path.Join(testFolder, \"live\", \".terragrunt-stack\")\n\trequire.DirExists(t, stackDir)\n}\n\nfunc TestStackRunNonExistentDir(t *testing.T) {\n\tt.Parallel()\n\n\t// Test with non-existent directory\n\t_, err := StackRunE(t, &Options{\n\t\tTerragruntDir:    \"/non/existent/path\",\n\t\tTerragruntBinary: \"terragrunt\",\n\t})\n\trequire.Error(t, err)\n}\n\nfunc TestStackRunEmptyOptions(t *testing.T) {\n\tt.Parallel()\n\n\t// Test with minimal options to verify default behavior\n\t_, err := StackRunE(t, &Options{})\n\trequire.Error(t, err)\n\t// Should fail due to missing TerragruntDir\n}\n"
  },
  {
    "path": "modules/terragrunt/terragrunt_e2e_test.go",
    "content": "package terragrunt\n\nimport (\n\t\"encoding/json\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/files\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// TestTerragruntEndToEndIntegration is a comprehensive integration test that validates\n// the complete terragrunt workflow with TerragruntArgs and TerraformArgs.\n// This test exercises the fix for issue #1609 where args were being ignored.\nfunc TestTerragruntEndToEndIntegration(t *testing.T) {\n\tt.Parallel()\n\n\t// Setup: Copy test fixture to temp directory\n\ttestFolder, err := files.CopyTerragruntFolderToTemp(\"testdata/terragrunt-multi-plan\", t.Name())\n\trequire.NoError(t, err)\n\n\t// Configure options with TerragruntArgs\n\toptions := &Options{\n\t\tTerragruntDir: testFolder,\n\t\t// TerragruntArgs: Global terragrunt flags that should be respected\n\t\tTerragruntArgs: []string{\"--log-level\", \"error\"},\n\t}\n\n\t// Step 1: Plan with exit code (original bug scenario from issue #1609)\n\t// This is the exact scenario from the bug report\n\tt.Log(\"Step 1: Testing PlanAllExitCode with TerragruntArgs (original bug scenario)\")\n\texitCode, err := PlanAllExitCodeE(t, options)\n\trequire.NoError(t, err)\n\t// Should show changes (exit code 2) since nothing has been applied yet\n\trequire.Equal(t, 2, exitCode, \"Plan should detect changes\")\n\n\t// Step 2: Apply all modules\n\tt.Log(\"Step 2: Testing ApplyAll with TerragruntArgs\")\n\tapplyOutput := ApplyAll(t, options)\n\trequire.NotEmpty(t, applyOutput)\n\t// Verify TerragruntArgs: should not see info-level logs\n\trequire.NotContains(t, applyOutput, \"level=info\", \"TerragruntArgs should suppress info logs\")\n\n\t// Step 3: Plan again - should show no changes (exit code 0)\n\tt.Log(\"Step 3: Verifying infrastructure is up-to-date\")\n\texitCode, err = PlanAllExitCodeE(t, options)\n\trequire.NoError(t, err)\n\trequire.Equal(t, 0, exitCode, \"Plan should show no changes after apply\")\n\n\t// Step 4: Clean up - Destroy all\n\tt.Log(\"Step 4: Testing DestroyAll with TerragruntArgs\")\n\tdestroyOutput := DestroyAll(t, options)\n\trequire.NotEmpty(t, destroyOutput)\n\t// Verify TerragruntArgs: should not see info-level logs\n\trequire.NotContains(t, destroyOutput, \"level=info\", \"TerragruntArgs should suppress info logs\")\n\n\tt.Log(\"Integration test completed successfully - all args were properly passed\")\n}\n\n// TestStackEndToEndIntegration tests the complete stack workflow with args\nfunc TestStackEndToEndIntegration(t *testing.T) {\n\tt.Parallel()\n\n\t// Setup: Copy stack test fixture\n\ttestFolder, err := files.CopyTerraformFolderToTemp(\n\t\t\"testdata/terragrunt-stack-init\", t.Name())\n\trequire.NoError(t, err)\n\n\toptions := &Options{\n\t\tTerragruntDir:  filepath.Join(testFolder, \"live\"),\n\t\tTerragruntArgs: []string{\"--log-level\", \"error\"},\n\t}\n\n\t// Step 1: Initialize stack\n\tt.Log(\"Step 1: Initializing stack with TerragruntArgs\")\n\toutput, err := InitE(t, options)\n\trequire.NoError(t, err)\n\trequire.NotContains(t, output, \"level=info\", \"TerragruntArgs should suppress info logs\")\n\n\t// Step 2: Generate stack\n\tt.Log(\"Step 2: Generating stack with TerragruntArgs\")\n\tgenOutput, err := StackGenerateE(t, options)\n\trequire.NoError(t, err)\n\trequire.NotContains(t, genOutput, \"level=info\", \"TerragruntArgs should suppress info logs\")\n\n\t// Step 3: Run stack plan\n\tt.Log(\"Step 3: Running stack plan with TerraformArgs\")\n\trunOptions := *options\n\trunOptions.TerraformArgs = []string{\"plan\"}\n\tplanOutput, err := StackRunE(t, &runOptions)\n\trequire.NoError(t, err)\n\t// Check for common plan indicator (works with both Terraform and OpenTofu)\n\trequire.Contains(t, planOutput, \"will perform\")\n\n\t// Step 4: Clean stack\n\tt.Log(\"Step 4: Cleaning stack\")\n\t_, err = StackCleanE(t, options)\n\trequire.NoError(t, err)\n\n\tt.Log(\"Stack integration test completed successfully\")\n}\n\n// TestOutputAllJsonEndToEnd tests OutputAllJson extracts clean JSON from terragrunt output\nfunc TestOutputAllJsonEndToEnd(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerragruntFolderToTemp(\n\t\t\"testdata/terragrunt-multi-plan\", t.Name())\n\trequire.NoError(t, err)\n\n\toptions := &Options{TerragruntDir: testFolder}\n\n\tApplyAll(t, options)\n\tdefer DestroyAll(t, options)\n\n\toutput := OutputAllJson(t, options)\n\n\t// Contains module outputs, no log noise\n\trequire.Contains(t, output, `\"value\": \"foo\"`)\n\trequire.Contains(t, output, `\"value\": \"bar\"`)\n\t// Check for both old and new log format markers\n\trequire.NotContains(t, output, \"time=\")\n\trequire.NotContains(t, output, \" INFO \")\n\trequire.NotContains(t, output, \" STDOUT \")\n\trequire.NotContains(t, output, \"Group 1\")\n\trequire.NotContains(t, output, \"- Unit \")\n\n\t// Validate output contains at least 2 valid JSON objects (foo and bar modules)\n\tdec := json.NewDecoder(strings.NewReader(output))\n\tvar jsonCount int\n\tfor dec.More() {\n\t\tvar obj json.RawMessage\n\t\trequire.NoError(t, dec.Decode(&obj))\n\t\tjsonCount++\n\t}\n\trequire.GreaterOrEqual(t, jsonCount, 2)\n}\n"
  },
  {
    "path": "modules/terragrunt/terragrunt_example_test.go",
    "content": "package terragrunt\n\nimport (\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/files\"\n\t\"github.com/gruntwork-io/terratest/modules/terraform\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// This file demonstrates two approaches for testing Terragrunt configurations:\n//\n// 1. UNIT TESTING: Use the terraform module with TerraformBinary set to \"terragrunt\".\n//    This works because terragrunt is a thin wrapper around terraform for single units.\n//    See: TestTerragruntExample, TestTerragruntConsole\n//\n// 2. STACK TESTING: Use the dedicated terragrunt module with ApplyAll/DestroyAll.\n//    This is for testing a stack of Terragrunt units with dependencies using --all commands.\n//    See: TestTerragruntMultiModuleExample\n\n// TestTerragruntExample demonstrates testing a single Terragrunt unit using the terraform package.\n// For unit testing, use terraform.Options with TerraformBinary set to \"terragrunt\".\nfunc TestTerragruntExample(t *testing.T) {\n\tt.Parallel()\n\n\t// Copy the example folder to a temp folder to avoid state conflicts between parallel tests.\n\ttestFolder, err := files.CopyTerragruntFolderToTemp(\"../../examples/terragrunt-example\", t.Name())\n\trequire.NoError(t, err)\n\n\tterraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{\n\t\t// Set the path to the Terragrunt unit that will be tested.\n\t\tTerraformDir: testFolder,\n\t\t// Set the terraform binary path to terragrunt so that terratest uses terragrunt\n\t\t// instead of terraform. You must ensure that you have terragrunt downloaded and\n\t\t// available in your PATH.\n\t\tTerraformBinary: \"terragrunt\",\n\t})\n\n\t// Clean up resources with \"terragrunt destroy\" at the end of the test.\n\tdefer terraform.Destroy(t, terraformOptions)\n\n\t// Run \"terragrunt apply\". Under the hood, terragrunt will run \"terraform init\" and\n\t// \"terraform apply\". Fail the test if there are any errors.\n\tterraform.Apply(t, terraformOptions)\n\n\t// Run `terraform output` to get the values of output variables and check they have\n\t// the expected values.\n\t// Note: When using terragrunt, OutputAll is recommended because terragrunt returns\n\t// all outputs in the full JSON format even when a specific key is requested.\n\toutputs := terraform.OutputAll(t, terraformOptions)\n\tassert.Equal(t, \"one input another input\", outputs[\"output\"])\n}\n\n// TestTerragruntConsole demonstrates running terragrunt console command.\nfunc TestTerragruntConsole(t *testing.T) {\n\tt.Parallel()\n\n\t// Copy the example folder to a temp folder to avoid state conflicts between parallel tests.\n\ttestFolder, err := files.CopyTerragruntFolderToTemp(\"../../examples/terragrunt-example\", t.Name())\n\trequire.NoError(t, err)\n\n\tterraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{\n\t\tTerraformDir:    testFolder,\n\t\tTerraformBinary: \"terragrunt\",\n\t\tStdin:           strings.NewReader(\"local.mylocal\"),\n\t})\n\n\tdefer terraform.Destroy(t, terraformOptions)\n\n\t// Run \"terragrunt run -- console\".\n\tout := terraform.RunTerraformCommand(t, terraformOptions, \"run\", \"--\", \"console\")\n\tassert.Contains(t, out, `\"local variable named mylocal\"`)\n}\n\n// TestTerragruntMultiModuleExample demonstrates testing a stack of Terragrunt units\n// using the dedicated terragrunt package. Use this approach when you have a stack of\n// units with dependencies that need to be applied/destroyed together using --all.\nfunc TestTerragruntMultiModuleExample(t *testing.T) {\n\tt.Parallel()\n\n\t// Copy the entire example folder (including modules) to a temp folder.\n\t// We copy the parent folder because terragrunt.hcl files reference ../modules.\n\ttestFolder, err := files.CopyTerragruntFolderToTemp(\n\t\t\"../../examples/terragrunt-multi-module-example\", t.Name())\n\trequire.NoError(t, err)\n\n\toptions := &Options{\n\t\t// Run from the live subfolder where the terragrunt configs are\n\t\tTerragruntDir: filepath.Join(testFolder, \"live\"),\n\t\t// Optional: Set log level for cleaner output\n\t\tTerragruntArgs: []string{\"--log-level\", \"error\"},\n\t}\n\n\t// Clean up all modules with \"terragrunt destroy --all\" at the end of the test.\n\t// DestroyAll respects the reverse dependency order.\n\tdefer DestroyAll(t, options)\n\n\t// Run \"terragrunt apply --all\". This applies all modules in dependency order.\n\tApplyAll(t, options)\n\n\t// Verify the plan shows no changes (infrastructure is up-to-date)\n\texitCode := PlanAllExitCode(t, options)\n\tassert.Equal(t, 0, exitCode, \"Plan should show no changes after apply\")\n}\n"
  },
  {
    "path": "modules/terragrunt/testdata/terragrunt-multi-plan/bar/main.tf",
    "content": "output \"test\" {\n  value = \"bar\"\n}\n"
  },
  {
    "path": "modules/terragrunt/testdata/terragrunt-multi-plan/bar/terragrunt.hcl",
    "content": "terraform {\n  source = \"..//bar\"\n  extra_arguments \"common_vars\" {\n    commands = get_terraform_commands_that_need_vars()\n    arguments = [\n      \"-var-file=terraform.tfvars\"\n    ]\n  }\n}\n"
  },
  {
    "path": "modules/terragrunt/testdata/terragrunt-multi-plan/foo/main.tf",
    "content": "output \"test\" {\n  value = \"foo\"\n}"
  },
  {
    "path": "modules/terragrunt/testdata/terragrunt-multi-plan/foo/terragrunt.hcl",
    "content": "terraform {\n  source = \"..//foo\"\n  extra_arguments \"common_vars\" {\n    commands = get_terraform_commands_that_need_vars()\n    arguments = [\n      \"-var-file=terraform.tfvars\"\n    ]\n  }\n}\n"
  },
  {
    "path": "modules/terragrunt/testdata/terragrunt-no-error/main.tf",
    "content": "output \"test\" {\n  value = \"Hello, World\"\n}"
  },
  {
    "path": "modules/terragrunt/testdata/terragrunt-no-error/terragrunt.hcl",
    "content": "terraform {\n  source = \"..//terragrunt-no-error\"\n  extra_arguments \"common_vars\" {\n    commands = get_terraform_commands_that_need_vars()\n    arguments = [\n      \"-var-file=terraform.tfvars\"\n    ]\n  }\n}\n"
  },
  {
    "path": "modules/terragrunt/testdata/terragrunt-output/main.tf",
    "content": "output \"str\" {\n  value = \"str\"\n}\n\noutput \"list\" {\n  value = [\"a\", \"b\", \"c\"]\n}\n\noutput \"map\" {\n  value = { foo = \"bar\" }\n}"
  },
  {
    "path": "modules/terragrunt/testdata/terragrunt-output/terragrunt.hcl",
    "content": "terraform {\n  # Intentionally empty\n}"
  },
  {
    "path": "modules/terragrunt/testdata/terragrunt-stack-init/live/placeholder.tf",
    "content": "# Placeholder Terraform file for Terragrunt stack tests "
  },
  {
    "path": "modules/terragrunt/testdata/terragrunt-stack-init/live/terragrunt.hcl",
    "content": "# Minimal terragrunt.hcl required for stack commands "
  },
  {
    "path": "modules/terragrunt/testdata/terragrunt-stack-init/live/terragrunt.stack.hcl",
    "content": "unit \"mother\" {\n\tsource = \"../units/chicken\"\n\tpath   = \"mother\"\n}\n\nunit \"father\" {\n\tsource = \"../units/chicken\"\n\tpath   = \"father\"\n}\n\nunit \"chick_1\" {\n\tsource = \"../units/chick\"\n\tpath   = \"chicks/chick-1\"\n}\n\nunit \"chick_2\" {\n\tsource = \"../units/chick\"\n\tpath   = \"chicks/chick-2\"\n}\n\n"
  },
  {
    "path": "modules/terragrunt/testdata/terragrunt-stack-init/units/chick/main.tf",
    "content": "\nresource \"local_file\" \"file\" {\n  content  = \"chick\"\n  filename = \"${path.module}/test.txt\"\n}\n\noutput \"output\" {\n  value = local_file.file.filename\n}\n"
  },
  {
    "path": "modules/terragrunt/testdata/terragrunt-stack-init/units/chick/terragrunt.hcl",
    "content": "\nterraform {\n  source = \".\"\n}"
  },
  {
    "path": "modules/terragrunt/testdata/terragrunt-stack-init/units/chicken/main.tf",
    "content": "\nresource \"local_file\" \"file\" {\n  content  = \"chicken\"\n  filename = \"${path.module}/test.txt\"\n}\n\noutput \"output\" {\n  value = local_file.file.filename\n}\n"
  },
  {
    "path": "modules/terragrunt/testdata/terragrunt-stack-init/units/chicken/terragrunt.hcl",
    "content": "\nterraform {\n  source = \".\"\n}"
  },
  {
    "path": "modules/terragrunt/testdata/terragrunt-stack-init/units/father/main.tf",
    "content": "\nresource \"local_file\" \"file\" {\n  content  = \"father\"\n  filename = \"${path.module}/test.txt\"\n}\n\noutput \"output\" {\n  value = local_file.file.filename\n}\n"
  },
  {
    "path": "modules/terragrunt/testdata/terragrunt-stack-init/units/father/terragrunt.hcl",
    "content": "\nterraform {\n  source = \".\"\n}"
  },
  {
    "path": "modules/terragrunt/testdata/terragrunt-stack-init/units/mother/main.tf",
    "content": "\nresource \"local_file\" \"file\" {\n  content  = \"mother\"\n  filename = \"${path.module}/test.txt\"\n}\n\noutput \"output\" {\n  value = local_file.file.filename\n}\n"
  },
  {
    "path": "modules/terragrunt/testdata/terragrunt-stack-init/units/mother/terragrunt.hcl",
    "content": "\nterraform {\n  source = \".\"\n}"
  },
  {
    "path": "modules/terragrunt/testdata/terragrunt-stack-init-error/main.tf",
    "content": "# Simple Terraform configuration\nresource \"null_resource\" \"test\" {\n  provisioner \"local-exec\" {\n    command = \"echo 'Test resource'\"\n  }\n} "
  },
  {
    "path": "modules/terragrunt/testdata/terragrunt-stack-init-error/terragrunt.hcl",
    "content": "terraform {\n  source = \"..//terragrunt-stack-init-error\"\n  extra_arguments \"common_vars\" {\n    commands = get_terraform_commands_that_need_vars()\n    arguments = [\n      \"-var-file=terraform.tfvars\"\n    ]\n  }\n}\n\n# This is intentionally invalid HCL syntax - missing closing brace\ninputs = {\n  test_var = \"test_value\"\n  # Missing closing brace for the inputs block "
  },
  {
    "path": "modules/terragrunt/testdata/terragrunt-with-plan-error/main.tf",
    "content": "output \"test\" {\n  value = var.test\n}\n"
  },
  {
    "path": "modules/terragrunt/testdata/terragrunt-with-plan-error/terragrunt.hcl",
    "content": "terraform {\n  source = \"..//terraform-with-plan-error\"\n  extra_arguments \"common_vars\" {\n    commands = get_terraform_commands_that_need_vars()\n    arguments = [\n      \"-var-file=terraform.tfvars\"\n    ]\n  }\n}\n"
  },
  {
    "path": "modules/terragrunt/validate.go",
    "content": "package terragrunt\n\nimport (\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// ValidateAll runs terragrunt run --all validate with the given options and returns stdout/stderr\nfunc ValidateAll(t testing.TestingT, options *Options) string {\n\tout, err := ValidateAllE(t, options)\n\trequire.NoError(t, err)\n\treturn out\n}\n\n// ValidateAllE runs terragrunt run --all -- validate with the given options and returns stdout/stderr\nfunc ValidateAllE(t testing.TestingT, options *Options) (string, error) {\n\targs := buildRunArgs([]string{\"--all\"}, []string{\"validate\"})\n\treturn runTerragruntCommandE(t, options, \"run\", args...)\n}\n\n// Validate runs terragrunt run validate for a single unit and returns stdout/stderr.\nfunc Validate(t testing.TestingT, options *Options) string {\n\tout, err := ValidateE(t, options)\n\trequire.NoError(t, err)\n\treturn out\n}\n\n// ValidateE runs terragrunt run -- validate for a single unit and returns stdout/stderr.\nfunc ValidateE(t testing.TestingT, options *Options) (string, error) {\n\targs := buildRunArgs([]string{}, []string{\"validate\"})\n\treturn runTerragruntCommandE(t, options, \"run\", args...)\n}\n\n// InitAndValidate runs terragrunt init followed by validate for a single unit and returns the validate stdout/stderr.\nfunc InitAndValidate(t testing.TestingT, options *Options) string {\n\tout, err := InitAndValidateE(t, options)\n\trequire.NoError(t, err)\n\treturn out\n}\n\n// InitAndValidateE runs terragrunt init followed by validate for a single unit and returns the validate stdout/stderr.\nfunc InitAndValidateE(t testing.TestingT, options *Options) (string, error) {\n\tif _, err := InitE(t, options); err != nil {\n\t\treturn \"\", err\n\t}\n\treturn ValidateE(t, options)\n}\n"
  },
  {
    "path": "modules/terragrunt/validate_test.go",
    "content": "package terragrunt\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/files\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestValidateAll(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerragruntFolderToTemp(\"testdata/terragrunt-multi-plan\", t.Name())\n\trequire.NoError(t, err)\n\n\tValidateAll(t, &Options{\n\t\tTerragruntDir:    testFolder,\n\t\tTerragruntBinary: \"terragrunt\",\n\t})\n}\n\nfunc TestValidate(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerragruntFolderToTemp(\"testdata/terragrunt-no-error\", t.Name())\n\trequire.NoError(t, err)\n\n\tValidate(t, &Options{\n\t\tTerragruntDir:    testFolder,\n\t\tTerragruntBinary: \"terragrunt\",\n\t})\n}\n\nfunc TestInitAndValidate(t *testing.T) {\n\tt.Parallel()\n\n\ttestFolder, err := files.CopyTerragruntFolderToTemp(\"testdata/terragrunt-no-error\", t.Name())\n\trequire.NoError(t, err)\n\n\tout := InitAndValidate(t, &Options{\n\t\tTerragruntDir:    testFolder,\n\t\tTerragruntBinary: \"terragrunt\",\n\t})\n\trequire.NotEmpty(t, out)\n}\n"
  },
  {
    "path": "modules/test-structure/save_test_data.go",
    "content": "package test_structure\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/gruntwork-io/terratest/modules/aws\"\n\t\"github.com/gruntwork-io/terratest/modules/files\"\n\t\"github.com/gruntwork-io/terratest/modules/k8s\"\n\t\"github.com/gruntwork-io/terratest/modules/logger\"\n\t\"github.com/gruntwork-io/terratest/modules/packer\"\n\t\"github.com/gruntwork-io/terratest/modules/ssh\"\n\t\"github.com/gruntwork-io/terratest/modules/terraform\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// SaveTerraformOptions serializes and saves TerraformOptions into the given folder. This allows you to create TerraformOptions during setup\n// and to reuse that TerraformOptions later during validation and teardown.\nfunc SaveTerraformOptions(t testing.TestingT, testFolder string, terraformOptions *terraform.Options) {\n\tSaveTestData(t, formatTerraformOptionsPath(testFolder), true, terraformOptions)\n}\n\n// SaveTerraformOptionsIfNotPresent serializes and saves TerraformOptions into the given folder if the file does not exist or the json is\n// empty. This allows you to create TerraformOptions during setup and to reuse that TerraformOptions later during validation and teardown,\n// but will prevent overwritting the contents and potentially duplicating resources.\nfunc SaveTerraformOptionsIfNotPresent(t testing.TestingT, testFolder string, terraformOptions *terraform.Options) {\n\tSaveTestData(t, formatTerraformOptionsPath(testFolder), false, terraformOptions)\n}\n\n// LoadTerraformOptions loads and unserializes TerraformOptions from the given folder. This allows you to reuse a TerraformOptions that was\n// created during an earlier setup step in later validation and teardown steps.\nfunc LoadTerraformOptions(t testing.TestingT, testFolder string) *terraform.Options {\n\tvar terraformOptions terraform.Options\n\tLoadTestData(t, formatTerraformOptionsPath(testFolder), &terraformOptions)\n\treturn &terraformOptions\n}\n\n// formatTerraformOptionsPath formats a path to save TerraformOptions in the given folder.\nfunc formatTerraformOptionsPath(testFolder string) string {\n\treturn FormatTestDataPath(testFolder, \"TerraformOptions.json\")\n}\n\n// SavePackerOptions serializes and saves PackerOptions into the given folder. This allows you to create PackerOptions during setup\n// and to reuse that PackerOptions later during validation and teardown.\nfunc SavePackerOptions(t testing.TestingT, testFolder string, packerOptions *packer.Options) {\n\tSaveTestData(t, formatPackerOptionsPath(testFolder), true, packerOptions)\n}\n\n// LoadPackerOptions loads and unserializes PackerOptions from the given folder. This allows you to reuse a PackerOptions that was\n// created during an earlier setup step in later validation and teardown steps.\nfunc LoadPackerOptions(t testing.TestingT, testFolder string) *packer.Options {\n\tvar packerOptions packer.Options\n\tLoadTestData(t, formatPackerOptionsPath(testFolder), &packerOptions)\n\treturn &packerOptions\n}\n\n// formatPackerOptionsPath formats a path to save PackerOptions in the given folder.\nfunc formatPackerOptionsPath(testFolder string) string {\n\treturn FormatTestDataPath(testFolder, \"PackerOptions.json\")\n}\n\n// SaveEc2KeyPair serializes and saves an Ec2KeyPair into the given folder. This allows you to create an Ec2KeyPair during setup\n// and to reuse that Ec2KeyPair later during validation and teardown.\nfunc SaveEc2KeyPair(t testing.TestingT, testFolder string, keyPair *aws.Ec2Keypair) {\n\tsaveTestData(t, formatEc2KeyPairPath(testFolder), true, keyPair, false)\n}\n\n// LoadEc2KeyPair loads and unserializes an Ec2KeyPair from the given folder. This allows you to reuse an Ec2KeyPair that was\n// created during an earlier setup step in later validation and teardown steps.\nfunc LoadEc2KeyPair(t testing.TestingT, testFolder string) *aws.Ec2Keypair {\n\tvar keyPair aws.Ec2Keypair\n\tLoadTestData(t, formatEc2KeyPairPath(testFolder), &keyPair)\n\treturn &keyPair\n}\n\n// formatEc2KeyPairPath formats a path to save an Ec2KeyPair in the given folder.\nfunc formatEc2KeyPairPath(testFolder string) string {\n\treturn FormatTestDataPath(testFolder, \"Ec2KeyPair.json\")\n}\n\n// SaveSshKeyPair serializes and saves an SshKeyPair into the given folder. This allows you to create an SshKeyPair during setup\n// and to reuse that SshKeyPair later during validation and teardown.\nfunc SaveSshKeyPair(t testing.TestingT, testFolder string, keyPair *ssh.KeyPair) {\n\tSaveTestData(t, formatSshKeyPairPath(testFolder), true, keyPair)\n}\n\n// LoadSshKeyPair loads and unserializes an SshKeyPair from the given folder. This allows you to reuse an SshKeyPair that was\n// created during an earlier setup step in later validation and teardown steps.\nfunc LoadSshKeyPair(t testing.TestingT, testFolder string) *ssh.KeyPair {\n\tvar keyPair ssh.KeyPair\n\tLoadTestData(t, formatSshKeyPairPath(testFolder), &keyPair)\n\treturn &keyPair\n}\n\n// formatSshKeyPairPath formats a path to save an SshKeyPair in the given folder.\nfunc formatSshKeyPairPath(testFolder string) string {\n\treturn FormatTestDataPath(testFolder, \"SshKeyPair.json\")\n}\n\n// SaveKubectlOptions serializes and saves KubectlOptions into the given folder. This allows you to create a KubectlOptions during setup\n// and reuse that KubectlOptions later during validation and teardown.\nfunc SaveKubectlOptions(t testing.TestingT, testFolder string, kubectlOptions *k8s.KubectlOptions) {\n\tSaveTestData(t, formatKubectlOptionsPath(testFolder), true, kubectlOptions)\n}\n\n// LoadKubectlOptions loads and unserializes a KubectlOptions from the given folder. This allows you to reuse a KubectlOptions that was\n// created during an earlier setup step in later validation and teardown steps.\nfunc LoadKubectlOptions(t testing.TestingT, testFolder string) *k8s.KubectlOptions {\n\tvar kubectlOptions k8s.KubectlOptions\n\tLoadTestData(t, formatKubectlOptionsPath(testFolder), &kubectlOptions)\n\treturn &kubectlOptions\n}\n\n// formatKubectlOptionsPath formats a path to save a KubectlOptions in the given folder.\nfunc formatKubectlOptionsPath(testFolder string) string {\n\treturn FormatTestDataPath(testFolder, \"KubectlOptions.json\")\n}\n\n// SaveString serializes and saves a uniquely named string value into the given folder. This allows you to create one or more string\n// values during one stage -- each with a unique name -- and to reuse those values during later stages.\nfunc SaveString(t testing.TestingT, testFolder string, name string, val string) {\n\tpath := formatNamedTestDataPath(testFolder, name)\n\tSaveTestData(t, path, true, val)\n}\n\n// LoadString loads and unserializes a uniquely named string value from the given folder. This allows you to reuse one or more string\n// values that were created during an earlier setup step in later steps.\nfunc LoadString(t testing.TestingT, testFolder string, name string) string {\n\tvar val string\n\tLoadTestData(t, formatNamedTestDataPath(testFolder, name), &val)\n\treturn val\n}\n\n// SaveInt saves a uniquely named int value into the given folder. This allows you to create one or more int\n// values during one stage -- each with a unique name -- and to reuse those values during later stages.\nfunc SaveInt(t testing.TestingT, testFolder string, name string, val int) {\n\tpath := formatNamedTestDataPath(testFolder, name)\n\tSaveTestData(t, path, true, val)\n}\n\n// LoadInt loads a uniquely named int value from the given folder. This allows you to reuse one or more int\n// values that were created during an earlier setup step in later steps.\nfunc LoadInt(t testing.TestingT, testFolder string, name string) int {\n\tvar val int\n\tLoadTestData(t, formatNamedTestDataPath(testFolder, name), &val)\n\treturn val\n}\n\n// SaveArtifactID serializes and saves an Artifact ID into the given folder. This allows you to build an Artifact during setup and to reuse that\n// Artifact later during validation and teardown.\nfunc SaveArtifactID(t testing.TestingT, testFolder string, artifactID string) {\n\tSaveString(t, testFolder, \"Artifact\", artifactID)\n}\n\n// LoadArtifactID loads and unserializes an Artifact ID from the given folder. This allows you to reuse an Artifact that was created during an\n// earlier setup step in later validation and teardown steps.\nfunc LoadArtifactID(t testing.TestingT, testFolder string) string {\n\treturn LoadString(t, testFolder, \"Artifact\")\n}\n\n// SaveAmiId serializes and saves an AMI ID into the given folder. This allows you to build an AMI during setup and to reuse that\n// AMI later during validation and teardown.\n//\n// Deprecated: Use SaveArtifactID instead.\nfunc SaveAmiId(t testing.TestingT, testFolder string, amiId string) {\n\tSaveString(t, testFolder, \"AMI\", amiId)\n}\n\n// LoadAmiId loads and unserializes an AMI ID from the given folder. This allows you to reuse an AMI  that was created during an\n// earlier setup step in later validation and teardown steps.\n//\n// Deprecated: Use LoadArtifactID instead.\nfunc LoadAmiId(t testing.TestingT, testFolder string) string {\n\treturn LoadString(t, testFolder, \"AMI\")\n}\n\n// formatNamedTestDataPath formats a path to save an arbitrary named value in the given folder.\nfunc formatNamedTestDataPath(testFolder string, name string) string {\n\tfilename := fmt.Sprintf(\"%s.json\", name)\n\treturn FormatTestDataPath(testFolder, filename)\n}\n\n// FormatTestDataPath formats a path to save test data.\nfunc FormatTestDataPath(testFolder string, filename string) string {\n\treturn filepath.Join(testFolder, \".test-data\", filename)\n}\n\n// SaveTestData serializes and saves a value used at test time to the given path. This allows you to create some sort of test data\n// (e.g., TerraformOptions) during setup and to reuse this data later during validation and teardown. If `overwrite` is `true`,\n// any contents that exist in the file found at `path` will be overwritten. This has the potential for causing duplicated resources\n// and should be used with caution. If `overwrite` is `false`, the save will be skipped and a warning will be logged.\nfunc SaveTestData(t testing.TestingT, path string, overwrite bool, value interface{}) {\n\tsaveTestData(t, path, overwrite, value, true)\n}\n\n// saveTestData serializes and saves a value used at test time to the given path. This allows you to create some sort of test data\n// (e.g., TerraformOptions) during setup and to reuse this data later during validation and teardown. If `overwrite` is `true`,\n// any contents that exist in the file found at `path` will be overwritten. This has the potential for causing duplicated resources\n// and should be used with caution. If `overwrite` is `false`, the save will be skipped and a warning will be logged.\n// If `loggedVal` is `true`, the value will be logged as JSON.\nfunc saveTestData(t testing.TestingT, path string, overwrite bool, value interface{}, loggedVal bool) {\n\tlogger.Default.Logf(t, \"Storing test data in %s so it can be reused later\", path)\n\n\tif IsTestDataPresent(t, path) {\n\t\tif overwrite {\n\t\t\tlogger.Default.Logf(t, \"[WARNING] The named test data at path %s is non-empty. Save operation will overwrite existing value with \\\"%v\\\".\\n.\", path, value)\n\t\t} else {\n\t\t\tlogger.Default.Logf(t, \"[WARNING] The named test data at path %s is non-empty. Skipping save operation to prevent overwriting existing value with \\\"%v\\\".\\n.\", path, value)\n\t\t\treturn\n\t\t}\n\t}\n\n\tbytes, err := json.Marshal(value)\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to convert value %s to JSON: %v\", path, err)\n\t}\n\n\tif loggedVal {\n\t\tlogger.Default.Logf(t, \"Marshalled JSON: %s\", string(bytes))\n\t}\n\n\tparentDir := filepath.Dir(path)\n\tif err := os.MkdirAll(parentDir, 0755); err != nil {\n\t\tt.Fatalf(\"Failed to create folder %s: %v\", parentDir, err)\n\t}\n\n\tif err := os.WriteFile(path, bytes, 0644); err != nil {\n\t\tt.Fatalf(\"Failed to save value %s: %v\", path, err)\n\t}\n}\n\n// LoadTestData loads and unserializes a value stored at the given path. The value should be a pointer to a struct into which the\n// value will be deserialized. This allows you to reuse some sort of test data (e.g., TerraformOptions) from earlier\n// setup steps in later validation and teardown steps.\nfunc LoadTestData(t testing.TestingT, path string, value interface{}) {\n\tlogger.Default.Logf(t, \"Loading test data from %s\", path)\n\n\tbytes, err := os.ReadFile(path)\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to load value from %s: %v\", path, err)\n\t}\n\n\tif err := json.Unmarshal(bytes, value); err != nil {\n\t\tt.Fatalf(\"Failed to parse JSON for value %s: %v\", path, err)\n\t}\n}\n\n// IsTestDataPresent returns true if a file exists at $path and the test data there is non-empty.\nfunc IsTestDataPresent(t testing.TestingT, path string) bool {\n\texists, err := files.FileExistsE(path)\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to load test data from %s due to unexpected error: %v\", path, err)\n\t}\n\tif !exists {\n\t\treturn false\n\t}\n\n\tbytes, err := os.ReadFile(path)\n\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to load test data from %s due to unexpected error: %v\", path, err)\n\t}\n\n\tif isEmptyJSON(t, bytes) {\n\t\treturn false\n\t}\n\n\treturn true\n}\n\n// isEmptyJSON returns true if the given bytes are empty, or in a valid JSON format that can reasonably be considered empty.\n// The types used are based on the type possibilities listed at https://golang.org/src/encoding/json/decode.go?s=4062:4110#L51\nfunc isEmptyJSON(t testing.TestingT, bytes []byte) bool {\n\tvar value interface{}\n\n\tif len(bytes) == 0 {\n\t\treturn true\n\t}\n\n\tif err := json.Unmarshal(bytes, &value); err != nil {\n\t\tt.Fatalf(\"Failed to parse JSON while testing whether it is empty: %v\", err)\n\t}\n\n\tif value == nil {\n\t\treturn true\n\t}\n\n\tvalueBool, ok := value.(bool)\n\tif ok && !valueBool {\n\t\treturn true\n\t}\n\n\tvalueFloat64, ok := value.(float64)\n\tif ok && valueFloat64 == 0 {\n\t\treturn true\n\t}\n\n\tvalueString, ok := value.(string)\n\tif ok && valueString == \"\" {\n\t\treturn true\n\t}\n\n\tvalueSlice, ok := value.([]interface{})\n\tif ok && len(valueSlice) == 0 {\n\t\treturn true\n\t}\n\n\tvalueMap, ok := value.(map[string]interface{})\n\tif ok && len(valueMap) == 0 {\n\t\treturn true\n\t}\n\n\treturn false\n}\n\n// CleanupTestData cleans up the test data at the given path.\nfunc CleanupTestData(t testing.TestingT, path string) {\n\tif files.FileExists(path) {\n\t\tlogger.Default.Logf(t, \"Cleaning up test data from %s\", path)\n\t\tif err := os.Remove(path); err != nil {\n\t\t\tt.Fatalf(\"Failed to clean up file at %s: %v\", path, err)\n\t\t}\n\t} else {\n\t\tlogger.Default.Logf(t, \"%s does not exist. Nothing to cleanup.\", path)\n\t}\n}\n\n// CleanupTestDataFolder cleans up the .test-data folder inside the given folder.\n// If there are any errors, fail the test.\nfunc CleanupTestDataFolder(t testing.TestingT, path string) {\n\terr := CleanupTestDataFolderE(t, path)\n\trequire.NoError(t, err)\n}\n\n// CleanupTestDataFolderE cleans up the .test-data folder inside the given folder.\nfunc CleanupTestDataFolderE(t testing.TestingT, path string) error {\n\tpath = filepath.Join(path, \".test-data\")\n\texists, err := files.FileExistsE(path)\n\tif err != nil {\n\t\tlogger.Default.Logf(t, \"Failed to clean up test data folder at %s: %v\", path, err)\n\t\treturn err\n\t}\n\n\tif !exists {\n\t\tlogger.Default.Logf(t, \"%s does not exist. Nothing to cleanup.\", path)\n\t\treturn nil\n\t}\n\tif err := os.RemoveAll(path); err != nil {\n\t\tlogger.Default.Logf(t, \"Failed to clean up test data folder at %s: %v\", path, err)\n\t\treturn err\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "modules/test-structure/save_test_data_test.go",
    "content": "package test_structure\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/aws\"\n\t\"github.com/gruntwork-io/terratest/modules/files\"\n\t\"github.com/gruntwork-io/terratest/modules/k8s\"\n\t\"github.com/gruntwork-io/terratest/modules/logger\"\n\t\"github.com/gruntwork-io/terratest/modules/ssh\"\n\t\"github.com/gruntwork-io/terratest/modules/terraform\"\n\tgotesting \"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\ntype testData struct {\n\tFoo string\n\tBar bool\n\tBaz map[string]interface{}\n}\n\nfunc TestSaveAndLoadTestData(t *testing.T) {\n\tt.Parallel()\n\n\tisTestDataPresent := IsTestDataPresent(t, \"/file/that/does/not/exist\")\n\tassert.False(t, isTestDataPresent, \"Expected no test data would be present because no test data file exists.\")\n\n\ttmpFile, err := os.CreateTemp(\"\", \"save-and-load-test-data\")\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to create temp dir: %v\", err)\n\t}\n\n\texpectedData := testData{\n\t\tFoo: \"foo\",\n\t\tBar: true,\n\t\tBaz: map[string]interface{}{\"abc\": \"def\", \"ghi\": 1.0, \"klm\": false},\n\t}\n\n\tisTestDataPresent = IsTestDataPresent(t, tmpFile.Name())\n\tassert.False(t, isTestDataPresent, \"Expected no test data would be present because file exists but no data has been written yet.\")\n\n\toverwrite := true\n\tSaveTestData(t, tmpFile.Name(), overwrite, expectedData)\n\n\tisTestDataPresent = IsTestDataPresent(t, tmpFile.Name())\n\tassert.True(t, isTestDataPresent, \"Expected test data would be present because file exists and data has been written to file.\")\n\n\tactualData := testData{}\n\tLoadTestData(t, tmpFile.Name(), &actualData)\n\tassert.Equal(t, expectedData, actualData)\n\n\toverwritingData := testData{\n\t\tFoo: \"foo\",\n\t\tBar: false,\n\t\tBaz: map[string]interface{}{\"123\": \"456\", \"789\": 1.0, \"0\": false},\n\t}\n\tSaveTestData(t, tmpFile.Name(), !overwrite, overwritingData)\n\tLoadTestData(t, tmpFile.Name(), &actualData)\n\tassert.Equal(t, expectedData, actualData)\n\n\tCleanupTestData(t, tmpFile.Name())\n\tassert.False(t, files.FileExists(tmpFile.Name()))\n}\n\nfunc TestIsEmptyJson(t *testing.T) {\n\tt.Parallel()\n\n\tvar jsonValue []byte\n\tvar isEmpty bool\n\n\tjsonValue = []byte(\"null\")\n\tisEmpty = isEmptyJSON(t, jsonValue)\n\tassert.True(t, isEmpty, `The JSON literal \"null\" should be treated as an empty value.`)\n\n\tjsonValue = []byte(\"false\")\n\tisEmpty = isEmptyJSON(t, jsonValue)\n\tassert.True(t, isEmpty, `The JSON literal \"false\" should be treated as an empty value.`)\n\n\tjsonValue = []byte(\"true\")\n\tisEmpty = isEmptyJSON(t, jsonValue)\n\tassert.False(t, isEmpty, `The JSON literal \"true\" should be treated as a non-empty value.`)\n\n\tjsonValue = []byte(\"0\")\n\tisEmpty = isEmptyJSON(t, jsonValue)\n\tassert.True(t, isEmpty, `The JSON literal \"0\" should be treated as an empty value.`)\n\n\tjsonValue = []byte(\"1\")\n\tisEmpty = isEmptyJSON(t, jsonValue)\n\tassert.False(t, isEmpty, `The JSON literal \"1\" should be treated as a non-empty value.`)\n\n\tjsonValue = []byte(\"{}\")\n\tisEmpty = isEmptyJSON(t, jsonValue)\n\tassert.True(t, isEmpty, `The JSON value \"{}\" should be treated as an empty value.`)\n\n\tjsonValue = []byte(`{ \"key\": \"val\" }`)\n\tisEmpty = isEmptyJSON(t, jsonValue)\n\tassert.False(t, isEmpty, `The JSON value { \"key\": \"val\" } should be treated as a non-empty value.`)\n\n\tjsonValue = []byte(`[]`)\n\tisEmpty = isEmptyJSON(t, jsonValue)\n\tassert.True(t, isEmpty, `The JSON value \"[]\" should be treated as an empty value.`)\n\n\tjsonValue = []byte(`[{ \"key\": \"val\" }]`)\n\tisEmpty = isEmptyJSON(t, jsonValue)\n\tassert.False(t, isEmpty, `The JSON value [{ \"key\": \"val\" }] should be treated as a non-empty value.`)\n}\n\nfunc TestSaveAndLoadTerraformOptions(t *testing.T) {\n\tt.Parallel()\n\n\ttmpFolder := t.TempDir()\n\n\texpectedData := &terraform.Options{\n\t\tTerraformDir: \"/abc/def/ghi\",\n\t\tVars:         map[string]interface{}{},\n\t}\n\tSaveTerraformOptions(t, tmpFolder, expectedData)\n\n\tactualData := LoadTerraformOptions(t, tmpFolder)\n\tassert.Equal(t, expectedData, actualData)\n}\n\nfunc TestSaveTerraformOptionsIfNotPresent(t *testing.T) {\n\tt.Parallel()\n\n\ttmpFolder := t.TempDir()\n\n\texpectedData := &terraform.Options{\n\t\tTerraformDir: \"/abc/def/ghi\",\n\t\tVars:         map[string]interface{}{},\n\t}\n\tSaveTerraformOptionsIfNotPresent(t, tmpFolder, expectedData)\n\n\toverwritingData := &terraform.Options{\n\t\tTerraformDir: \"/123/456/789\",\n\t\tVars:         map[string]interface{}{},\n\t}\n\tSaveTerraformOptionsIfNotPresent(t, tmpFolder, overwritingData)\n\n\tactualData := LoadTerraformOptions(t, tmpFolder)\n\tassert.Equal(t, expectedData, actualData)\n}\n\nfunc TestSaveTerraformOptionsOverwrite(t *testing.T) {\n\tt.Parallel()\n\n\ttmpFolder := t.TempDir()\n\n\toriginaData := &terraform.Options{\n\t\tTerraformDir: \"/abc/def/ghi\",\n\t\tVars:         map[string]interface{}{},\n\t}\n\tSaveTerraformOptions(t, tmpFolder, originaData)\n\n\toverwritingData := &terraform.Options{\n\t\tTerraformDir: \"/123/456/789\",\n\t\tVars:         map[string]interface{}{},\n\t}\n\tSaveTerraformOptions(t, tmpFolder, overwritingData)\n\n\tactualData := LoadTerraformOptions(t, tmpFolder)\n\tassert.Equal(t, overwritingData, actualData)\n}\n\nfunc TestSaveAndLoadAmiId(t *testing.T) {\n\tt.Parallel()\n\n\ttmpFolder := t.TempDir()\n\n\texpectedData := \"ami-abcd1234\"\n\tSaveAmiId(t, tmpFolder, expectedData)\n\n\tactualData := LoadAmiId(t, tmpFolder)\n\tassert.Equal(t, expectedData, actualData)\n}\n\nfunc TestSaveAndLoadArtifactID(t *testing.T) {\n\tt.Parallel()\n\n\ttmpFolder := t.TempDir()\n\n\texpectedData := \"terratest-packer-example-2018-08-08t15-35-19z\"\n\tSaveArtifactID(t, tmpFolder, expectedData)\n\n\tactualData := LoadArtifactID(t, tmpFolder)\n\tassert.Equal(t, expectedData, actualData)\n}\n\nfunc TestSaveAndLoadNamedStrings(t *testing.T) {\n\tt.Parallel()\n\n\ttmpFolder := t.TempDir()\n\n\tname1 := \"test-ami\"\n\texpectedData1 := \"ami-abcd1234\"\n\n\tname2 := \"test-ami2\"\n\texpectedData2 := \"ami-xyz98765\"\n\n\tname3 := \"test-image\"\n\texpectedData3 := \"terratest-packer-example-2018-08-08t15-35-19z\"\n\n\tname4 := \"test-image2\"\n\texpectedData4 := \"terratest-packer-example-2018-01-03t12-35-00z\"\n\n\tSaveString(t, tmpFolder, name1, expectedData1)\n\tSaveString(t, tmpFolder, name2, expectedData2)\n\tSaveString(t, tmpFolder, name3, expectedData3)\n\tSaveString(t, tmpFolder, name4, expectedData4)\n\n\tactualData1 := LoadString(t, tmpFolder, name1)\n\tactualData2 := LoadString(t, tmpFolder, name2)\n\tactualData3 := LoadString(t, tmpFolder, name3)\n\tactualData4 := LoadString(t, tmpFolder, name4)\n\n\tassert.Equal(t, expectedData1, actualData1)\n\tassert.Equal(t, expectedData2, actualData2)\n\tassert.Equal(t, expectedData3, actualData3)\n\tassert.Equal(t, expectedData4, actualData4)\n}\n\nfunc TestSaveDuplicateTestData(t *testing.T) {\n\tt.Parallel()\n\n\ttmpFolder := t.TempDir()\n\n\tname := \"hello-world\"\n\tval1 := \"hello world\"\n\tval2 := \"buenos dias, mundo\"\n\n\tSaveString(t, tmpFolder, name, val1)\n\tSaveString(t, tmpFolder, name, val2)\n\n\tactualVal := LoadString(t, tmpFolder, name)\n\n\tassert.Equal(t, val2, actualVal, \"Actual test data should use overwritten values\")\n}\n\nfunc TestSaveAndLoadNamedInts(t *testing.T) {\n\tt.Parallel()\n\n\ttmpFolder := t.TempDir()\n\n\tname1 := \"test-int1\"\n\texpectedData1 := 23842834\n\n\tname2 := \"test-int2\"\n\texpectedData2 := 52\n\n\tSaveInt(t, tmpFolder, name1, expectedData1)\n\tSaveInt(t, tmpFolder, name2, expectedData2)\n\n\tactualData1 := LoadInt(t, tmpFolder, name1)\n\tactualData2 := LoadInt(t, tmpFolder, name2)\n\n\tassert.Equal(t, expectedData1, actualData1)\n\tassert.Equal(t, expectedData2, actualData2)\n}\n\nfunc TestSaveAndLoadKubectlOptions(t *testing.T) {\n\tt.Parallel()\n\n\ttmpFolder := t.TempDir()\n\n\texpectedData := &k8s.KubectlOptions{\n\t\tContextName: \"terratest-context\",\n\t\tConfigPath:  \"~/.kube/config\",\n\t\tNamespace:   \"default\",\n\t\tEnv: map[string]string{\n\t\t\t\"TERRATEST_ENV_VAR\": \"terratest\",\n\t\t},\n\t}\n\tSaveKubectlOptions(t, tmpFolder, expectedData)\n\n\tactualData := LoadKubectlOptions(t, tmpFolder)\n\tassert.Equal(t, expectedData, actualData)\n}\n\ntype tStringLogger struct {\n\tsb strings.Builder\n}\n\nfunc (l *tStringLogger) Logf(t gotesting.TestingT, format string, args ...interface{}) {\n\tl.sb.WriteString(fmt.Sprintf(format, args...))\n\tl.sb.WriteRune('\\n')\n}\n\nfunc TestSaveAndLoadEC2KeyPair(t *testing.T) {\n\tdef, slogger := logger.Default, &tStringLogger{}\n\tlogger.Default = logger.New(slogger)\n\tt.Cleanup(func() {\n\t\tlogger.Default = def\n\t})\n\n\tkeyPair, err := ssh.GenerateRSAKeyPairE(t, 2048)\n\trequire.NoError(t, err)\n\tec2KeyPair := &aws.Ec2Keypair{\n\t\tKeyPair: keyPair,\n\t\tName:    \"test-ec2-key-pair\",\n\t\tRegion:  \"us-east-1\",\n\t}\n\tstoredEC2KeyPair, err := json.Marshal(ec2KeyPair)\n\trequire.NoError(t, err)\n\n\ttmpFolder := t.TempDir()\n\tSaveEc2KeyPair(t, tmpFolder, ec2KeyPair)\n\tloadedEC2KeyPair := LoadEc2KeyPair(t, tmpFolder)\n\tassert.Equal(t, ec2KeyPair, loadedEC2KeyPair)\n\n\tassert.NotContains(t, slogger.sb.String(), string(storedEC2KeyPair), \"stored ec2 key pair should not be logged\")\n}\n"
  },
  {
    "path": "modules/test-structure/test_structure.go",
    "content": "package test_structure\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/gruntwork-io/terratest/modules/git\"\n\n\tgo_test \"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/files\"\n\t\"github.com/gruntwork-io/terratest/modules/logger\"\n\t\"github.com/gruntwork-io/terratest/modules/opa\"\n\t\"github.com/gruntwork-io/terratest/modules/terraform\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// SKIP_STAGE_ENV_VAR_PREFIX is the prefix used for skipping stage environment variables.\nconst SKIP_STAGE_ENV_VAR_PREFIX = \"SKIP_\"\n\n// RunTestStage executes the given test stage (e.g., setup, teardown, validation) if an environment variable of the name\n// `SKIP_<stageName>` (e.g., SKIP_teardown) is not set.\nfunc RunTestStage(t testing.TestingT, stageName string, stage func()) {\n\tenvVarName := fmt.Sprintf(\"%s%s\", SKIP_STAGE_ENV_VAR_PREFIX, stageName)\n\tif os.Getenv(envVarName) == \"\" {\n\t\tlogger.Default.Logf(t, \"The '%s' environment variable is not set, so executing stage '%s'.\", envVarName, stageName)\n\t\tstage()\n\t} else {\n\t\tlogger.Default.Logf(t, \"The '%s' environment variable is set, so skipping stage '%s'.\", envVarName, stageName)\n\t}\n}\n\n// SkipStageEnvVarSet returns true if an environment variable is set instructing Terratest to skip a test stage. This can be an easy way\n// to tell if the tests are running in a local dev environment vs a CI server.\nfunc SkipStageEnvVarSet() bool {\n\tfor _, environmentVariable := range os.Environ() {\n\t\tif strings.HasPrefix(environmentVariable, SKIP_STAGE_ENV_VAR_PREFIX) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// CopyTerraformFolderToTemp copies the given root folder to a randomly-named temp folder and return the path to the\n// given terraform modules folder within the new temp root folder. This is useful when running multiple tests in\n// parallel against the same set of Terraform files to ensure the tests don't overwrite each other's .terraform working\n// directory and terraform.tfstate files. To ensure relative paths work, we copy over the entire root folder to a temp\n// folder, and then return the path within that temp folder to the given terraform module dir, which is where the actual\n// test will be running.\n// For example, suppose you had the target terraform folder you want to test in \"/examples/terraform-aws-example\"\n// relative to the repo root. If your tests reside in the \"/test\" relative to the root, then you will use this as\n// follows:\n//\n//\t// Root folder where terraform files should be (relative to the test folder)\n//\trootFolder := \"..\"\n//\n//\t// Relative path to terraform module being tested from the root folder\n//\tterraformFolderRelativeToRoot := \"examples/terraform-aws-example\"\n//\n//\t// Copy the terraform folder to a temp folder\n//\ttempTestFolder := test_structure.CopyTerraformFolderToTemp(t, rootFolder, terraformFolderRelativeToRoot)\n//\n//\t// Make sure to use the temp test folder in the terraform options\n//\tterraformOptions := &terraform.Options{\n//\t\t\tTerraformDir: tempTestFolder,\n//\t}\n//\n// Note that if any of the SKIP_<stage> environment variables is set, we assume this is a test in the local dev where\n// there are no other concurrent tests running and we want to be able to cache test data between test stages, so in that\n// case, we do NOT copy anything to a temp folder, and return the path to the original terraform module folder instead.\nfunc CopyTerraformFolderToTemp(t testing.TestingT, rootFolder string, terraformModuleFolder string) string {\n\treturn CopyTerraformFolderToDest(t, rootFolder, terraformModuleFolder, os.TempDir())\n}\n\n// CopyTerraformFolderToDest copies the given root folder to a randomly-named temp folder and return the path to the\n// given terraform modules folder within the new temp root folder. This is useful when running multiple tests in\n// parallel against the same set of Terraform files to ensure the tests don't overwrite each other's .terraform working\n// directory and terraform.tfstate files. To ensure relative paths work, we copy over the entire root folder to a temp\n// folder, and then return the path within that temp folder to the given terraform module dir, which is where the actual\n// test will be running.\n// For example, suppose you had the target terraform folder you want to test in \"/examples/terraform-aws-example\"\n// relative to the repo root. If your tests reside in the \"/test\" relative to the root, then you will use this as\n// follows:\n//\n//\t// Destination for the copy of the files.  In this example we are using the Azure Dev Ops variable\n//\t// for the folder that is cleaned after each pipeline job.\n//\tdestRootFolder := os.Getenv(\"AGENT_TEMPDIRECTORY\")\n//\n//\t// Root folder where terraform files should be (relative to the test folder)\n//\trootFolder := \"..\"\n//\n//\t// Relative path to terraform module being tested from the root folder\n//\tterraformFolderRelativeToRoot := \"examples/terraform-aws-example\"\n//\n//\t// Copy the terraform folder to a temp folder\n//\ttempTestFolder := test_structure.CopyTerraformFolderToTemp(t, rootFolder, terraformFolderRelativeToRoot, destRootFolder)\n//\n//\t// Make sure to use the temp test folder in the terraform options\n//\tterraformOptions := &terraform.Options{\n//\t\t\tTerraformDir: tempTestFolder,\n//\t}\n//\n// Note that if any of the SKIP_<stage> environment variables is set, we assume this is a test in the local dev where\n// there are no other concurrent tests running and we want to be able to cache test data between test stages, so in that\n// case, we do NOT copy anything to a temp folder, and return the path to the original terraform module folder instead.\nfunc CopyTerraformFolderToDest(t testing.TestingT, rootFolder string, terraformModuleFolder string, destRootFolder string) string {\n\tif SkipStageEnvVarSet() {\n\t\tlogger.Default.Logf(t, \"A SKIP_XXX environment variable is set. Using original examples folder rather than a temp folder so we can cache data between stages for faster local testing.\")\n\t\treturn filepath.Join(rootFolder, terraformModuleFolder)\n\t}\n\n\tfullTerraformModuleFolder := filepath.Join(rootFolder, terraformModuleFolder)\n\n\texists, err := files.FileExistsE(fullTerraformModuleFolder)\n\trequire.NoError(t, err)\n\tif !exists {\n\t\tt.Fatal(files.DirNotFoundError{Directory: fullTerraformModuleFolder})\n\t}\n\n\ttmpRootFolder, err := files.CopyTerraformFolderToDest(rootFolder, destRootFolder, cleanName(t.Name()))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\ttmpTestFolder := filepath.Join(tmpRootFolder, terraformModuleFolder)\n\n\t// Log temp folder so we can see it\n\tlogger.Default.Logf(t, \"Copied terraform folder %s to %s\", fullTerraformModuleFolder, tmpTestFolder)\n\n\treturn tmpTestFolder\n}\n\nfunc cleanName(originalName string) string {\n\tparts := strings.Split(originalName, \"/\")\n\treturn parts[len(parts)-1]\n}\n\n// ValidateAllTerraformModules automatically finds all folders specified in RootDir that contain .tf files and runs\n// InitAndValidate in all of them.\n// Filters down to only those paths passed in ValidationOptions.IncludeDirs, if passed.\n// Excludes any folders specified in the ValidationOptions.ExcludeDirs. IncludeDirs will take precedence over ExcludeDirs\n// Use the NewValidationOptions method to pass relative paths for either of these options to have the full paths built\n// Note that go_test is an alias to Golang's native testing package created to avoid naming conflicts with Terratest's\n// own testing package. We are using the native testing.T here because Terratest's testing.T struct does not implement Run\n// Note that we have opted to place the ValidateAllTerraformModules function here instead of in the terraform package\n// to avoid import cycling\nfunc ValidateAllTerraformModules(t *go_test.T, opts *ValidationOptions) {\n\trunValidateOnAllTerraformModules(\n\t\tt,\n\t\topts,\n\t\tfunc(t *go_test.T, _ ValidateFileType, tfOpts *terraform.Options) {\n\t\t\tterraform.InitAndValidate(t, tfOpts)\n\t\t},\n\t)\n}\n\n// OPAEvalAllTerraformModules automatically finds all folders specified in RootDir that contain .tf files and runs\n// OPAEval in all of them. The behavior of this function is similar to ValidateAllTerraformModules. Refer to the docs of\n// that function for more details.\nfunc OPAEvalAllTerraformModules(\n\tt *go_test.T,\n\topts *ValidationOptions,\n\topaEvalOpts *opa.EvalOptions,\n\tresultQuery string,\n) {\n\tif opts.FileType != TF {\n\t\tt.Fatalf(\"OPAEvalAllTerraformModules currently only works with Terraform modules\")\n\t}\n\trunValidateOnAllTerraformModules(\n\t\tt,\n\t\topts,\n\t\tfunc(t *go_test.T, _ ValidateFileType, tfOpts *terraform.Options) {\n\t\t\tterraform.OPAEval(t, tfOpts, opaEvalOpts, resultQuery)\n\t\t},\n\t)\n}\n\n// runValidateOnAllTerraformModules main driver for ValidateAllTerraformModules and OPAEvalAllTerraformModules. Refer to\n// the function docs of ValidateAllTerraformModules for more details.\nfunc runValidateOnAllTerraformModules(\n\tt *go_test.T,\n\topts *ValidationOptions,\n\tvalidationFunc func(t *go_test.T, fileType ValidateFileType, tfOps *terraform.Options),\n) {\n\t// Find the Git root\n\tgitRoot, err := git.GetRepoRootForDirE(t, opts.RootDir)\n\trequire.NoError(t, err)\n\n\t// Find the relative path between the root dir and the git root\n\trelPath, err := filepath.Rel(gitRoot, opts.RootDir)\n\trequire.NoError(t, err)\n\n\t// Copy git root to tmp\n\ttestFolder := CopyTerraformFolderToTemp(t, gitRoot, relPath)\n\trequire.NotNil(t, testFolder)\n\n\t// Clone opts and override the root dir to the temp folder\n\tclonedOpts, err := CloneWithNewRootDir(opts, testFolder)\n\trequire.NoError(t, err)\n\n\t// Find TF modules\n\tdirsToValidate, readErr := FindTerraformModulePathsInRootE(clonedOpts)\n\trequire.NoError(t, readErr)\n\n\tfor _, dir := range dirsToValidate {\n\t\tdir := dir\n\t\tt.Run(strings.TrimLeft(dir, \"/\"), func(t *go_test.T) {\n\t\t\t// Run the validation function on the test folder that was copied to /tmp to avoid any potential conflicts\n\t\t\t// with tests that may not use the same copy to /tmp behavior\n\t\t\ttfOpts := &terraform.Options{TerraformDir: dir}\n\t\t\tvalidationFunc(t, clonedOpts.FileType, tfOpts)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "modules/test-structure/test_structure_test.go",
    "content": "package test_structure\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestCopyToTempFolder(t *testing.T) {\n\ttempFolder := CopyTerraformFolderToTemp(t, \"../../\", \"examples\")\n\tt.Log(tempFolder)\n}\n\nfunc TestCopySubtestToTempFolder(t *testing.T) {\n\tt.Run(\"Subtest\", func(t *testing.T) {\n\t\ttempFolder := CopyTerraformFolderToTemp(t, \"../../\", \"examples\")\n\t\tt.Log(tempFolder)\n\t})\n}\n\n// TestValidateAllTerraformModulesSucceedsOnValidTerraform points at a simple text fixture Terraform module that is\n// known to be valid\nfunc TestValidateAllTerraformModulesSucceedsOnValidTerraform(t *testing.T) {\n\tcwd, err := os.Getwd()\n\trequire.NoError(t, err)\n\n\t// Use the test fixtures directory as the RootDir for ValidationOptions\n\tprojectRootDir := filepath.Join(cwd, \"../../test/fixtures\")\n\n\topts, optsErr := NewValidationOptions(projectRootDir, []string{\"terraform-validation-valid\"}, []string{})\n\trequire.NoError(t, optsErr)\n\n\tValidateAllTerraformModules(t, opts)\n}\n\nfunc TestNewValidationOptionsRejectsEmptyRootDir(t *testing.T) {\n\t_, err := NewValidationOptions(\"\", []string{}, []string{})\n\trequire.Error(t, err)\n}\n\nfunc TestFindTerraformModulePathsInRootEExamples(t *testing.T) {\n\tcwd, cwdErr := os.Getwd()\n\trequire.NoError(t, cwdErr)\n\n\topts, optsErr := NewValidationOptions(filepath.Join(cwd, \"../../\"), []string{}, []string{})\n\trequire.NoError(t, optsErr)\n\n\tsubDirs, err := FindTerraformModulePathsInRootE(opts)\n\trequire.NoError(t, err)\n\t// There are many valid Terraform modules in the root/examples directory of the Terratest project, so we should get back many results\n\trequire.Greater(t, len(subDirs), 0)\n}\n\n// This test calls ValidateAllTerraformModules on the Terratest root directory\nfunc TestValidateAllTerraformModulesOnTerratest(t *testing.T) {\n\tcwd, err := os.Getwd()\n\trequire.NoError(t, err)\n\n\tprojectRootDir := filepath.Join(cwd, \"../..\")\n\n\topts, optsErr := NewValidationOptions(projectRootDir, []string{}, []string{\n\t\t\"test/fixtures/terraform-with-plan-error\",\n\t\t\"modules/terragrunt/testdata/terragrunt-with-plan-error\",\n\t\t\"examples/terraform-backend-example\",\n\t})\n\trequire.NoError(t, optsErr)\n\n\tValidateAllTerraformModules(t, opts)\n}\n\n// Verify ExcludeDirs is working properly, by explicitly passing a list of two test fixture modules to exclude\n// and ensuring at the end that they do not appear in the returned slice of sub directories to validate\n// Then, re-run the function with no exclusions and ensure the excluded paths ARE returned in the result set when no\n// exclusions are passed\nfunc TestFindTerraformModulePathsInRootEWithResultsExclusion(t *testing.T) {\n\n\tcwd, cwdErr := os.Getwd()\n\trequire.NoError(t, cwdErr)\n\n\tprojectRootDir := filepath.Join(cwd, \"../..\")\n\n\t// First, call the FindTerraformModulePathsInRootE method with several exclusions\n\texclusions := []string{\n\t\tfilepath.Join(\"test\", \"fixtures\", \"terraform-output\"),\n\t\tfilepath.Join(\"test\", \"fixtures\", \"terraform-output-map\"),\n\t}\n\n\topts, optsErr := NewValidationOptions(projectRootDir, []string{}, exclusions)\n\trequire.NoError(t, optsErr)\n\n\tsubDirs, err := FindTerraformModulePathsInRootE(opts)\n\trequire.NoError(t, err)\n\trequire.Greater(t, len(subDirs), 0)\n\t// Ensure none of the excluded paths were returned by FindTerraformModulePathsInRootE\n\tfor _, exclusion := range exclusions {\n\t\tassert.False(t, slices.Contains(subDirs, filepath.Join(projectRootDir, exclusion)))\n\t}\n\n\t// Next, call the same function but this time without exclusions and ensure that the excluded paths\n\t// exist in the non-excluded result set\n\toptsWithoutExclusions, optswoErr := NewValidationOptions(projectRootDir, []string{}, []string{})\n\trequire.NoError(t, optswoErr)\n\n\tsubDirsWithoutExclusions, woExErr := FindTerraformModulePathsInRootE(optsWithoutExclusions)\n\trequire.NoError(t, woExErr)\n\trequire.Greater(t, len(subDirsWithoutExclusions), 0)\n\tfor _, exclusion := range exclusions {\n\t\tassert.True(t, slices.Contains(subDirsWithoutExclusions, filepath.Join(projectRootDir, exclusion)))\n\t}\n}\n"
  },
  {
    "path": "modules/test-structure/validate_struct.go",
    "content": "package test_structure\n\nimport (\n\t\"fmt\"\n\t\"path\"\n\t\"path/filepath\"\n\n\tgo_commons_collections \"github.com/gruntwork-io/go-commons/collections\"\n\t\"github.com/gruntwork-io/terratest/modules/collections\"\n\t\"github.com/gruntwork-io/terratest/modules/files\"\n\t\"github.com/mattn/go-zglob\"\n)\n\n// ValidateFileType is the underlying module type to search for when performing validation.\ntype ValidateFileType string\n\nconst (\n\t// TF represents repositories that contain Terraform code\n\tTF = \"*.tf\"\n)\n\n// ValidationOptions represent the configuration for a given validation sweep of a target repo\ntype ValidationOptions struct {\n\t// The target directory to recursively search for all Terraform directories (those that contain .tf files)\n\t// If you provide RootDir and do not pass entries in either IncludeDirs or ExcludeDirs, then all Terraform directories\n\t// From the RootDir, recursively, will be validated\n\tRootDir  string\n\tFileType ValidateFileType\n\t// If you only want to include certain sub directories of RootDir, add the absolute paths here. For example, if the\n\t// RootDir is /home/project and you want to only include /home/project/examples, add /home/project/examples here\n\t// Note that while the struct requires full paths, you can pass relative paths to the NewValidationOptions function\n\t// which will build the full paths based on the supplied RootDir\n\tIncludeDirs []string\n\t// If you want to explicitly exclude certain sub directories of RootDir, add the absolute paths here. For example, if the\n\t// RootDir is /home/project and you want to include everything EXCEPT /home/project/modules, add\n\t// /home/project/modules to this slice. Note that ExcludeDirs is only considered when IncludeDirs is not passed\n\t// Note that while the struct requires full paths, you can pass relative paths to the NewValidationOptions function\n\t// which will build the full paths based on the supplied RootDir\n\tExcludeDirs []string\n}\n\n// CloneWithNewRootDir clones the given opts with a new root dir. Updates all include and exclude dirs to be relative\n// to the new root dir.\nfunc CloneWithNewRootDir(opts *ValidationOptions, newRootDir string) (*ValidationOptions, error) {\n\tincludeDirs, err := buildRelPathsFromFull(opts.RootDir, opts.IncludeDirs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\texcludeDirs, err := buildRelPathsFromFull(opts.RootDir, opts.ExcludeDirs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tout, err := NewValidationOptions(newRootDir, includeDirs, excludeDirs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tout.FileType = opts.FileType\n\treturn out, nil\n}\n\n// configureBaseValidationOptions returns a pointer to a ValidationOptions struct configured with sane, override-able defaults\n// Note that the ValidationOptions's fields IncludeDirs and ExcludeDirs must be absolute paths, but this method will accept relative paths\n// and build the absolute paths when instantiating the ValidationOptions struct,  making it the preferred means of configuring\n// ValidationOptions.\n//\n// For example, if your RootDir is /home/project/ and you want to exclude \"modules\" and \"test\" you need\n// only pass the relative paths in your excludeDirs slice like so:\n// opts, err := NewValidationOptions(\"/home/project\", []string{}, []string{\"modules\", \"test\"})\nfunc configureBaseValidationOptions(rootDir string, includeDirs, excludeDirs []string) (*ValidationOptions, error) {\n\tvo := &ValidationOptions{\n\t\tRootDir:     \"\",\n\t\tIncludeDirs: []string{},\n\t\tExcludeDirs: []string{},\n\t}\n\n\tif rootDir == \"\" {\n\t\treturn nil, ValidationUndefinedRootDirErr{}\n\t}\n\n\tif !filepath.IsAbs(rootDir) {\n\t\trootDirAbs, err := filepath.Abs(rootDir)\n\t\tif err != nil {\n\t\t\treturn nil, ValidationAbsolutePathErr{rootDir: rootDir}\n\t\t}\n\t\trootDir = rootDirAbs\n\t}\n\n\tvo.RootDir = filepath.Clean(rootDir)\n\n\tif len(includeDirs) > 0 {\n\t\tvo.IncludeDirs = buildFullPathsFromRelative(vo.RootDir, includeDirs)\n\t}\n\n\tif len(excludeDirs) > 0 {\n\t\tvo.ExcludeDirs = buildFullPathsFromRelative(vo.RootDir, excludeDirs)\n\t}\n\n\treturn vo, nil\n}\n\n// NewValidationOptions returns a ValidationOptions struct, with override-able sane defaults, configured to find\n// and process all directories containing .tf files\nfunc NewValidationOptions(rootDir string, includeDirs, excludeDirs []string) (*ValidationOptions, error) {\n\topts, err := configureBaseValidationOptions(rootDir, includeDirs, excludeDirs)\n\tif err != nil {\n\t\treturn opts, err\n\t}\n\topts.FileType = TF\n\treturn opts, nil\n}\n\nfunc buildRelPathsFromFull(rootDir string, fullPaths []string) ([]string, error) {\n\tvar relPaths []string\n\tfor _, maybeFullPath := range fullPaths {\n\t\tif filepath.IsAbs(maybeFullPath) {\n\t\t\trelPath, err := filepath.Rel(rootDir, maybeFullPath)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\trelPaths = append(relPaths, relPath)\n\t\t} else {\n\t\t\trelPaths = append(relPaths, maybeFullPath)\n\t\t}\n\t}\n\treturn relPaths, nil\n}\n\nfunc buildFullPathsFromRelative(rootDir string, relativePaths []string) []string {\n\tvar fullPaths []string\n\tfor _, maybeRelativePath := range relativePaths {\n\t\t// If the relativePath is already an absolute path, don't modify it\n\t\tif filepath.IsAbs(maybeRelativePath) {\n\t\t\tfullPaths = append(fullPaths, filepath.Clean(maybeRelativePath))\n\t\t} else {\n\t\t\tfullPaths = append(fullPaths, filepath.Clean(filepath.Join(rootDir, maybeRelativePath)))\n\t\t}\n\t}\n\treturn fullPaths\n}\n\n// FindTerraformModulePathsInRootE returns a slice strings representing the filepaths for all valid Terraform\n// modules in the given RootDir, subject to the include / exclude filters.\nfunc FindTerraformModulePathsInRootE(opts *ValidationOptions) ([]string, error) {\n\t// Find all Terraform files (as specified by opts.FileType) from the configured RootDir\n\tpattern := fmt.Sprintf(\"%s/**/%s\", opts.RootDir, opts.FileType)\n\tmatches, err := zglob.Glob(pattern)\n\tif err != nil {\n\t\treturn matches, err\n\t}\n\t// Keep a unique set of the base dirs that contain Terraform files\n\tterraformDirSet := make(map[string]string)\n\tfor _, match := range matches {\n\t\t// The glob match returns all full paths to every target file, whereas we're only interested in their root\n\t\t// directories for the purposes of running Terraform validate\n\t\trootDir := path.Dir(match)\n\t\t// Don't include hidden .terraform directories when finding paths to validate\n\t\tif !files.PathContainsHiddenFileOrFolder(rootDir) {\n\t\t\tterraformDirSet[rootDir] = \"exists\"\n\t\t}\n\t}\n\n\t// Retrieve just the unique paths to each Terraform module directory from the map we're using as a set\n\tterraformDirs := go_commons_collections.Keys(terraformDirSet)\n\n\tif len(opts.IncludeDirs) > 0 {\n\t\tterraformDirs = collections.ListIntersection(terraformDirs, opts.IncludeDirs)\n\t}\n\n\tif len(opts.ExcludeDirs) > 0 {\n\t\tterraformDirs = collections.ListSubtract(terraformDirs, opts.ExcludeDirs)\n\t}\n\n\t// Filter out any filepaths that were explicitly included in opts.ExcludeDirs\n\treturn terraformDirs, nil\n}\n\n// Custom error types\n\n// ValidationAbsolutePathErr is returned when NewValidationOptions was unable to convert a non-absolute RootDir to\n// an absolute path\ntype ValidationAbsolutePathErr struct {\n\trootDir string\n}\n\nfunc (e ValidationAbsolutePathErr) Error() string {\n\treturn fmt.Sprintf(\"Could not convert RootDir: %s to absolute path\", e.rootDir)\n}\n\n// ValidationUndefinedRootDirErr is returned when NewValidationOptions is called without a RootDir argument\ntype ValidationUndefinedRootDirErr struct{}\n\nfunc (e ValidationUndefinedRootDirErr) Error() string {\n\treturn \"RootDir must be defined in ValidationOptions passed to ValidateAllTerraformModules\"\n}\n"
  },
  {
    "path": "modules/testing/types.go",
    "content": "// Package testing provides the TestingT interface used throughout Terratest.\npackage testing\n\n// TestingT is an interface that describes the implementation of the testing object\n// that the majority of Terratest functions accept as first argument.\n// Using an interface that describes testing.T instead of the actual implementation\n// makes terratest usable in a wider variety of contexts (e.g. use with ginkgo : https://godoc.org/github.com/onsi/ginkgo#GinkgoT)\ntype TestingT interface {\n\t// Fail marks the function as having failed but continues execution.\n\tFail()\n\t// FailNow marks the function as having failed and stops its execution\n\t// by calling runtime.Goexit (which then runs all deferred calls in the\n\t// current goroutine).\n\t// Execution will continue at the next test or benchmark.\n\t// FailNow must be called from the goroutine running the\n\t// test or benchmark function, not from other goroutines\n\t// created during the test. Calling FailNow does not stop\n\t// those other goroutines.\n\tFailNow()\n\tFatal(args ...interface{})\n\t// Fatalf is equivalent to Logf followed by FailNow.\n\tFatalf(format string, args ...interface{})\n\t// Error is equivalent to Log followed by Fail.\n\tError(args ...interface{})\n\t// Errorf is equivalent to Logf followed by Fail.\n\tErrorf(format string, args ...interface{})\n\t// Name returns the name of the running test or benchmark.\n\tName() string\n}\n"
  },
  {
    "path": "modules/version-checker/errors.go",
    "content": "package version_checker\n\n// VersionMismatchErr is an error to indicate version mismatch.\ntype VersionMismatchErr struct {\n\terrorMessage string\n}\n\nfunc (r *VersionMismatchErr) Error() string {\n\treturn r.errorMessage\n}\n"
  },
  {
    "path": "modules/version-checker/version_checker.go",
    "content": "package version_checker\n\nimport (\n\t\"fmt\"\n\t\"regexp\"\n\n\t\"github.com/gruntwork-io/terratest/modules/terraform\"\n\n\t\"github.com/gruntwork-io/terratest/modules/shell\"\n\t\"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/hashicorp/go-version\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// VersionCheckerBinary is an enum for supported version checking.\ntype VersionCheckerBinary int\n\n// List of binaries supported for version checking.\nconst (\n\tDocker VersionCheckerBinary = iota\n\tTerraform\n\tPacker\n)\n\nconst (\n\t// versionRegexMatcher is a regex used to extract version string from shell command output.\n\tversionRegexMatcher = `\\d+(\\.\\d+)+`\n\t// defaultVersionArg is a default arg to pass in to get version output from shell command.\n\tdefaultVersionArg = \"--version\"\n)\n\ntype CheckVersionParams struct {\n\t// BinaryPath is a path to the binary you want to check the version for.\n\tBinaryPath string\n\t// Binary is the name of the binary you want to check the version for.\n\tBinary VersionCheckerBinary\n\t// VersionConstraint is a string literal containing one or more conditions, which are separated by commas.\n\t// More information here:https://www.terraform.io/language/expressions/version-constraints\n\tVersionConstraint string\n\t// WorkingDir is a directory you want to run the shell command.\n\tWorkingDir string\n}\n\n// CheckVersionE checks whether the given Binary version is greater than or equal\n// to the given expected version.\nfunc CheckVersionE(\n\tt testing.TestingT,\n\tparams CheckVersionParams) error {\n\n\tif err := validateParams(params); err != nil {\n\t\treturn err\n\t}\n\n\tbinaryVersion, err := getVersionWithShellCommand(t, params)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn checkVersionConstraint(binaryVersion, params.VersionConstraint)\n}\n\n// CheckVersion checks whether the given Binary version is greater than or equal to the\n// given expected version and fails if it's not.\nfunc CheckVersion(\n\tt testing.TestingT,\n\tparams CheckVersionParams) {\n\trequire.NoError(t, CheckVersionE(t, params))\n}\n\n// Validate whether the given params contains valid data to check version.\nfunc validateParams(params CheckVersionParams) error {\n\t// Check for empty parameters\n\tif params.WorkingDir == \"\" {\n\t\treturn fmt.Errorf(\"set WorkingDir in params\")\n\t} else if params.VersionConstraint == \"\" {\n\t\treturn fmt.Errorf(\"set VersionConstraint in params\")\n\t}\n\n\t// Check the format of the version constraint if present.\n\tif _, err := version.NewConstraint(params.VersionConstraint); params.VersionConstraint != \"\" && err != nil {\n\t\treturn fmt.Errorf(\n\t\t\t\"invalid version constraint format found {%s}\", params.VersionConstraint)\n\t}\n\n\treturn nil\n}\n\n// getVersionWithShellCommand get version by running a shell command.\nfunc getVersionWithShellCommand(t testing.TestingT, params CheckVersionParams) (string, error) {\n\tvar versionArg = defaultVersionArg\n\tbinary, err := getBinary(params)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// Run a shell command to get the version string.\n\toutput, err := shell.RunCommandAndGetOutputE(t, shell.Command{\n\t\tCommand:    binary,\n\t\tArgs:       []string{versionArg},\n\t\tWorkingDir: params.WorkingDir,\n\t\tEnv:        map[string]string{},\n\t})\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to run shell command for Binary {%s} \"+\n\t\t\t\"w/ version args {%s}: %w\", binary, versionArg, err)\n\t}\n\n\tversionStr, err := extractVersionFromShellCommandOutput(output)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to extract version from shell \"+\n\t\t\t\"command output {%s}: %w\", output, err)\n\t}\n\n\treturn versionStr, nil\n}\n\n// getBinary retrieves the binary to use from the given params.\nfunc getBinary(params CheckVersionParams) (string, error) {\n\t// Use BinaryPath if it is set, otherwise use the binary enum.\n\tif params.BinaryPath != \"\" {\n\t\treturn params.BinaryPath, nil\n\t}\n\n\tswitch params.Binary {\n\tcase Docker:\n\t\treturn \"docker\", nil\n\tcase Packer:\n\t\treturn \"packer\", nil\n\tcase Terraform:\n\t\treturn terraform.DefaultExecutable, nil\n\tdefault:\n\t\treturn \"\", fmt.Errorf(\"unsupported Binary for checking versions {%d}\", params.Binary)\n\t}\n}\n\n// extractVersionFromShellCommandOutput extracts version with regex string matching\n// from the given shell command output string.\nfunc extractVersionFromShellCommandOutput(output string) (string, error) {\n\tregexMatcher := regexp.MustCompile(versionRegexMatcher)\n\tversionStr := regexMatcher.FindString(output)\n\tif versionStr == \"\" {\n\t\treturn \"\", fmt.Errorf(\"failed to find version using regex matcher\")\n\t}\n\n\treturn versionStr, nil\n}\n\n// checkVersionConstraint checks whether the given version pass the version constraint.\n//\n// It returns Error for ill-formatted version string and VersionMismatchErr for\n// minimum version check failure.\n//\n//\tcheckVersionConstraint(t, \"1.2.31\",  \">= 1.2.0, < 2.0.0\") - no error\n//\tcheckVersionConstraint(t, \"1.0.31\",  \">= 1.2.0, < 2.0.0\") - error\nfunc checkVersionConstraint(actualVersionStr string, versionConstraintStr string) error {\n\tactualVersion, err := version.NewVersion(actualVersionStr)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"invalid version format found for actualVersionStr: %s\", actualVersionStr)\n\t}\n\n\tversionConstraint, err := version.NewConstraint(versionConstraintStr)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"invalid version format found for versionConstraint: %s\", versionConstraintStr)\n\t}\n\n\tif !versionConstraint.Check(actualVersion) {\n\t\treturn &VersionMismatchErr{\n\t\t\terrorMessage: fmt.Sprintf(\"actual version {%s} failed \"+\n\t\t\t\t\"the version constraint {%s}\", actualVersionStr, versionConstraint),\n\t\t}\n\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "modules/version-checker/version_checker_test.go",
    "content": "package version_checker\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestParamValidation(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname                 string\n\t\tparam                CheckVersionParams\n\t\tcontainError         bool\n\t\texpectedErrorMessage string\n\t}{\n\t\t{\n\t\t\tname:                 \"Empty Params\",\n\t\t\tparam:                CheckVersionParams{},\n\t\t\tcontainError:         true,\n\t\t\texpectedErrorMessage: \"set WorkingDir in params\",\n\t\t},\n\t\t{\n\t\t\tname: \"Missing VersionConstraint\",\n\t\t\tparam: CheckVersionParams{\n\t\t\t\tBinary:            Docker,\n\t\t\t\tVersionConstraint: \"\",\n\t\t\t\tWorkingDir:        \".\",\n\t\t\t},\n\t\t\tcontainError:         true,\n\t\t\texpectedErrorMessage: \"set VersionConstraint in params\",\n\t\t},\n\t\t{\n\t\t\tname: \"Invalid Version Constraint Format\",\n\t\t\tparam: CheckVersionParams{\n\t\t\t\tBinary:            Docker,\n\t\t\t\tVersionConstraint: \"abc\",\n\t\t\t\tWorkingDir:        \".\",\n\t\t\t},\n\t\t\tcontainError:         true,\n\t\t\texpectedErrorMessage: \"invalid version constraint format found {abc}\",\n\t\t},\n\t\t{\n\t\t\tname: \"Success\",\n\t\t\tparam: CheckVersionParams{\n\t\t\t\tBinary:            Docker,\n\t\t\t\tVersionConstraint: \">1.2.3\",\n\t\t\t\tWorkingDir:        \".\",\n\t\t\t},\n\t\t\tcontainError:         false,\n\t\t\texpectedErrorMessage: \"\",\n\t\t},\n\t}\n\n\tfor _, tc := range tests {\n\t\terr := validateParams(tc.param)\n\t\tif tc.containError {\n\t\t\trequire.EqualError(t, err, tc.expectedErrorMessage, tc.name)\n\t\t} else {\n\t\t\trequire.NoError(t, err, tc.name)\n\t\t}\n\t}\n}\n\nfunc TestExtractVersionFromShellCommandOutput(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname                 string\n\t\toutputStr            string\n\t\texpectedVersionStr   string\n\t\tcontainError         bool\n\t\texpectedErrorMessage string\n\t}{\n\t\t{\n\t\t\tname:                 \"Stand-alone version string\",\n\t\t\toutputStr:            \"version is 1.2.3\",\n\t\t\texpectedVersionStr:   \"1.2.3\",\n\t\t\tcontainError:         false,\n\t\t\texpectedErrorMessage: \"\",\n\t\t},\n\t\t{\n\t\t\tname:                 \"version string with v prefix\",\n\t\t\toutputStr:            \"version is v1.0.0\",\n\t\t\texpectedVersionStr:   \"1.0.0\",\n\t\t\tcontainError:         false,\n\t\t\texpectedErrorMessage: \"\",\n\t\t},\n\t\t{\n\t\t\tname:                 \"2 digit version string\",\n\t\t\toutputStr:            \"version is v1.0\",\n\t\t\texpectedVersionStr:   \"1.0\",\n\t\t\tcontainError:         false,\n\t\t\texpectedErrorMessage: \"\",\n\t\t},\n\t\t{\n\t\t\tname:                 \"invalid output string\",\n\t\t\toutputStr:            \"version is vabc\",\n\t\t\texpectedVersionStr:   \"\",\n\t\t\tcontainError:         true,\n\t\t\texpectedErrorMessage: \"failed to find version using regex matcher\",\n\t\t},\n\t\t{\n\t\t\tname:                 \"empty output string\",\n\t\t\toutputStr:            \"\",\n\t\t\texpectedVersionStr:   \"\",\n\t\t\tcontainError:         true,\n\t\t\texpectedErrorMessage: \"failed to find version using regex matcher\",\n\t\t},\n\t}\n\n\tfor _, tc := range tests {\n\t\tversionStr, err := extractVersionFromShellCommandOutput(tc.outputStr)\n\t\tif tc.containError {\n\t\t\trequire.EqualError(t, err, tc.expectedErrorMessage, tc.name)\n\t\t} else {\n\t\t\trequire.NoError(t, err, tc.name)\n\t\t\trequire.Equal(t, tc.expectedVersionStr, versionStr, tc.name)\n\t\t}\n\t}\n}\n\nfunc TestCheckVersionConstraint(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname                 string\n\t\tactualVersionStr     string\n\t\tversionConstraint    string\n\t\tcontainError         bool\n\t\texpectedErrorMessage string\n\t}{\n\t\t{\n\t\t\tname:                 \"invalid actualVersionStr\",\n\t\t\tactualVersionStr:     \"\",\n\t\t\tversionConstraint:    \"1.2.3\",\n\t\t\tcontainError:         true,\n\t\t\texpectedErrorMessage: \"invalid version format found for actualVersionStr: \",\n\t\t},\n\t\t{\n\t\t\tname:                 \"invalid versionConstraint\",\n\t\t\tactualVersionStr:     \"1.2.3\",\n\t\t\tversionConstraint:    \"\",\n\t\t\tcontainError:         true,\n\t\t\texpectedErrorMessage: \"invalid version format found for versionConstraint: \",\n\t\t},\n\t\t{\n\t\t\tname:                 \"pass version constraint\",\n\t\t\tactualVersionStr:     \"1.2.3\",\n\t\t\tversionConstraint:    \"1.2.3\",\n\t\t\tcontainError:         false,\n\t\t\texpectedErrorMessage: \"\",\n\t\t},\n\t\t{\n\t\t\tname:                 \"fail version constraint\",\n\t\t\tactualVersionStr:     \"1.2.3\",\n\t\t\tversionConstraint:    \"1.2.4\",\n\t\t\tcontainError:         true,\n\t\t\texpectedErrorMessage: \"actual version {1.2.3} failed the version constraint {1.2.4}\",\n\t\t},\n\t\t{\n\t\t\tname:                 \"special syntax version constraint\",\n\t\t\tactualVersionStr:     \"1.0.5\",\n\t\t\tversionConstraint:    \"~> 1.0.4\",\n\t\t\tcontainError:         false,\n\t\t\texpectedErrorMessage: \"\",\n\t\t},\n\t\t{\n\t\t\tname:                 \"version constraint w/ operators\",\n\t\t\tactualVersionStr:     \"1.2.7\",\n\t\t\tversionConstraint:    \">= 1.2.0, < 2.0.0\",\n\t\t\tcontainError:         false,\n\t\t\texpectedErrorMessage: \"\"},\n\t}\n\n\tfor _, tc := range tests {\n\t\terr := checkVersionConstraint(tc.actualVersionStr, tc.versionConstraint)\n\t\tif tc.containError {\n\t\t\trequire.EqualError(t, err, tc.expectedErrorMessage, tc.name)\n\t\t} else {\n\t\t\trequire.NoError(t, err, tc.name)\n\t\t}\n\t}\n}\n\n// Note: with the current implementation of running shell command, it's not easy to\n// mock the output of running a shell command. So we assume a certain Binary is installed in the working\n// directory and it's greater than 0.0.1 version.\nfunc TestCheckVersionEndToEnd(t *testing.T) {\n\tt.Parallel()\n\ttests := []struct {\n\t\tname  string\n\t\tparam CheckVersionParams\n\t}{\n\t\t{name: \"Docker\", param: CheckVersionParams{\n\t\t\tBinary:            Docker,\n\t\t\tVersionConstraint: \">= 0.0.1\",\n\t\t\tWorkingDir:        \".\",\n\t\t}},\n\t\t{name: \"Terraform\", param: CheckVersionParams{\n\t\t\tBinaryPath:        \"\",\n\t\t\tBinary:            Terraform,\n\t\t\tVersionConstraint: \">= 0.0.1\",\n\t\t\tWorkingDir:        \".\",\n\t\t}},\n\t\t{name: \"Packer\", param: CheckVersionParams{\n\t\t\tBinaryPath:        \"/usr/local/bin/packer\",\n\t\t\tBinary:            Packer,\n\t\t\tVersionConstraint: \">= 0.0.1\",\n\t\t\tWorkingDir:        \".\",\n\t\t}},\n\t}\n\n\tfor _, tc := range tests {\n\t\terr := CheckVersionE(t, tc.param)\n\t\trequire.NoError(t, err, tc.name)\n\t}\n}\n"
  },
  {
    "path": "test/azure/terraform_azure_aci_example_test.go",
    "content": "//go:build azure\n// +build azure\n\n// NOTE: We use build tags to differentiate azure testing because we currently do not have azure access setup for\n// CircleCI.\n\npackage test\n\nimport (\n\t\"strings\"\n\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/azure\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/gruntwork-io/terratest/modules/terraform\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestTerraformAzureACIExample(t *testing.T) {\n\tt.Parallel()\n\n\tuniquePostfix := strings.ToLower(random.UniqueId())\n\n\t// website::tag::1:: Configure Terraform setting up a path to Terraform code.\n\tterraformOptions := &terraform.Options{\n\t\tTerraformDir: \"../../examples/azure/terraform-azure-aci-example\",\n\t\tVars: map[string]interface{}{\n\t\t\t\"postfix\": uniquePostfix,\n\t\t},\n\t}\n\n\t// website::tag::5:: At the end of the test, run `terraform destroy` to clean up any resources that were created\n\tdefer terraform.Destroy(t, terraformOptions)\n\n\t// website::tag::2:: Run `terraform init` and `terraform apply`. Fail the test if there are any errors.\n\tterraform.InitAndApply(t, terraformOptions)\n\n\t// website::tag::3:: Run `terraform output` to get the values of output variables\n\tresourceGroupName := terraform.Output(t, terraformOptions, \"resource_group_name\")\n\taciName := terraform.Output(t, terraformOptions, \"container_instance_name\")\n\tipAddress := terraform.Output(t, terraformOptions, \"ip_address\")\n\tfqdn := terraform.Output(t, terraformOptions, \"fqdn\")\n\n\t// website::tag::4:: Assert\n\tassert.True(t, azure.ContainerInstanceExists(t, aciName, resourceGroupName, \"\"))\n\n\tactualInstance := azure.GetContainerInstance(t, aciName, resourceGroupName, \"\")\n\n\tassert.Equal(t, ipAddress, *actualInstance.ContainerGroupProperties.IPAddress.IP)\n\tassert.Equal(t, fqdn, *actualInstance.ContainerGroupProperties.IPAddress.Fqdn)\n}\n"
  },
  {
    "path": "test/azure/terraform_azure_acr_example_test.go",
    "content": "//go:build azure\n// +build azure\n\n// NOTE: We use build tags to differentiate azure testing because we currently do not have azure access setup for\n// CircleCI.\n\npackage test\n\nimport (\n\t\"strings\"\n\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/azure\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/gruntwork-io/terratest/modules/terraform\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestTerraformAzureACRExample(t *testing.T) {\n\tt.Parallel()\n\n\tuniquePostfix := strings.ToLower(random.UniqueId())\n\tacrSKU := \"Premium\"\n\n\t// website::tag::1:: Configure Terraform setting up a path to Terraform code.\n\tterraformOptions := &terraform.Options{\n\t\tTerraformDir: \"../../examples/azure/terraform-azure-acr-example\",\n\t\tVars: map[string]interface{}{\n\t\t\t\"postfix\": uniquePostfix,\n\t\t\t\"sku\":     acrSKU,\n\t\t},\n\t}\n\n\t// website::tag::5:: At the end of the test, run `terraform destroy` to clean up any resources that were created\n\tdefer terraform.Destroy(t, terraformOptions)\n\n\t// website::tag::2:: Run `terraform init` and `terraform apply`. Fail the test if there are any errors.\n\tterraform.InitAndApply(t, terraformOptions)\n\n\t// website::tag::3:: Run `terraform output` to get the values of output variables\n\tresourceGroupName := terraform.Output(t, terraformOptions, \"resource_group_name\")\n\tacrName := terraform.Output(t, terraformOptions, \"container_registry_name\")\n\tloginServer := terraform.Output(t, terraformOptions, \"login_server\")\n\n\t// website::tag::4:: Assert\n\tassert.True(t, azure.ContainerRegistryExists(t, acrName, resourceGroupName, \"\"))\n\n\tactualACR := azure.GetContainerRegistry(t, acrName, resourceGroupName, \"\")\n\n\tassert.Equal(t, loginServer, *actualACR.LoginServer)\n\tassert.True(t, *actualACR.AdminUserEnabled)\n\tassert.Equal(t, acrSKU, string(actualACR.Sku.Name))\n}\n"
  },
  {
    "path": "test/azure/terraform_azure_actiongroup_example_test.go",
    "content": "//go:build azure\n// +build azure\n\n// NOTE: We use build tags to differentiate azure testing because we currently do not have azure access setup for\n// CircleCI.\n\npackage test\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/azure\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/gruntwork-io/terratest/modules/terraform\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestTerraformAzureActionGroupExample(t *testing.T) {\n\tt.Parallel()\n\t_random := strings.ToLower(random.UniqueId())\n\n\texpectedResourceGroupName := fmt.Sprintf(\"tmp-rg-%s\", _random)\n\texpectedAppName := fmt.Sprintf(\"tmp-asp-%s\", _random)\n\n\tterraformOptions := &terraform.Options{\n\t\tTerraformDir: \"../../examples/azure/terraform-azure-actiongroup-example\",\n\t\tVars: map[string]interface{}{\n\t\t\t\"resource_group_name\": expectedResourceGroupName,\n\t\t\t\"app_name\":            expectedAppName,\n\t\t\t\"location\":            \"westus2\",\n\t\t\t\"short_name\":          \"blah\",\n\t\t\t\"enable_email\":        true,\n\t\t\t\"email_name\":          \"emailTestName\",\n\t\t\t\"email_address\":       \"sample@test.com\",\n\t\t\t\"enable_webhook\":      true,\n\t\t\t\"webhook_name\":        \"webhookTestName\",\n\t\t\t\"webhook_service_uri\": \"http://example.com/alert\",\n\t\t},\n\t}\n\t// At the end of the test, run `terraform destroy` to clean up any resources that were created\n\tdefer terraform.Destroy(t, terraformOptions)\n\n\t// This will run `terraform init` and `terraform apply` and fail the test if there are any errors\n\tterraform.InitAndApply(t, terraformOptions)\n\n\tassert := assert.New(t)\n\tactionGroupId := terraform.Output(t, terraformOptions, \"action_group_id\")\n\tassert.NotNil(actionGroupId)\n\tassert.Contains(actionGroupId, expectedAppName)\n\n\tactionGroup := azure.GetActionGroupResource(t, expectedAppName, expectedResourceGroupName, \"\")\n\n\tassert.NotNil(actionGroup)\n\tassert.Equal(1, len(*actionGroup.EmailReceivers))\n\tassert.Equal(0, len(*actionGroup.SmsReceivers))\n\tassert.Equal(1, len(*actionGroup.WebhookReceivers))\n}\n"
  },
  {
    "path": "test/azure/terraform_azure_aks_example_test.go",
    "content": "//go:build azure\n// +build azure\n\n// NOTE: We use build tags to differentiate azure testing because we currently do not have azure access setup for\n// CircleCI.\n\npackage test\n\nimport (\n\t\"crypto/tls\"\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/gruntwork-io/terratest/modules/azure\"\n\thttp_helper \"github.com/gruntwork-io/terratest/modules/http-helper\"\n\t\"github.com/gruntwork-io/terratest/modules/k8s\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/gruntwork-io/terratest/modules/terraform\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestTerraformAzureAKSExample(t *testing.T) {\n\tt.Parallel()\n\t// MC_+ResourceGroupName_ClusterName_AzureRegion must be no greater than 80 chars.\n\t// https://docs.microsoft.com/en-us/azure/aks/troubleshooting#what-naming-restrictions-are-enforced-for-aks-resources-and-parameters\n\texpectedClusterName := fmt.Sprintf(\"terratest-aks-cluster-%s\", random.UniqueId())\n\texpectedResourceGroupName := fmt.Sprintf(\"terratest-aks-rg-%s\", random.UniqueId())\n\texpectedAagentCount := 3\n\n\tterraformOptions := &terraform.Options{\n\t\tTerraformDir: \"../../examples/azure/terraform-azure-aks-example\",\n\t\tVars: map[string]interface{}{\n\t\t\t\"cluster_name\":        expectedClusterName,\n\t\t\t\"resource_group_name\": expectedResourceGroupName,\n\t\t\t\"agent_count\":         expectedAagentCount,\n\t\t},\n\t}\n\t// At the end of the test, run `terraform destroy` to clean up any resources that were created\n\tdefer terraform.Destroy(t, terraformOptions)\n\n\t// This will run `terraform init` and `terraform apply` and fail the test if there are any errors\n\tterraform.InitAndApply(t, terraformOptions)\n\n\t// Look up the cluster node count\n\tcluster, err := azure.GetManagedClusterE(t, expectedResourceGroupName, expectedClusterName, \"\")\n\trequire.NoError(t, err)\n\tactualCount := *(*cluster.ManagedClusterProperties.AgentPoolProfiles)[0].Count\n\n\t// Test that the Node count matches the Terraform specification\n\tassert.Equal(t, int32(expectedAagentCount), actualCount)\n\n\t// Path to the Kubernetes resource config we will test\n\tkubeResourcePath, err := filepath.Abs(\"../../examples/azure/terraform-azure-aks-example/nginx-deployment.yml\")\n\trequire.NoError(t, err)\n\n\t// To ensure we can reuse the resource config on the same cluster to test different scenarios, we setup a unique\n\t// namespace for the resources for this test.\n\t// Note that namespaces must be lowercase.\n\tnamespaceName := strings.ToLower(random.UniqueId())\n\n\t// Setup the kubectl config and context. Here we choose to use the defaults, which is:\n\t// - HOME/.kube/config for the kubectl config file\n\t// - Current context of the kubectl config file\n\toptions := k8s.NewKubectlOptions(\"\", \"../../examples/azure/terraform-azure-aks-example/kubeconfig\", namespaceName)\n\n\tk8s.CreateNamespace(t, options, namespaceName)\n\t// ... and make sure to delete the namespace at the end of the test\n\tdefer k8s.DeleteNamespace(t, options, namespaceName)\n\n\t// At the end of the test, run `kubectl delete -f RESOURCE_CONFIG` to clean up any resources that were created.\n\tdefer k8s.KubectlDelete(t, options, kubeResourcePath)\n\n\t// This will run `kubectl apply -f RESOURCE_CONFIG` and fail the test if there are any errors\n\tk8s.KubectlApply(t, options, kubeResourcePath)\n\n\t// This will wait up to 10 seconds for the service to become available, to ensure that we can access it.\n\tk8s.WaitUntilServiceAvailable(t, options, \"nginx-service\", 10, 20*time.Second)\n\t// Now we verify that the service will successfully boot and start serving requests\n\tservice := k8s.GetService(t, options, \"nginx-service\")\n\tendpoint := k8s.GetServiceEndpoint(t, options, service, 80)\n\n\t// Setup a TLS configuration to submit with the helper, a blank struct is acceptable\n\ttlsConfig := tls.Config{}\n\n\t// Test the endpoint for up to 5 minutes. This will only fail if we timeout waiting for the service to return a 200\n\t// response.\n\thttp_helper.HttpGetWithRetryWithCustomValidation(\n\t\tt,\n\t\tfmt.Sprintf(\"http://%s\", endpoint),\n\t\t&tlsConfig,\n\t\t30,\n\t\t10*time.Second,\n\t\tfunc(statusCode int, body string) bool {\n\t\t\treturn statusCode == 200\n\t\t},\n\t)\n}\n"
  },
  {
    "path": "test/azure/terraform_azure_availabilityset_example_test.go",
    "content": "//go:build azure\n// +build azure\n\n// NOTE: We use build tags to differentiate azure testing because we currently do not have azure access setup for\n// CircleCI.\n\npackage test\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/azure\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/gruntwork-io/terratest/modules/terraform\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestTerraformAzureAvailabilitySetExample(t *testing.T) {\n\tt.Parallel()\n\n\t// subscriptionID is overridden by the environment variable \"ARM_SUBSCRIPTION_ID\"\n\tsubscriptionID := \"\"\n\tuniquePostfix := random.UniqueId()\n\texpectedAvsName := fmt.Sprintf(\"avs-%s\", uniquePostfix)\n\texpectedVMName := fmt.Sprintf(\"vm-%s\", uniquePostfix)\n\tvar expectedAvsFaultDomainCount int32 = 3\n\n\t// Configure Terraform setting up a path to Terraform code.\n\tterraformOptions := &terraform.Options{\n\t\t// Relative path to the Terraform dir\n\t\tTerraformDir: \"../../examples/azure/terraform-azure-availabilityset-example\",\n\n\t\t// Variables to pass to our Terraform code using -var options\n\t\tVars: map[string]interface{}{\n\t\t\t\"postfix\":                uniquePostfix,\n\t\t\t\"avs_fault_domain_count\": expectedAvsFaultDomainCount,\n\t\t\t// \"location\": \"East US\",\n\t\t},\n\t}\n\n\t// At the end of the test, run `terraform destroy` to clean up any resources that were created\n\tdefer terraform.Destroy(t, terraformOptions)\n\n\t// Run `terraform init` and `terraform apply`. Fail the test if there are any errors.\n\tterraform.InitAndApply(t, terraformOptions)\n\n\t// Run `terraform output` to get the values of output variables\n\tresourceGroupName := terraform.Output(t, terraformOptions, \"resource_group_name\")\n\n\t// Check the Availability Set Exists\n\tactualAvsExists := azure.AvailabilitySetExists(t, expectedAvsName, resourceGroupName, subscriptionID)\n\tassert.True(t, actualAvsExists)\n\n\t// Check the Availability Set Fault Domain Count\n\tactualAvsFaultDomainCount := azure.GetAvailabilitySetFaultDomainCount(t, expectedAvsName, resourceGroupName, subscriptionID)\n\tassert.Equal(t, expectedAvsFaultDomainCount, actualAvsFaultDomainCount)\n\n\t// Check the Availability Set for a VM\n\tactualVMPresent := azure.CheckAvailabilitySetContainsVM(t, expectedVMName, expectedAvsName, resourceGroupName, subscriptionID)\n\tassert.True(t, actualVMPresent)\n}\n"
  },
  {
    "path": "test/azure/terraform_azure_container_apps_example_test.go",
    "content": "//go:build azure\n// +build azure\n\n// NOTE: We use build tags to differentiate azure testing because we currently do not have azure access setup for\n// CircleCI.\n\npackage test\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/azure\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/gruntwork-io/terratest/modules/terraform\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestTerraformAzureContainerAppExample(t *testing.T) {\n\tt.Parallel()\n\n\tsubscriptionID := \"\"\n\tuniquePostfix := strings.ToLower(random.UniqueId())\n\n\tterraformOptions := &terraform.Options{\n\t\tTerraformBinary: \"\",\n\t\t// The path to where our Terraform code is located\n\t\tTerraformDir: \"../../examples/azure/terraform-azure-container-apps-example\",\n\t\tVars: map[string]interface{}{\n\t\t\t\"postfix\": uniquePostfix,\n\t\t},\n\t}\n\n\tdefer terraform.Destroy(t, terraformOptions)\n\n\tterraform.InitAndApply(t, terraformOptions)\n\n\tresourceGroupName := terraform.Output(t, terraformOptions, \"resource_group_name\")\n\tenvName := terraform.Output(t, terraformOptions, \"container_app_env_name\")\n\tcontainerAppName := terraform.Output(t, terraformOptions, \"container_app_name\")\n\tcontainerAppJobName := terraform.Output(t, terraformOptions, \"container_app_job_name\")\n\n\t// NOTE: the value of subscriptionID can be left blank, it will be replaced by the value\n\t//       of the environment variable ARM_SUBSCRIPTION_ID\n\n\tenvExsists := azure.ManagedEnvironmentExists(t, envName, resourceGroupName, subscriptionID)\n\tassert.True(t, envExsists)\n\n\tactualEnv := azure.GetManagedEnvironment(t, envName, resourceGroupName, subscriptionID)\n\tassert.Equal(t, envName, *actualEnv.Name)\n\n\tcontainerAppExists := azure.ContainerAppExists(t, containerAppName, resourceGroupName, subscriptionID)\n\tassert.True(t, containerAppExists)\n\n\tactualContainerApp := azure.GetContainerApp(t, containerAppName, resourceGroupName, subscriptionID)\n\tassert.Equal(t, containerAppName, *actualContainerApp.Name)\n\n\tcontainerAppJobExists := azure.ContainerAppJobExists(t, containerAppJobName, resourceGroupName, subscriptionID)\n\tassert.True(t, containerAppJobExists)\n\n\tactualContainerAppJob := azure.GetContainerAppJob(t, containerAppJobName, resourceGroupName, subscriptionID)\n\tassert.Equal(t, containerAppJobName, *actualContainerAppJob.Name)\n}\n"
  },
  {
    "path": "test/azure/terraform_azure_cosmosdb_example_test.go",
    "content": "//go:build azure\n// +build azure\n\n// NOTE: We use build tags to differentiate azure testing because we currently do not have azure access setup for\n// CircleCI.\n\npackage test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/Azure/azure-sdk-for-go/profiles/latest/cosmos-db/mgmt/documentdb\"\n\t\"github.com/gruntwork-io/terratest/modules/azure\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/gruntwork-io/terratest/modules/terraform\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestTerraformAzureCosmosDBExample(t *testing.T) {\n\tt.Parallel()\n\n\tsubscriptionID := \"\"\n\tuniquePostfix := random.Random(10000, 99999)\n\tthroughput := 400\n\n\t// website::tag::1:: Configure Terraform setting up a path to Terraform code.\n\tterraformOptions := &terraform.Options{\n\t\t// The path to where our Terraform code is located\n\t\tTerraformDir: \"../../examples/azure/terraform-azure-cosmosdb-example\",\n\t\tVars: map[string]interface{}{\n\t\t\t\"postfix\":    uniquePostfix,\n\t\t\t\"throughput\": throughput,\n\t\t},\n\t}\n\n\t// website::tag::4:: At the end of the test, run `terraform destroy` to clean up any resources that were created\n\tdefer terraform.Destroy(t, terraformOptions)\n\n\t// website::tag::2:: Run `terraform init` and `terraform apply`. Fail the test if there are any errors.\n\tterraform.InitAndApply(t, terraformOptions)\n\n\t// website::tag::3:: Run `terraform output` to get the values of output variables\n\tresourceGroupName := terraform.Output(t, terraformOptions, \"resource_group_name\")\n\taccountName := terraform.Output(t, terraformOptions, \"account_name\")\n\n\t// website::tag::4:: Get CosmosDB details and assert them against the terraform output\n\t// NOTE: the value of subscriptionID can be left blank, it will be replaced by the value\n\t//       of the environment variable ARM_SUBSCRIPTION_ID\n\n\t// Database Account properties\n\tactualCosmosDBAccount := azure.GetCosmosDBAccount(t, subscriptionID, resourceGroupName, accountName)\n\tassert.Equal(t, accountName, *actualCosmosDBAccount.Name)\n\tassert.Equal(t, documentdb.GlobalDocumentDB, actualCosmosDBAccount.Kind)\n\tassert.Equal(t, documentdb.Session, actualCosmosDBAccount.DatabaseAccountGetProperties.ConsistencyPolicy.DefaultConsistencyLevel)\n\n\t// SQL Database properties\n\tcosmosSQLDB := azure.GetCosmosDBSQLDatabase(t, subscriptionID, resourceGroupName, accountName, \"testdb\")\n\tassert.Equal(t, \"testdb\", *cosmosSQLDB.Name)\n\n\t// SQL Database throughput\n\tcosmosSQLDBThroughput := azure.GetCosmosDBSQLDatabaseThroughput(t, subscriptionID, resourceGroupName, accountName, \"testdb\")\n\tassert.Equal(t, int32(throughput), *cosmosSQLDBThroughput.ThroughputSettingsGetProperties.Resource.Throughput)\n\n\t// SQL Container properties\n\tcosmosSQLContainer1 := azure.GetCosmosDBSQLContainer(t, subscriptionID, resourceGroupName, accountName, \"testdb\", \"test-container-1\")\n\tcosmosSQLContainer2 := azure.GetCosmosDBSQLContainer(t, subscriptionID, resourceGroupName, accountName, \"testdb\", \"test-container-2\")\n\tcosmosSQLContainer3 := azure.GetCosmosDBSQLContainer(t, subscriptionID, resourceGroupName, accountName, \"testdb\", \"test-container-3\")\n\tassert.Equal(t, \"test-container-1\", *cosmosSQLContainer1.Name)\n\tassert.Equal(t, \"/key1\", (*cosmosSQLContainer1.SQLContainerGetProperties.Resource.PartitionKey.Paths)[0])\n\tassert.Equal(t, \"test-container-2\", *cosmosSQLContainer2.Name)\n\tassert.Equal(t, \"/key2\", (*cosmosSQLContainer2.SQLContainerGetProperties.Resource.PartitionKey.Paths)[0])\n\tassert.Equal(t, \"test-container-3\", *cosmosSQLContainer3.Name)\n\tassert.Equal(t, \"/key3\", (*cosmosSQLContainer3.SQLContainerGetProperties.Resource.PartitionKey.Paths)[0])\n\n\t// SQL Container throughput\n\tcosmosSQLContainer1Throughput := azure.GetCosmosDBSQLContainerThroughput(t, subscriptionID, resourceGroupName, accountName, \"testdb\", \"test-container-1\")\n\tassert.Equal(t, int32(400), *cosmosSQLContainer1Throughput.ThroughputSettingsGetProperties.Resource.Throughput)\n}\n"
  },
  {
    "path": "test/azure/terraform_azure_datafactory_example_test.go",
    "content": "//go:build azure\n// +build azure\n\npackage test\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/azure\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/gruntwork-io/terratest/modules/terraform\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestTerraformAzureDataFactoryExample(t *testing.T) {\n\tt.Parallel()\n\n\tuniquePostfix := strings.ToLower(random.UniqueId())\n\texpectedDataFactoryProvisioningState := \"Succeeded\"\n\texpectedLocation := \"eastus\"\n\n\t//Configure Terraform setting up a path to Terraform code.\n\tterraformOptions := &terraform.Options{\n\t\t// The path to where our Terraform code is located\n\t\tTerraformDir: \"../../examples/azure/terraform-azure-datafactory-example\",\n\t\tVars: map[string]interface{}{\n\t\t\t\"postfix\":  uniquePostfix,\n\t\t\t\"location\": expectedLocation,\n\t\t},\n\t}\n\n\t// At the end of the test, run `terraform destroy` to clean up any resources that were created\n\tdefer terraform.Destroy(t, terraformOptions)\n\n\t//Run `terraform init` and `terraform apply`. Fail the test if there are any errors.\n\tterraform.InitAndApply(t, terraformOptions)\n\n\t// Run `terraform output` to get the values of output variables\n\texpectedResourceGroupName := terraform.Output(t, terraformOptions, \"resource_group_name\")\n\texpectedDataFactoryName := terraform.Output(t, terraformOptions, \"datafactory_name\")\n\n\t// check for if data factory exists\n\tactualDataFactoryExits := azure.DataFactoryExists(t, expectedDataFactoryName, expectedResourceGroupName, \"\")\n\tassert.True(t, actualDataFactoryExits)\n\n\t//Get data factory details and assert them against the terraform output\n\tactualDataFactory := azure.GetDataFactory(t, expectedResourceGroupName, expectedDataFactoryName, \"\")\n\tassert.Equal(t, expectedDataFactoryName, *actualDataFactory.Name)\n\tassert.Equal(t, expectedDataFactoryProvisioningState, *actualDataFactory.Properties.ProvisioningState)\n\n}\n"
  },
  {
    "path": "test/azure/terraform_azure_disk_example_test.go",
    "content": "//go:build azure\n// +build azure\n\n// NOTE: We use build tags to differentiate azure testing because we currently do not have azure access setup for\n// CircleCI.\n\npackage test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2019-07-01/compute\"\n\n\t\"github.com/gruntwork-io/terratest/modules/azure\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/gruntwork-io/terratest/modules/terraform\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestTerraformAzureDiskExample(t *testing.T) {\n\tt.Parallel()\n\n\t// Subscription ID, leave blank if available as an Environment Var\n\tsubID := \"\"\n\tuniquePostfix := random.UniqueId()\n\n\t// Configure Terraform setting up a path to Terraform code.\n\tterraformOptions := &terraform.Options{\n\t\t// The path to where our Terraform code is located\n\t\tTerraformDir: \"../../examples/azure/terraform-azure-disk-example\",\n\n\t\t// Variables to pass to our Terraform code using -var options\n\t\tVars: map[string]interface{}{\n\t\t\t\"postfix\": uniquePostfix,\n\t\t},\n\t}\n\n\t// At the end of the test, run `terraform destroy` to clean up any resources that were created\n\tdefer terraform.Destroy(t, terraformOptions)\n\n\t// Run `terraform init` and `terraform apply`. Fail the test if there are any errors.\n\tterraform.InitAndApply(t, terraformOptions)\n\n\t// Run `terraform output` to get the values of output variables\n\tresourceGroupName := terraform.Output(t, terraformOptions, \"resource_group_name\")\n\texpectedDiskName := terraform.Output(t, terraformOptions, \"disk_name\")\n\texpectedDiskType := terraform.Output(t, terraformOptions, \"disk_type\")\n\n\t// Check the Disk Type\n\tactualDisk := azure.GetDisk(t, expectedDiskName, resourceGroupName, subID)\n\tassert.Equal(t, compute.DiskStorageAccountTypes(expectedDiskType), actualDisk.Sku.Name)\n}\n"
  },
  {
    "path": "test/azure/terraform_azure_example_test.go",
    "content": "//go:build azure || (azureslim && compute)\n// +build azure azureslim,compute\n\n// NOTE: We use build tags to differentiate azure testing because we currently do not have azure access setup for\n// CircleCI.\n\npackage test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2019-07-01/compute\"\n\t\"github.com/gruntwork-io/terratest/modules/azure\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/gruntwork-io/terratest/modules/terraform\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestTerraformAzureExample(t *testing.T) {\n\tt.Parallel()\n\n\tuniquePostfix := random.UniqueId()\n\n\t// website::tag::1:: Configure Terraform setting up a path to Terraform code.\n\tterraformOptions := &terraform.Options{\n\t\t// The path to where our Terraform code is located\n\t\tTerraformDir: \"../../examples/azure/terraform-azure-example\",\n\t\tVars: map[string]interface{}{\n\t\t\t\"postfix\": uniquePostfix,\n\t\t},\n\t}\n\n\t// website::tag::4:: At the end of the test, run `terraform destroy` to clean up any resources that were created\n\tdefer terraform.Destroy(t, terraformOptions)\n\n\t// website::tag::2:: Run `terraform init` and `terraform apply`. Fail the test if there are any errors.\n\tterraform.InitAndApply(t, terraformOptions)\n\n\t// website::tag::3:: Run `terraform output` to get the values of output variables\n\tvmName := terraform.Output(t, terraformOptions, \"vm_name\")\n\tresourceGroupName := terraform.Output(t, terraformOptions, \"resource_group_name\")\n\n\t// website::tag::4:: Look up the size of the given Virtual Machine and ensure it matches the output.\n\tactualVMSize := azure.GetSizeOfVirtualMachine(t, vmName, resourceGroupName, \"\")\n\texpectedVMSize := compute.VirtualMachineSizeTypes(\"Standard_B1s\")\n\tassert.Equal(t, expectedVMSize, actualVMSize)\n}\n"
  },
  {
    "path": "test/azure/terraform_azure_frontdoor_example_test.go",
    "content": "//go:build azure\n// +build azure\n\n// NOTE: We use build tags to differentiate azure testing because we currently do not have azure access setup for\n// CircleCI.\n\npackage test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/azure\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/gruntwork-io/terratest/modules/terraform\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestTerraformAzureFrontDoorExample(t *testing.T) {\n\tt.Parallel()\n\n\tsubscriptionID := \"\"\n\tuniquePostfix := random.UniqueId()\n\n\t// website::tag::1:: Configure Terraform setting up a path to Terraform code.\n\tterraformOptions := &terraform.Options{\n\t\t// The path to where our Terraform code is located\n\t\tTerraformDir: \"../../examples/azure/terraform-azure-frontdoor-example\",\n\t\tVars: map[string]interface{}{\n\t\t\t\"postfix\": uniquePostfix,\n\t\t},\n\t}\n\n\t// website::tag::4:: At the end of the test, run `terraform destroy` to clean up any resources that were created\n\tdefer terraform.Destroy(t, terraformOptions)\n\n\t// website::tag::2:: Run `terraform init` and `terraform apply`. Fail the test if there are any errors.\n\tterraform.InitAndApply(t, terraformOptions)\n\n\t// website::tag::3:: Run `terraform output` to get the values of output variables\n\tresourceGroupName := terraform.Output(t, terraformOptions, \"resource_group_name\")\n\tfrontDoorName := terraform.Output(t, terraformOptions, \"front_door_name\")\n\tfrontDoorUrl := terraform.Output(t, terraformOptions, \"front_door_url\")\n\tfrontendEndpointName := terraform.Output(t, terraformOptions, \"front_door_endpoint_name\")\n\n\t// website::tag::4:: Get FrontDoor details and assert them against the terraform output\n\t// NOTE: the value of subscriptionID can be left blank, it will be replaced by the value\n\t//       of the environment variable ARM_SUBSCRIPTION_ID\n\n\tfrontDoorExists := azure.FrontDoorExists(t, frontDoorName, resourceGroupName, subscriptionID)\n\tassert.True(t, frontDoorExists)\n\n\tactualFrontDoorInstance := azure.GetFrontDoor(t, frontDoorName, resourceGroupName, subscriptionID)\n\tassert.Equal(t, frontDoorName, *actualFrontDoorInstance.Name)\n\n\tendpointExists := azure.FrontDoorFrontendEndpointExists(t, frontendEndpointName, frontDoorName, resourceGroupName, subscriptionID)\n\tassert.True(t, endpointExists)\n\n\tactualFrontDoorEndpoint := azure.GetFrontDoorFrontendEndpoint(t, frontendEndpointName, frontDoorName, resourceGroupName, subscriptionID)\n\tendpointProperties := *actualFrontDoorEndpoint.FrontendEndpointProperties\n\tassert.Equal(t, frontDoorUrl, *endpointProperties.HostName)\n}\n"
  },
  {
    "path": "test/azure/terraform_azure_functionapp_example_test.go",
    "content": "//go:build azure\n// +build azure\n\n// NOTE: We use build tags to differentiate azure testing because we currently do not have azure access setup for\n// CircleCI.\n\npackage test\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/azure\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/gruntwork-io/terratest/modules/terraform\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestTerraformAzureFunctionAppExample(t *testing.T) {\n\tt.Parallel()\n\n\t//_random := strings.ToLower(random.UniqueId())\n\tuniquePostfix := strings.ToLower(random.UniqueId())\n\n\t// website::tag::1:: Configure Terraform setting up a path to Terraform code.\n\tterraformOptions := &terraform.Options{\n\t\tTerraformDir: \"../../examples/azure/terraform-azure-functionapp-example\",\n\t\tVars: map[string]interface{}{\n\t\t\t\"postfix\": uniquePostfix,\n\t\t},\n\t}\n\t// website::tag::5:: At the end of the test, run `terraform destroy` to clean up any resources that were created\n\tdefer terraform.Destroy(t, terraformOptions)\n\n\t// website::tag::2:: Run `terraform init` and `terraform apply`. Fail the test if there are any errors.\n\tterraform.InitAndApply(t, terraformOptions)\n\n\t// website::tag::3:: Run `terraform output` to get the values of output variables\n\tresourceGroupName := terraform.Output(t, terraformOptions, \"resource_group_name\")\n\tappName := terraform.Output(t, terraformOptions, \"function_app_name\")\n\n\tappId := terraform.Output(t, terraformOptions, \"function_app_id\")\n\tappDefaultHostName := terraform.Output(t, terraformOptions, \"default_hostname\")\n\tappKind := terraform.Output(t, terraformOptions, \"function_app_kind\")\n\n\t// website::tag::4:: Assert\n\tassert.True(t, azure.AppExists(t, appName, resourceGroupName, \"\"))\n\tsite := azure.GetAppService(t, appName, resourceGroupName, \"\")\n\n\tassert.Equal(t, appId, *site.ID)\n\tassert.Equal(t, appDefaultHostName, *site.DefaultHostName)\n\tassert.Equal(t, appKind, *site.Kind)\n\n\tassert.NotEmpty(t, *site.OutboundIPAddresses)\n\tassert.Equal(t, \"Running\", *site.State)\n}\n"
  },
  {
    "path": "test/azure/terraform_azure_keyvault_example_test.go",
    "content": "//go:build azure\n// +build azure\n\n// NOTE: We use build tags to differentiate azure testing because we currently do not have azure access setup for\n// CircleCI.\n\npackage test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/azure\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/gruntwork-io/terratest/modules/terraform\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestTerraformAzureKeyVaultExample(t *testing.T) {\n\tt.Parallel()\n\n\tuniquePostfix := random.UniqueId()\n\n\t// website::tag::1:: Configure Terraform setting up a path to Terraform code.\n\tterraformOptions := &terraform.Options{\n\t\t// The path to where our Terraform code is located\n\t\tTerraformDir: \"../../examples/azure/terraform-azure-keyvault-example\",\n\t\tVars: map[string]interface{}{\n\t\t\t\"postfix\": uniquePostfix,\n\t\t},\n\t}\n\n\t// website::tag::6:: At the end of the test, run `terraform destroy` to clean up any resources that were created\n\tdefer terraform.Destroy(t, terraformOptions)\n\n\t// website::tag::2:: Run `terraform init` and `terraform apply`. Fail the test if there are any errors.\n\tterraform.InitAndApply(t, terraformOptions)\n\n\t// website::tag::3:: Run `terraform output` to get the values of output variables\n\tresourceGroupName := terraform.Output(t, terraformOptions, \"resource_group_name\")\n\tkeyVaultName := terraform.Output(t, terraformOptions, \"key_vault_name\")\n\texpectedSecretName := terraform.Output(t, terraformOptions, \"secret_name\")\n\texpectedKeyName := terraform.Output(t, terraformOptions, \"key_name\")\n\texpectedCertificateName := terraform.Output(t, terraformOptions, \"certificate_name\")\n\n\t// website::tag::4:: Determine whether the keyvault exists\n\tkeyVault := azure.GetKeyVault(t, resourceGroupName, keyVaultName, \"\")\n\tassert.Equal(t, keyVaultName, *keyVault.Name)\n\n\t// website::tag::5:: Determine whether the secret, key, and certificate exists\n\tsecretExists := azure.KeyVaultSecretExists(t, keyVaultName, expectedSecretName)\n\tassert.True(t, secretExists, \"kv-secret does not exist\")\n\n\tkeyExists := azure.KeyVaultKeyExists(t, keyVaultName, expectedKeyName)\n\tassert.True(t, keyExists, \"kv-key does not exist\")\n\n\tcertificateExists := azure.KeyVaultCertificateExists(t, keyVaultName, expectedCertificateName)\n\tassert.True(t, certificateExists, \"kv-cert does not exist\")\n}\n"
  },
  {
    "path": "test/azure/terraform_azure_loadbalancer_example_test.go",
    "content": "//go:build azure\n// +build azure\n\n// NOTE: We use build tags to differentiate azure testing because we currently do not have azure access setup for\n// CircleCI.\n\npackage test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/azure\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/gruntwork-io/terratest/modules/terraform\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestTerraformAzureLoadBalancerExample(t *testing.T) {\n\tt.Parallel()\n\n\t// subscriptionID is overridden by the environment variable \"ARM_SUBSCRIPTION_ID\"\n\tsubscriptionID := \"\"\n\tuniquePostfix := random.UniqueId()\n\tprivateIPForLB02 := \"10.200.2.10\"\n\n\t// Configure Terraform setting up a path to Terraform code.\n\tterraformOptions := &terraform.Options{\n\t\t// The path to where our Terraform code is located\n\t\tTerraformDir: \"../../examples/azure/terraform-azure-loadbalancer-example\",\n\n\t\t// Variables to pass to our Terraform code using -var options\n\t\tVars: map[string]interface{}{\n\t\t\t\"postfix\":       uniquePostfix,\n\t\t\t\"lb_private_ip\": privateIPForLB02,\n\t\t\t// \"location\": \"East US\",\n\t\t},\n\t}\n\n\t// At the end of the test, run `terraform destroy` to clean up any resources that were created.\n\tdefer terraform.Destroy(t, terraformOptions)\n\n\t// Run `terraform init` and `terraform apply`. Fail the test if there are any errors.\n\tterraform.InitAndApply(t, terraformOptions)\n\n\t// Run `terraform output` to get the values of output variables\n\tresourceGroupName := terraform.Output(t, terraformOptions, \"resource_group_name\")\n\texpectedLBPublicName := terraform.Output(t, terraformOptions, \"lb_public_name\")\n\texpectedLBPrivateName := terraform.Output(t, terraformOptions, \"lb_private_name\")\n\texpectedLBNoFEConfigName := terraform.Output(t, terraformOptions, \"lb_default_name\")\n\texpectedLBPublicFeConfigName := terraform.Output(t, terraformOptions, \"lb_public_fe_config_name\")\n\texpectedLBPrivateFeConfigName := terraform.Output(t, terraformOptions, \"lb_private_fe_config_static_name\")\n\texpectedLBPrivateIP := terraform.Output(t, terraformOptions, \"lb_private_ip_static\")\n\n\tactualLBDoesNotExist := azure.LoadBalancerExists(t, \"negative-test\", resourceGroupName, subscriptionID)\n\tassert.False(t, actualLBDoesNotExist)\n\n\tt.Run(\"LoadBalancer_Public\", func(t *testing.T) {\n\t\t// Check Public Load Balancer exists.\n\t\tactualLBPublicExists := azure.LoadBalancerExists(t, expectedLBPublicName, resourceGroupName, subscriptionID)\n\t\tassert.True(t, actualLBPublicExists)\n\n\t\t// Check Frontend Configuration for Load Balancer.\n\t\tactualLBPublicFeConfigNames := azure.GetLoadBalancerFrontendIPConfigNames(t, expectedLBPublicName, resourceGroupName, subscriptionID)\n\t\tassert.Contains(t, actualLBPublicFeConfigNames, expectedLBPublicFeConfigName)\n\n\t\t// Check Frontend Configuration Public Address and Public IP assignment\n\t\tactualLBPublicIPAddress, actualLBPublicIPType := azure.GetIPOfLoadBalancerFrontendIPConfig(t, expectedLBPublicFeConfigName, expectedLBPublicName, resourceGroupName, subscriptionID)\n\t\tassert.NotEmpty(t, actualLBPublicIPAddress)\n\t\tassert.Equal(t, azure.PublicIP, actualLBPublicIPType)\n\t})\n\n\tt.Run(\"LoadBalancer_Private\", func(t *testing.T) {\n\t\t// Check Private Load Balancer exists.\n\t\tactualLBPrivateExists := azure.LoadBalancerExists(t, expectedLBPrivateName, resourceGroupName, subscriptionID)\n\t\tassert.True(t, actualLBPrivateExists)\n\n\t\t// Check Frontend Configuration for Load Balancer.\n\t\tactualLBPrivateFeConfigNames := azure.GetLoadBalancerFrontendIPConfigNames(t, expectedLBPrivateName, resourceGroupName, subscriptionID)\n\t\tassert.Equal(t, 2, len(actualLBPrivateFeConfigNames))\n\t\tassert.Contains(t, actualLBPrivateFeConfigNames, expectedLBPrivateFeConfigName)\n\n\t\t// Check Frontend Configuration Private IP Type and Address.\n\t\tactualLBPrivateIPAddress, actualLBPrivateIPType := azure.GetIPOfLoadBalancerFrontendIPConfig(t, expectedLBPrivateFeConfigName, expectedLBPrivateName, resourceGroupName, subscriptionID)\n\t\tassert.NotEmpty(t, actualLBPrivateIPAddress)\n\t\tassert.Equal(t, expectedLBPrivateIP, actualLBPrivateIPAddress)\n\t\tassert.Equal(t, azure.PrivateIP, actualLBPrivateIPType)\n\t})\n\n\tt.Run(\"LoadBalancer_Default\", func(t *testing.T) {\n\t\t// Check No Frontend Config Load Balancer exists.\n\t\tactualLBNoFEConfigExists := azure.LoadBalancerExists(t, expectedLBNoFEConfigName, resourceGroupName, subscriptionID)\n\t\tassert.True(t, actualLBNoFEConfigExists)\n\n\t\t// Check for No Frontend Configuration for Load Balancer.\n\t\tactualLBNoFEConfigFeConfigNames := azure.GetLoadBalancerFrontendIPConfigNames(t, expectedLBNoFEConfigName, resourceGroupName, subscriptionID)\n\t\tassert.Equal(t, 0, len(actualLBNoFEConfigFeConfigNames))\n\t})\n}\n"
  },
  {
    "path": "test/azure/terraform_azure_loganalytics_example_test.go",
    "content": "//go:build azure\n// +build azure\n\n// NOTE: We use build tags to differentiate azure testing because we currently do not have azure access setup for\n// CircleCI.\n\npackage test\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/azure\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/gruntwork-io/terratest/modules/terraform\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestTerraformAzureLogAnalyticsExample(t *testing.T) {\n\tt.Parallel()\n\n\t// subscriptionID is overridden by the environment variable \"ARM_SUBSCRIPTION_ID\"\n\tsubscriptionID := \"\"\n\tuniquePostfix := random.UniqueId()\n\n\t// website::tag::1:: Configure Terraform setting up a path to Terraform code.\n\tterraformOptions := &terraform.Options{\n\t\t// The path to where our Terraform code is located\n\t\tTerraformDir: \"../../examples/azure/terraform-azure-loganalytics-example\",\n\t\t// Variables to pass to our Terraform code using -var options\n\t\tVars: map[string]interface{}{\n\t\t\t\"postfix\": uniquePostfix,\n\t\t},\n\t}\n\n\t// website::tag::4:: At the end of the test, run `terraform destroy` to clean up any resources that were created\n\tdefer terraform.Destroy(t, terraformOptions)\n\n\t// website::tag::2:: Run `terraform init` and `terraform apply`. Fail the test if there are any errors.\n\tterraform.InitAndApply(t, terraformOptions)\n\n\t// website::tag::3:: Run `terraform output` to get the values of output variables\n\tresourceGroupName := terraform.Output(t, terraformOptions, \"resource_group_name\")\n\tworkspaceName := terraform.Output(t, terraformOptions, \"loganalytics_workspace_name\")\n\tsku := terraform.Output(t, terraformOptions, \"loganalytics_workspace_sku\")\n\tretentionPeriodString := terraform.Output(t, terraformOptions, \"loganalytics_workspace_retention\")\n\n\t// website::tag::4:: Verify the Log Analytics properties and ensure it matches the output.\n\tworkspaceExists := azure.LogAnalyticsWorkspaceExists(t, workspaceName, resourceGroupName, subscriptionID)\n\tassert.True(t, workspaceExists, \"log analytics workspace not found.\")\n\n\tactualWorkspace := azure.GetLogAnalyticsWorkspace(t, workspaceName, resourceGroupName, subscriptionID)\n\n\tactualSku := string(actualWorkspace.Sku.Name)\n\tassert.Equal(t, strings.ToLower(sku), strings.ToLower(actualSku), \"log analytics sku mismatch\")\n\n\tactualRetentionPeriod := *actualWorkspace.RetentionInDays\n\texpectedPeriod, _ := strconv.ParseInt(retentionPeriodString, 10, 32)\n\tassert.Equal(t, int32(expectedPeriod), actualRetentionPeriod, \"log analytics retention period mismatch\")\n}\n"
  },
  {
    "path": "test/azure/terraform_azure_monitor_example_test.go",
    "content": "//go:build azure\n// +build azure\n\n// NOTE: We use build tags to differentiate azure testing because we currently do not have azure access setup for\n// CircleCI.\n\npackage test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/azure\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/gruntwork-io/terratest/modules/terraform\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestTerraformAzureMonitorExample(t *testing.T) {\n\tt.Parallel()\n\n\t// subscriptionID is overridden by the environment variable \"ARM_SUBSCRIPTION_ID\"\n\tsubscriptionID := \"\"\n\tuniquePostfix := random.UniqueId()\n\n\t// website::tag::1:: Configure Terraform setting up a path to Terraform code.\n\tterraformOptions := &terraform.Options{\n\t\t// The path to where our Terraform code is located\n\t\tTerraformDir: \"../../examples/azure/terraform-azure-monitor-example\",\n\t\tVars: map[string]interface{}{\n\t\t\t\"postfix\": uniquePostfix,\n\t\t},\n\t}\n\n\t// website::tag::4:: At the end of the test, run `terraform destroy` to clean up any resources that were created\n\tdefer terraform.Destroy(t, terraformOptions)\n\n\t// website::tag::2:: Run `terraform init` and `terraform apply`. Fail the test if there are any errors.\n\tterraform.InitAndApply(t, terraformOptions)\n\n\texpectedDiagnosticSettingName := terraform.Output(t, terraformOptions, \"diagnostic_setting_name\")\n\tkeyvaultID := terraform.Output(t, terraformOptions, \"keyvault_id\")\n\n\tdiagnosticSettingsResourceExists := azure.DiagnosticSettingsResourceExists(t, expectedDiagnosticSettingName, keyvaultID, subscriptionID)\n\n\tassert.Equal(t, diagnosticSettingsResourceExists, true, \"Diagnostic settings should exist\")\n}\n"
  },
  {
    "path": "test/azure/terraform_azure_mysqldb_example_test.go",
    "content": "//go:build azure\n// +build azure\n\n// NOTE: We use build tags to differentiate azure testing because we currently do not have azure access setup for\n// CircleCI.\n\npackage test\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/mysql/armmysql\"\n\t\"github.com/gruntwork-io/terratest/modules/azure\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/gruntwork-io/terratest/modules/terraform\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestTerraformAzureMySQLDBExample(t *testing.T) {\n\tt.Parallel()\n\n\tuniquePostfix := strings.ToLower(random.UniqueId())\n\texpectedServerSkuName := \"GP_Gen5_2\"\n\texpectedServerStoragemMb := \"5120\"\n\texpectedDatabaseCharSet := \"utf8\"\n\texpectedDatabaseCollation := \"utf8_unicode_ci\"\n\n\t// website::tag::1:: Configure Terraform setting up a path to Terraform code.\n\tterraformOptions := &terraform.Options{\n\t\t// The path to where our Terraform code is located\n\t\tTerraformDir: \"../../examples/azure/terraform-azure-mysqldb-example\",\n\t\tVars: map[string]interface{}{\n\t\t\t\"postfix\":                uniquePostfix,\n\t\t\t\"mysqlserver_sku_name\":   expectedServerSkuName,\n\t\t\t\"mysqlserver_storage_mb\": expectedServerStoragemMb,\n\t\t\t\"mysqldb_charset\":        expectedDatabaseCharSet,\n\t\t},\n\t}\n\n\t// website::tag::4:: At the end of the test, run `terraform destroy` to clean up any resources that were created\n\tdefer terraform.Destroy(t, terraformOptions)\n\n\t// website::tag::2:: Run `terraform init` and `terraform apply`. Fail the test if there are any errors.\n\tterraform.InitAndApply(t, terraformOptions)\n\n\t// website::tag::3:: Run `terraform output` to get the values of output variables\n\texpectedResourceGroupName := terraform.Output(t, terraformOptions, \"resource_group_name\")\n\texpectedMYSQLServerName := terraform.Output(t, terraformOptions, \"mysql_server_name\")\n\n\texpectedMYSQLDBName := terraform.Output(t, terraformOptions, \"mysql_database_name\")\n\n\t// website::tag::4:: Get mySQL server details and assert them against the terraform output\n\tactualMYSQLServer := azure.GetMYSQLServer(t, expectedResourceGroupName, expectedMYSQLServerName, \"\")\n\n\tassert.Equal(t, expectedServerSkuName, *actualMYSQLServer.SKU.Name)\n\tassert.Equal(t, expectedServerStoragemMb, fmt.Sprint(*actualMYSQLServer.Properties.StorageProfile.StorageMB))\n\n\tassert.Equal(t, armmysql.ServerStateReady, *actualMYSQLServer.Properties.UserVisibleState)\n\n\t// website::tag::5:: Get  mySQL server DB details and assert them against the terraform output\n\tactualDatabase := azure.GetMYSQLDB(t, expectedResourceGroupName, expectedMYSQLServerName, expectedMYSQLDBName, \"\")\n\n\tassert.Equal(t, expectedDatabaseCharSet, *actualDatabase.Properties.Charset)\n\tassert.Equal(t, expectedDatabaseCollation, *actualDatabase.Properties.Collation)\n}\n"
  },
  {
    "path": "test/azure/terraform_azure_network_example_test.go",
    "content": "//go:build azure || (azureslim && network)\n// +build azure azureslim,network\n\n// NOTE: We use build tags to differentiate azure testing because we currently do not have azure access setup for\n// CircleCI.\n\npackage test\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/azure\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/gruntwork-io/terratest/modules/terraform\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestTerraformAzureNetworkExample(t *testing.T) {\n\tt.Parallel()\n\n\t// Create values for Terraform\n\tsubscriptionID := \"\"               // subscriptionID is overridden by the environment variable \"ARM_SUBSCRIPTION_ID\"\n\tuniquePostfix := random.UniqueId() // \"resource\" - switch for terratest or manual terraform deployment\n\texpectedLocation := \"eastus2\"\n\texpectedSubnetRange := \"10.0.20.0/24\"\n\texpectedPrivateIP := \"10.0.20.5\"\n\texpectedDnsIp01 := \"10.0.0.5\"\n\texpectedDnsIp02 := \"10.0.0.6\"\n\texectedDNSLabel := fmt.Sprintf(\"dns-terratest-%s\", strings.ToLower(uniquePostfix)) // only lowercase, numeric and hyphens chars allowed for DNS\n\n\t// Configure Terraform setting up a path to Terraform code.\n\tterraformOptions := &terraform.Options{\n\t\t// Relative path to the Terraform dir\n\t\tTerraformDir: \"../../examples/azure/terraform-azure-network-example\",\n\n\t\t// Variables to pass to our Terraform code using -var options.\n\t\tVars: map[string]interface{}{\n\t\t\t\"postfix\":           uniquePostfix,\n\t\t\t\"subnet_prefix\":     expectedSubnetRange,\n\t\t\t\"private_ip\":        expectedPrivateIP,\n\t\t\t\"dns_ip_01\":         expectedDnsIp01,\n\t\t\t\"dns_ip_02\":         expectedDnsIp02,\n\t\t\t\"location\":          expectedLocation,\n\t\t\t\"domain_name_label\": exectedDNSLabel,\n\t\t},\n\t}\n\n\t// At the end of the test, run `terraform destroy` to clean up any resources that were created\n\tdefer terraform.Destroy(t, terraformOptions)\n\n\t// Run `terraform init` and `terraform apply`. Fail the test if there are any errors\n\tterraform.InitAndApply(t, terraformOptions)\n\n\t// Run `terraform output` to get the values of output variables\n\texpectedRgName := terraform.Output(t, terraformOptions, \"resource_group_name\")\n\texpectedVNetName := terraform.Output(t, terraformOptions, \"virtual_network_name\")\n\texpectedSubnetName := terraform.Output(t, terraformOptions, \"subnet_name\")\n\texpectedPublicAddressName := terraform.Output(t, terraformOptions, \"public_address_name\")\n\texpectedPrivateNicName := terraform.Output(t, terraformOptions, \"network_interface_internal\")\n\texpectedPublicNicName := terraform.Output(t, terraformOptions, \"network_interface_external\")\n\n\t// Tests are separated into subtests to differentiate integrated tests and pure resource tests\n\n\t// Integrated network resource tests\n\tt.Run(\"VirtualNetwork_Subnet\", func(t *testing.T) {\n\t\t// Check the Subnet exists in the Virtual Network Subnets with the expected Address Prefix\n\t\tactualVnetSubnets := azure.GetVirtualNetworkSubnets(t, expectedVNetName, expectedRgName, subscriptionID)\n\t\tassert.NotNil(t, actualVnetSubnets[expectedSubnetName])\n\t\tassert.Equal(t, expectedSubnetRange, actualVnetSubnets[expectedSubnetName])\n\t})\n\n\tt.Run(\"NIC_PublicAddress\", func(t *testing.T) {\n\t\t// Check the internal network interface does NOT have a public IP\n\t\tactualPrivateIPOnly := azure.GetNetworkInterfacePublicIPs(t, expectedPrivateNicName, expectedRgName, subscriptionID)\n\t\tassert.Equal(t, 0, len(actualPrivateIPOnly))\n\n\t\t// Check the external network interface has a public IP\n\t\tactualPublicIPs := azure.GetNetworkInterfacePublicIPs(t, expectedPublicNicName, expectedRgName, subscriptionID)\n\t\tassert.Equal(t, 1, len(actualPublicIPs))\n\t})\n\n\tt.Run(\"Subnet_NIC\", func(t *testing.T) {\n\t\t// Check the private IP is in the subnet range\n\t\tcheckPrivateIpInSubnet := azure.CheckSubnetContainsIP(t, expectedPrivateIP, expectedSubnetName, expectedVNetName, expectedRgName, subscriptionID)\n\t\tassert.True(t, checkPrivateIpInSubnet)\n\t})\n\n\t// Test for resource presence\n\tt.Run(\"Exists\", func(t *testing.T) {\n\t\t// Check the Virtual Network exists\n\t\tassert.True(t, azure.VirtualNetworkExists(t, expectedVNetName, expectedRgName, subscriptionID))\n\n\t\t// Check the Subnet exists\n\t\tassert.True(t, azure.SubnetExists(t, expectedSubnetName, expectedVNetName, expectedRgName, subscriptionID))\n\n\t\t// Check the Network Interfaces exist\n\t\tassert.True(t, azure.NetworkInterfaceExists(t, expectedPrivateNicName, expectedRgName, subscriptionID))\n\t\tassert.True(t, azure.NetworkInterfaceExists(t, expectedPublicNicName, expectedRgName, subscriptionID))\n\n\t\t// Check Network Interface that does not exist in the Resource Group\n\t\tassert.False(t, azure.NetworkInterfaceExists(t, \"negative-test\", expectedRgName, subscriptionID))\n\n\t\t// Check Public Address exists\n\t\tassert.True(t, azure.PublicAddressExists(t, expectedPublicAddressName, expectedRgName, subscriptionID))\n\t})\n\n\t// Tests for useful network properties\n\tt.Run(\"Network\", func(t *testing.T) {\n\t\t// Check the Virtual Network DNS server IPs\n\t\tactualDNSIPs := azure.GetVirtualNetworkDNSServerIPs(t, expectedVNetName, expectedRgName, subscriptionID)\n\t\tassert.Contains(t, actualDNSIPs, expectedDnsIp01)\n\t\tassert.Contains(t, actualDNSIPs, expectedDnsIp02)\n\n\t\t// Check the Network Interface private IP\n\t\tactualPrivateIPs := azure.GetNetworkInterfacePrivateIPs(t, expectedPrivateNicName, expectedRgName, subscriptionID)\n\t\tassert.Contains(t, actualPrivateIPs, expectedPrivateIP)\n\n\t\t// Check the Public Address's Public IP is allocated\n\t\tactualPublicIP := azure.GetIPOfPublicIPAddressByName(t, expectedPublicAddressName, expectedRgName, subscriptionID)\n\t\tassert.NotEmpty(t, actualPublicIP)\n\n\t\t// Check DNS created for this example is reserved\n\t\tactualDnsNotAvailable := azure.CheckPublicDNSNameAvailability(t, expectedLocation, exectedDNSLabel, subscriptionID)\n\t\tassert.False(t, actualDnsNotAvailable)\n\n\t\t// Check new randomized DNS is available\n\t\tnewDNSLabel := fmt.Sprintf(\"dns-terratest-%s\", strings.ToLower(random.UniqueId()))\n\t\tactualDnsAvailable := azure.CheckPublicDNSNameAvailability(t, expectedLocation, newDNSLabel, subscriptionID)\n\t\tassert.True(t, actualDnsAvailable)\n\t})\n\n}\n"
  },
  {
    "path": "test/azure/terraform_azure_nsg_example_test.go",
    "content": "//go:build azure\n// +build azure\n\n// NOTE: We use build tags to differentiate azure testing because we currently do not have azure access setup for\n// CircleCI.\n\npackage test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/azure\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/gruntwork-io/terratest/modules/terraform\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestTerraformAzureNsgExample(t *testing.T) {\n\tt.Parallel()\n\n\trandomPostfixValue := random.UniqueId()\n\n\t// Construct options for TF apply\n\tterraformOptions := &terraform.Options{\n\t\t// The path to where our Terraform code is located\n\t\tTerraformDir: \"../../examples/azure/terraform-azure-nsg-example\",\n\t\tVars: map[string]interface{}{\n\t\t\t\"postfix\": randomPostfixValue,\n\t\t},\n\t}\n\n\tdefer terraform.Destroy(t, terraformOptions)\n\tterraform.InitAndApply(t, terraformOptions)\n\n\tresourceGroupName := terraform.Output(t, terraformOptions, \"resource_group_name\")\n\tnsgName := terraform.Output(t, terraformOptions, \"nsg_name\")\n\tsshRuleName := terraform.Output(t, terraformOptions, \"ssh_rule_name\")\n\thttpRuleName := terraform.Output(t, terraformOptions, \"http_rule_name\")\n\n\t// A default NSG has 6 rules, and we have two custom rules for a total of 8\n\trules, err := azure.GetAllNSGRulesE(resourceGroupName, nsgName, \"\")\n\tassert.NoError(t, err)\n\tassert.Equal(t, 8, len(rules.SummarizedRules))\n\n\t// We should have a rule for allowing ssh\n\tsshRule := rules.FindRuleByName(sshRuleName)\n\n\t// That rule should allow port 22 inbound\n\tassert.True(t, sshRule.AllowsDestinationPort(t, \"22\"))\n\n\t// But should not allow 80 inbound\n\tassert.False(t, sshRule.AllowsDestinationPort(t, \"80\"))\n\n\t// SSh is allowed from any port\n\tassert.True(t, sshRule.AllowsSourcePort(t, \"*\"))\n\n\t// We should have a rule for blocking HTTP\n\thttpRule := rules.FindRuleByName(httpRuleName)\n\n\t// This rule should BLOCK port 80 inbound\n\tassert.False(t, httpRule.AllowsDestinationPort(t, \"80\"))\n}\n"
  },
  {
    "path": "test/azure/terraform_azure_postgresql_example_test.go",
    "content": "//go:build azure\n// +build azure\n\n// NOTE: We use build tags to differentiate azure testing because we currently do not have azure access setup for\n// CircleCI.\n\npackage test\n\nimport (\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/azure\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/gruntwork-io/terratest/modules/terraform\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestPostgreSQLDatabase(t *testing.T) {\n\tt.Parallel()\n\n\tuniquePostfix := strings.ToLower(random.UniqueId())\n\n\t// website::tag::1:: Configure Terraform setting up a path to Terraform code.\n\tterraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{\n\t\tTerraformDir: \"../../examples/azure/terraform-azure-postgresql-example\",\n\t\tVars: map[string]interface{}{\n\t\t\t\"postfix\": uniquePostfix,\n\t\t},\n\t\tNoColor: true,\n\t})\n\t// website::tag::4:: At the end of the test, run `terraform destroy` to clean up any resources that were created\n\tdefer terraform.Destroy(t, terraformOptions)\n\n\t// website::tag::2:: Run `terraform init` and `terraform apply`. Fail the test if there are any errors.\n\tterraform.InitAndApply(t, terraformOptions)\n\n\tsubscriptionID := os.Getenv(\"ARM_SUBSCRIPTION_ID\")\n\n\t// website::tag::3:: Run `terraform output` to get the values of output variables\n\texpectedServername := \"postgresqlserver-\" + uniquePostfix // see fixture\n\tactualServername := terraform.Output(t, terraformOptions, \"servername\")\n\trgName := terraform.Output(t, terraformOptions, \"rgname\")\n\texpectedSkuName := terraform.Output(t, terraformOptions, \"sku_name\")\n\n\t// website::tag::4:: Get the Server details and assert them against the terraform output\n\tactualServer := azure.GetPostgreSQLServer(t, rgName, actualServername, subscriptionID)\n\t// Verify\n\tassert.NotNil(t, actualServer)\n\tassert.Equal(t, expectedServername, actualServername)\n\tassert.Equal(t, expectedSkuName, *actualServer.Sku.Name)\n\n}\n"
  },
  {
    "path": "test/azure/terraform_azure_recoveryservices_example_test.go",
    "content": "//go:build azure\n// +build azure\n\n// NOTE: We use build tags to differentiate azure testing because we currently do not have azure access setup for\n// CircleCI.\n\npackage test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/azure\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/gruntwork-io/terratest/modules/terraform\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestTerraformAzureRecoveryServicesExample(t *testing.T) {\n\tt.Parallel()\n\n\t// subscriptionID is overridden by the environment variable \"ARM_SUBSCRIPTION_ID\"\n\tsubscriptionID := \"\"\n\tuniquePostfix := random.UniqueId()\n\n\t// website::tag::1:: Configure Terraform setting up a path to Terraform code.\n\tterraformOptions := &terraform.Options{\n\t\t// The path to where our Terraform code is located\n\t\tTerraformDir: \"../../examples/azure/terraform-azure-recoveryservices-example\",\n\t\t// Variables to pass to our Terraform code using -var options\n\t\tVars: map[string]interface{}{\n\t\t\t\"postfix\": uniquePostfix,\n\t\t},\n\t}\n\n\t// website::tag::4:: At the end of the test, run `terraform destroy` to clean up any resources that were created\n\tdefer terraform.Destroy(t, terraformOptions)\n\n\t// website::tag::2:: Run `terraform init` and `terraform apply`. Fail the test if there are any errors.\n\tterraform.InitAndApply(t, terraformOptions)\n\n\t// website::tag::3:: Run `terraform output` to get the values of output variables\n\tresourceGroupName := terraform.Output(t, terraformOptions, \"resource_group_name\")\n\tvaultName := terraform.Output(t, terraformOptions, \"recovery_service_vault_name\")\n\tpolicyVmName := terraform.Output(t, terraformOptions, \"backup_policy_vm_name\")\n\n\t// website::tag::4:: Verify the recovery services resources\n\texists := azure.RecoveryServicesVaultExists(t, vaultName, resourceGroupName, subscriptionID)\n\tassert.True(t, exists, \"vault does not exist\")\n\n\tpolicyList := azure.GetRecoveryServicesVaultBackupPolicyList(t, vaultName, resourceGroupName, subscriptionID)\n\tassert.NotNil(t, policyList, \"vault backup policy list is nil\")\n\n\tvmPolicyList := azure.GetRecoveryServicesVaultBackupProtectedVMList(t, policyVmName, vaultName, resourceGroupName, subscriptionID)\n\tassert.NotNil(t, vmPolicyList, \"vault backup policy list for protected vm is nil\")\n}\n"
  },
  {
    "path": "test/azure/terraform_azure_resourcegroup_example_test.go",
    "content": "//go:build azure\n// +build azure\n\n// NOTE: We use build tags to differentiate azure testing because we currently do not have azure access setup for\n// CircleCI.\n\npackage test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/azure\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/gruntwork-io/terratest/modules/terraform\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestTerraformAzureResourceGroupExample(t *testing.T) {\n\tt.Parallel()\n\n\t// subscriptionID is overridden by the environment variable \"ARM_SUBSCRIPTION_ID\"\n\tsubscriptionID := \"\"\n\tuniquePostfix := random.UniqueId()\n\n\t// website::tag::1:: Configure Terraform setting up a path to Terraform code.\n\tterraformOptions := &terraform.Options{\n\t\t// The path to where our Terraform code is located\n\t\tTerraformDir: \"../../examples/azure/terraform-azure-resourcegroup-example\",\n\t\tVars: map[string]interface{}{\n\t\t\t\"postfix\": uniquePostfix,\n\t\t},\n\t}\n\n\t// website::tag::4:: At the end of the test, run `terraform destroy` to clean up any resources that were created\n\tdefer terraform.Destroy(t, terraformOptions)\n\n\t// website::tag::2:: Run `terraform init` and `terraform apply`. Fail the test if there are any errors.\n\tterraform.InitAndApply(t, terraformOptions)\n\n\t// website::tag::3:: Run `terraform output` to get the values of output variables\n\tresourceGroupName := terraform.Output(t, terraformOptions, \"resource_group_name\")\n\n\t// website::tag::4:: Verify the resource group exists\n\texists := azure.ResourceGroupExists(t, resourceGroupName, subscriptionID)\n\tassert.True(t, exists, \"Resource group does not exist\")\n\n\t// website::tag::4:: Verify the resource group exists\n\texistsv2 := azure.ResourceGroupExistsV2(t, resourceGroupName, subscriptionID)\n\tassert.True(t, existsv2, \"Resource group does not exist\")\n}\n"
  },
  {
    "path": "test/azure/terraform_azure_servicebus_example_test.go",
    "content": "//go:build azure\n// +build azure\n\n// NOTE: We use build tags to differentiate azure testing because we currently do not have azure access setup for\n// CircleCI.\n\npackage test\n\nimport (\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/azure\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/gruntwork-io/terratest/modules/terraform\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestTerraformAzureServiceBusExample(t *testing.T) {\n\tt.Parallel()\n\n\tuniquePostfix := strings.ToLower(random.UniqueId())\n\n\t// website::tag::1:: Configure Terraform setting up a path to Terraform code.\n\tterraformOptions := &terraform.Options{\n\t\t// The path to where our Terraform code is located\n\t\tTerraformDir: \"../../examples/azure/terraform-azure-servicebus-example\",\n\t\tVars: map[string]interface{}{\n\t\t\t\"postfix\": uniquePostfix,\n\t\t},\n\t}\n\n\t// website::tag::4:: At the end of the test, run `terraform destroy` to clean up any resources that were created\n\tdefer terraform.Destroy(t, terraformOptions)\n\n\t// website::tag::2:: Run `terraform init` and `terraform apply`. Fail the test if there are any errors.\n\tterraform.InitAndApply(t, terraformOptions)\n\n\t// website::tag::3:: Run `terraform output` to get the values of output variables\n\texpectedTopicSubscriptionsMap := terraform.OutputMapOfObjects(t, terraformOptions, \"topics\")\n\texpectedNamespaceName := terraform.Output(t, terraformOptions, \"namespace_name\")\n\texpectedResourceGroup := terraform.Output(t, terraformOptions, \"resource_group\")\n\n\tfor topicName, topicsMap := range expectedTopicSubscriptionsMap {\n\t\tactualsubscriptionNames := azure.ListTopicSubscriptionsName(t,\n\t\t\tos.Getenv(\"ARM_SUBSCRIPTION_ID\"),\n\t\t\texpectedNamespaceName,\n\t\t\texpectedResourceGroup,\n\t\t\ttopicName)\n\n\t\tsubscriptionsMap := topicsMap.(map[string]interface{})[\"subscriptions\"].(map[string]interface{})\n\t\tsubscriptionNamesFromOutput := getMapKeylist(subscriptionsMap)\n\t\t// each subscription from the output should also exist in Azure\n\t\tassert.Equal(t, len(subscriptionNamesFromOutput), len(actualsubscriptionNames))\n\t\tfor _, subscrptionName := range subscriptionNamesFromOutput {\n\t\t\tassert.Contains(t, actualsubscriptionNames, subscrptionName)\n\t\t}\n\t}\n}\n\nfunc getMapKeylist(mapList map[string]interface{}) []string {\n\tnames := make([]string, 0)\n\tfor key := range mapList {\n\t\tnames = append(names, key)\n\t}\n\treturn names\n}\n"
  },
  {
    "path": "test/azure/terraform_azure_sqldb_example_test.go",
    "content": "//go:build azure\n// +build azure\n\n// NOTE: We use build tags to differentiate azure testing because we currently do not have azure access setup for\n// CircleCI.\n\npackage test\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql\"\n\t\"github.com/gruntwork-io/terratest/modules/azure\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/gruntwork-io/terratest/modules/terraform\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestTerraformAzureSQLDBExample(t *testing.T) {\n\tt.Parallel()\n\n\tuniquePostfix := strings.ToLower(random.UniqueId())\n\n\t// website::tag::1:: Configure Terraform setting up a path to Terraform code.\n\tterraformOptions := &terraform.Options{\n\t\t// The path to where our Terraform code is located\n\t\tTerraformDir: \"../../examples/azure/terraform-azure-sqldb-example\",\n\t\tVars: map[string]interface{}{\n\t\t\t\"postfix\": uniquePostfix,\n\t\t},\n\t}\n\n\t// website::tag::4:: At the end of the test, run `terraform destroy` to clean up any resources that were created\n\tdefer terraform.Destroy(t, terraformOptions)\n\n\t// website::tag::2:: Run `terraform init` and `terraform apply`. Fail the test if there are any errors.\n\tterraform.InitAndApply(t, terraformOptions)\n\n\t// website::tag::3:: Run `terraform output` to get the values of output variables\n\texpectedSQLServerID := terraform.Output(t, terraformOptions, \"sql_server_id\")\n\texpectedSQLServerName := terraform.Output(t, terraformOptions, \"sql_server_name\")\n\n\texpectedSQLServerFullDomainName := terraform.Output(t, terraformOptions, \"sql_server_full_domain_name\")\n\texpectedSQLDBName := terraform.Output(t, terraformOptions, \"sql_database_name\")\n\n\texpectedSQLDBID := terraform.Output(t, terraformOptions, \"sql_database_id\")\n\texpectedResourceGroupName := terraform.Output(t, terraformOptions, \"resource_group_name\")\n\texpectedSQLDBStatus := \"Online\"\n\n\t// website::tag::4:: Get the SQL server details and assert them against the terraform output\n\tactualSQLServer := azure.GetSQLServer(t, expectedResourceGroupName, expectedSQLServerName, \"\")\n\n\tassert.Equal(t, expectedSQLServerID, *actualSQLServer.ID)\n\tassert.Equal(t, expectedSQLServerFullDomainName, *actualSQLServer.Properties.FullyQualifiedDomainName)\n\tassert.Equal(t, armsql.ServerStateReady, *actualSQLServer.Properties.State)\n\n\t// website::tag::5:: Get the SQL server DB details and assert them against the terraform output\n\tactualSQLDatabase := azure.GetSQLDatabase(t, expectedResourceGroupName, expectedSQLServerName, expectedSQLDBName, \"\")\n\n\tassert.Equal(t, expectedSQLDBID, *actualSQLDatabase.ID)\n\tassert.Equal(t, expectedSQLDBStatus, string(*actualSQLDatabase.Properties.Status))\n}\n"
  },
  {
    "path": "test/azure/terraform_azure_sqlmanagedinstance_example_test.go",
    "content": "//go:build azure_ci_excluded\n// +build azure_ci_excluded\n\n// This test is tagged as !azure to prevent it from being executed from CI workflow, as SQL Managed Instance takes 6-8 hours for deployment\n// Please refer to examples/azure/terraform-azure-sqlmanagedinstance-example/README.md for more details\n\npackage test\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/azure\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/gruntwork-io/terratest/modules/terraform\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestTerraformAzureSQLManagedInstanceExample(t *testing.T) {\n\tif testing.Short() {\n\t\tt.Skip(\"Skipping long-running test\")\n\t}\n\tt.Parallel()\n\n\tuniquePostfix := strings.ToLower(random.UniqueId())\n\texpectedLocation := \"westus\"\n\texpectedAdminLogin := \"sqlmiadmin\"\n\texpectedSQLMIState := \"Ready\"\n\texpectedSKUName := \"GP_Gen5\"\n\texpectedDatabaseName := \"testdb\"\n\n\t// Configure Terraform setting up a path to Terraform code.\n\tterraformOptions := &terraform.Options{\n\t\t// The path to where our Terraform code is located\n\t\tTerraformDir: \"../../examples/azure/terraform-azure-sqlmanagedinstance-example\",\n\t\tVars: map[string]interface{}{\n\t\t\t\"postfix\":       uniquePostfix,\n\t\t\t\"location\":      expectedLocation,\n\t\t\t\"admin_login\":   expectedAdminLogin,\n\t\t\t\"sku_name\":      expectedSKUName,\n\t\t\t\"sqlmi_db_name\": expectedDatabaseName,\n\t\t},\n\t}\n\n\t// At the end of the test, run `terraform destroy` to clean up any resources that were created\n\tdefer terraform.Destroy(t, terraformOptions)\n\n\t// Run `terraform init` and `terraform apply`. Fail the test if there are any errors.\n\tterraform.InitAndApply(t, terraformOptions)\n\n\t// Run `terraform output` to get the values of output variables\n\texpectedResourceGroupName := terraform.Output(t, terraformOptions, \"resource_group_name\")\n\texpectedManagedInstanceName := terraform.Output(t, terraformOptions, \"managed_instance_name\")\n\n\t// check for if data factory exists\n\tactualManagedInstanceExits := azure.SQLManagedInstanceExists(t, expectedManagedInstanceName, expectedResourceGroupName, \"\")\n\tassert.True(t, actualManagedInstanceExits)\n\n\t// Get the SQL Managed Instance details and assert them against the terraform output\n\tactualSQLManagedInstance := azure.GetManagedInstance(t, expectedResourceGroupName, expectedManagedInstanceName, \"\")\n\tactualSQLManagedInstanceDatabase := azure.GetManagedInstanceDatabase(t, expectedResourceGroupName, expectedManagedInstanceName, expectedDatabaseName, \"\")\n\n\tassert.Equal(t, expectedManagedInstanceName, *actualSQLManagedInstance.Name)\n\tassert.Equal(t, expectedLocation, *actualSQLManagedInstance.Location)\n\tassert.Equal(t, expectedSKUName, *actualSQLManagedInstance.Sku.Name)\n\tassert.Equal(t, expectedSQLMIState, *actualSQLManagedInstance.ManagedInstanceProperties.State)\n\n\tassert.Equal(t, expectedDatabaseName, *actualSQLManagedInstanceDatabase.Name)\n\n}\n"
  },
  {
    "path": "test/azure/terraform_azure_storage_example_test.go",
    "content": "//go:build azure\n// +build azure\n\n// NOTE: We use build tags to differentiate azure testing because we currently do not have azure access setup for\n// CircleCI.\n\npackage test\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/azure\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/gruntwork-io/terratest/modules/terraform\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestTerraformAzureStorageExample(t *testing.T) {\n\tt.Parallel()\n\n\t// subscriptionID is overridden by the environment variable \"ARM_SUBSCRIPTION_ID\"\n\tsubscriptionID := \"\"\n\tuniquePostfix := random.UniqueId()\n\n\t// website::tag::1:: Configure Terraform setting up a path to Terraform code.\n\tterraformOptions := &terraform.Options{\n\t\t// The path to where our Terraform code is located\n\t\tTerraformDir: \"../../examples/azure/terraform-azure-storage-example\",\n\n\t\t// Variables to pass to our Terraform code using -var options\n\t\tVars: map[string]interface{}{\n\t\t\t\"postfix\": strings.ToLower(uniquePostfix),\n\t\t},\n\t}\n\n\t// website::tag::4:: At the end of the test, run `terraform destroy` to clean up any resources that were created\n\tdefer terraform.Destroy(t, terraformOptions)\n\n\t// website::tag::2:: Run `terraform init` and `terraform apply`. Fail the test if there are any errors.\n\tterraform.InitAndApply(t, terraformOptions)\n\n\t// website::tag::3:: Run `terraform output` to get the values of output variables\n\tresourceGroupName := terraform.Output(t, terraformOptions, \"resource_group_name\")\n\tstorageAccountName := terraform.Output(t, terraformOptions, \"storage_account_name\")\n\tstorageAccountTier := terraform.Output(t, terraformOptions, \"storage_account_account_tier\")\n\tstorageAccountKind := terraform.Output(t, terraformOptions, \"storage_account_account_kind\")\n\tstorageBlobContainerName := terraform.Output(t, terraformOptions, \"storage_container_name\")\n\n\t// website::tag::4:: Verify storage account properties and ensure it matches the output.\n\tstorageAccountExists := azure.StorageAccountExists(t, storageAccountName, resourceGroupName, subscriptionID)\n\tassert.True(t, storageAccountExists, \"storage account does not exist\")\n\n\tcontainerExists := azure.StorageBlobContainerExists(t, storageBlobContainerName, storageAccountName, resourceGroupName, subscriptionID)\n\tassert.True(t, containerExists, \"storage container does not exist\")\n\n\tpublicAccess := azure.GetStorageBlobContainerPublicAccess(t, storageBlobContainerName, storageAccountName, resourceGroupName, subscriptionID)\n\tassert.False(t, publicAccess, \"storage container has public access\")\n\n\taccountKind := azure.GetStorageAccountKind(t, storageAccountName, resourceGroupName, subscriptionID)\n\tassert.Equal(t, storageAccountKind, accountKind, \"storage account kind mismatch\")\n\n\tskuTier := azure.GetStorageAccountSkuTier(t, storageAccountName, resourceGroupName, subscriptionID)\n\tassert.Equal(t, storageAccountTier, skuTier, \"sku tier mismatch\")\n\n\tactualDNSString := azure.GetStorageDNSString(t, storageAccountName, resourceGroupName, subscriptionID)\n\tstorageSuffix, _ := azure.GetStorageURISuffixE()\n\texpectedDNS := fmt.Sprintf(\"https://%s.blob.%s/\", storageAccountName, storageSuffix)\n\tassert.Equal(t, expectedDNS, actualDNSString, \"Storage DNS string mismatch\")\n}\n"
  },
  {
    "path": "test/azure/terraform_azure_synapse_example_test.go",
    "content": "//go:build azure\n// +build azure\n\n// NOTE: We use build tags to differentiate azure testing because we currently do not have azure access setup for\n// CircleCI.\n\npackage test\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/azure\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/gruntwork-io/terratest/modules/terraform\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestTerraformAzureSynapseExample(t *testing.T) {\n\tt.Parallel()\n\n\tuniquePostfix := strings.ToLower(random.UniqueId())\n\texpectedSynapseSqlUser := \"sqladminuser\"\n\texpectedSynapseProvisioningState := \"Succeeded\"\n\texpectedLocation := \"westus2\"\n\texpectedSyPoolSkuName := \"DW100c\"\n\n\t// website::tag::1:: Configure Terraform setting up a path to Terraform code.\n\tterraformOptions := &terraform.Options{\n\t\t// The path to where our Terraform code is located\n\t\tTerraformDir: \"../../examples/azure/terraform-azure-synapse-example\",\n\t\tVars: map[string]interface{}{\n\t\t\t\"postfix\":                  uniquePostfix,\n\t\t\t\"synapse_sql_user\":         expectedSynapseSqlUser,\n\t\t\t\"location\":                 expectedLocation,\n\t\t\t\"synapse_sqlpool_sku_name\": expectedSyPoolSkuName,\n\t\t},\n\t}\n\n\t// website::tag::4:: At the end of the test, run `terraform destroy` to clean up any resources that were created\n\tdefer terraform.Destroy(t, terraformOptions)\n\n\t// website::tag::2:: Run `terraform init` and `terraform apply`. Fail the test if there are any errors.\n\tterraform.InitAndApply(t, terraformOptions)\n\t// terraform.InitE(t, terraformOptions)\n\n\t// website::tag::3:: Run `terraform output` to get the values of output variables\n\texpectedResourceGroupName := terraform.Output(t, terraformOptions, \"resource_group_name\")\n\texpectedSyDLgen2Name := terraform.Output(t, terraformOptions, \"synapse_dlgen2_name\")\n\texpectedSyWorkspaceName := terraform.Output(t, terraformOptions, \"synapse_workspace_name\")\n\texpectedSqlPoolName := terraform.Output(t, terraformOptions, \"synapse_sqlpool_name\")\n\n\t// website::tag::4:: Get synapse details and assert them against the terraform output\n\tactualSynapseWorkspace := azure.GetSynapseWorkspace(t, expectedResourceGroupName, expectedSyWorkspaceName, \"\")\n\tactualSynapseSqlPool := azure.GetSynapseSqlPool(t, expectedResourceGroupName, expectedSyWorkspaceName, expectedSqlPoolName, \"\")\n\n\tassert.Equal(t, expectedSyWorkspaceName, *actualSynapseWorkspace.Name)\n\tassert.Equal(t, expectedSynapseSqlUser, *actualSynapseWorkspace.WorkspaceProperties.SQLAdministratorLogin)\n\tassert.Equal(t, expectedSynapseProvisioningState, *actualSynapseWorkspace.WorkspaceProperties.ProvisioningState)\n\tassert.Equal(t, expectedLocation, *actualSynapseWorkspace.Location)\n\tassert.Equal(t, expectedSyDLgen2Name, *actualSynapseWorkspace.WorkspaceProperties.DefaultDataLakeStorage.Filesystem)\n\n\tassert.Equal(t, expectedSqlPoolName, *actualSynapseSqlPool.Name)\n\tassert.Equal(t, expectedSyPoolSkuName, *actualSynapseSqlPool.Sku.Name)\n}\n"
  },
  {
    "path": "test/azure/terraform_azure_vm_example_test.go",
    "content": "//go:build azure || (azureslim && compute)\n// +build azure azureslim,compute\n\n// NOTE: We use build tags to differentiate azure testing because we currently do not have azure access setup for\n// CircleCI.\n\npackage test\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2019-07-01/compute\"\n\t\"github.com/gruntwork-io/terratest/modules/azure\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/gruntwork-io/terratest/modules/terraform\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestTerraformAzureVmExample(t *testing.T) {\n\tt.Parallel()\n\n\tsubscriptionID := \"\"\n\tuniquePostfix := random.UniqueId()\n\n\t// Configure Terraform setting up a path to Terraform code.\n\tterraformOptions := &terraform.Options{\n\t\t// The path to where our Terraform code is located.\n\t\tTerraformDir: \"../../examples/azure/terraform-azure-vm-example\",\n\n\t\t// Variables to pass to our Terraform code using -var options.\n\t\tVars: map[string]interface{}{\n\t\t\t\"postfix\": uniquePostfix,\n\t\t},\n\t}\n\n\t// At the end of the test, run `terraform destroy` to clean up any resources that were created.\n\tdefer terraform.Destroy(t, terraformOptions)\n\n\t// Run `terraform init` and `terraform apply`. Fail the test if there are any errors.\n\tterraform.InitAndApply(t, terraformOptions)\n\n\t// Run tests for the Virtual Machine.\n\ttestStrategiesForVMs(t, terraformOptions, subscriptionID)\n\ttestMultipleVMs(t, terraformOptions, subscriptionID)\n\ttestInformationOfVM(t, terraformOptions, subscriptionID)\n\ttestDisksOfVM(t, terraformOptions, subscriptionID)\n\ttestNetworkOfVM(t, terraformOptions, subscriptionID)\n}\n\n// These 3 tests check for the same property but illustrate different testing strategies for\n// retriving the data. The first strategy is used in the other tests of this module while\n// the other two can be extended by the user as needed.\nfunc testStrategiesForVMs(t *testing.T, terraformOptions *terraform.Options, subscriptionID string) {\n\t// Run `terraform output` to get the values of output variables.\n\tresourceGroupName := terraform.Output(t, terraformOptions, \"resource_group_name\")\n\tvirtualMachineName := terraform.Output(t, terraformOptions, \"vm_name\")\n\texpectedVMSize := compute.VirtualMachineSizeTypes(terraform.Output(t, terraformOptions, \"vm_size\"))\n\n\t// 1. Check the VM Size directly. This strategy gets one specific property of the VM per method.\n\tactualVMSize := azure.GetSizeOfVirtualMachine(t, virtualMachineName, resourceGroupName, subscriptionID)\n\tassert.Equal(t, expectedVMSize, actualVMSize)\n\n\t// 2. Check the VM size by reference. This strategy is beneficial when checking multiple properties\n\t// by using one VM reference. Optional parameters have to be checked first to avoid nil panics.\n\tvmByRef := azure.GetVirtualMachine(t, virtualMachineName, resourceGroupName, subscriptionID)\n\tactualVMSize = vmByRef.HardwareProfile.VMSize\n\tassert.Equal(t, expectedVMSize, actualVMSize)\n\n\t// 3. Check the VM size by instance. This strategy is beneficial when checking multiple properties\n\t// by using one VM instance and making calls against it with the added benefit of property check abstraction.\n\tvmInstance := azure.Instance{VirtualMachine: vmByRef}\n\tactualVMSize = vmInstance.GetVirtualMachineInstanceSize()\n\tassert.Equal(t, expectedVMSize, actualVMSize)\n}\n\n// These tests check for the multiple Virtual Machines in a Resource Group.\nfunc testMultipleVMs(t *testing.T, terraformOptions *terraform.Options, subscriptionID string) {\n\t// Run `terraform output` to get the values of output variables.\n\tresourceGroupName := terraform.Output(t, terraformOptions, \"resource_group_name\")\n\texpectedVMName := terraform.Output(t, terraformOptions, \"vm_name\")\n\texpectedVMSize := compute.VirtualMachineSizeTypes(terraform.Output(t, terraformOptions, \"vm_size\"))\n\texpectedAvsName := terraform.Output(t, terraformOptions, \"availability_set_name\")\n\n\t// Check against all VM names in a Resource Group.\n\tvmList := azure.ListVirtualMachinesForResourceGroup(t, resourceGroupName, subscriptionID)\n\texpectedVMCount := 1\n\tassert.Equal(t, expectedVMCount, len(vmList))\n\tassert.Contains(t, vmList, expectedVMName)\n\n\t// Check Availability Set for multiple VMs.\n\tactualVMsInAvs := azure.GetAvailabilitySetVMNamesInCaps(t, expectedAvsName, resourceGroupName, subscriptionID)\n\tassert.Contains(t, actualVMsInAvs, strings.ToUpper(expectedVMName))\n\n\t// Get all VMs in a Resource Group, including their properties, therefore avoiding\n\t// multiple SDK calls. The penalty for this approach is introducing direct references\n\t// which need to be checked for nil for optional configurations.\n\tvmsByRef := azure.GetVirtualMachinesForResourceGroup(t, resourceGroupName, subscriptionID)\n\tthisVM := vmsByRef[expectedVMName]\n\tassert.Equal(t, expectedVMSize, thisVM.HardwareProfile.VMSize)\n\n\t// Check for the VM negative test.\n\tfakeVM := fmt.Sprintf(\"vm-%s\", random.UniqueId())\n\tassert.Nil(t, vmsByRef[fakeVM].VMID)\n}\n\n// These tests check information directly related to the specified Azure Virtual Machine.\nfunc testInformationOfVM(t *testing.T, terraformOptions *terraform.Options, subscriptionID string) {\n\t// Run `terraform output` to get the values of output variables.\n\tresourceGroupName := terraform.Output(t, terraformOptions, \"resource_group_name\")\n\tvirtualMachineName := terraform.Output(t, terraformOptions, \"vm_name\")\n\texpectedVmAdminUser := terraform.OutputList(t, terraformOptions, \"vm_admin_username\")\n\texpectedImageSKU := terraform.OutputList(t, terraformOptions, \"vm_image_sku\")\n\texpectedImageVersion := terraform.OutputList(t, terraformOptions, \"vm_image_version\")\n\texpectedAvsName := terraform.Output(t, terraformOptions, \"availability_set_name\")\n\texpectedVMTags := terraform.OutputMap(t, terraformOptions, \"vm_tags\")\n\n\t// Check if the Virtual Machine exists.\n\tassert.True(t, azure.VirtualMachineExists(t, virtualMachineName, resourceGroupName, subscriptionID))\n\n\t// Check the Admin User of the VM.\n\tactualVM := azure.GetVirtualMachine(t, virtualMachineName, resourceGroupName, subscriptionID)\n\tactualVmAdminUser := *actualVM.OsProfile.AdminUsername\n\tassert.Equal(t, expectedVmAdminUser[0], actualVmAdminUser)\n\n\t// Check the Storage Image properties of the VM.\n\tactualImage := azure.GetVirtualMachineImage(t, virtualMachineName, resourceGroupName, subscriptionID)\n\tassert.Contains(t, expectedImageSKU[0], actualImage.SKU)\n\tassert.Contains(t, expectedImageVersion[0], actualImage.Version)\n\n\t// Check the Availability Set of the VM.\n\t// The AVS ID returned from the VM is always CAPS so ignoring case in the assertion.\n\tactualexpectedAvsName := azure.GetVirtualMachineAvailabilitySetID(t, virtualMachineName, resourceGroupName, subscriptionID)\n\tassert.True(t, strings.EqualFold(expectedAvsName, actualexpectedAvsName))\n\n\t// Check the assigned Tags of the VM, assert empty if no tags.\n\tactualVMTags := azure.GetVirtualMachineTags(t, virtualMachineName, resourceGroupName, \"\")\n\tassert.Equal(t, expectedVMTags, actualVMTags)\n}\n\n// These tests check the OS Disk and Attached Managed Disks for the Azure Virtual Machine.\n// The following Terratest Azure module is utilized in addition to the compute module:\n// - disk\n// See the terraform_azure_disk_example_test.go for other related tests.\nfunc testDisksOfVM(t *testing.T, terraformOptions *terraform.Options, subscriptionID string) {\n\t// Run `terraform output` to get the values of output variables.\n\tresourceGroupName := terraform.Output(t, terraformOptions, \"resource_group_name\")\n\tvirtualMachineName := terraform.Output(t, terraformOptions, \"vm_name\")\n\texpectedOSDiskName := terraform.Output(t, terraformOptions, \"os_disk_name\")\n\texpectedDiskName := terraform.Output(t, terraformOptions, \"managed_disk_name\")\n\texpectedDiskType := terraform.Output(t, terraformOptions, \"managed_disk_type\")\n\n\t// Check the OS Disk name of the VM.\n\tactualOSDiskName := azure.GetVirtualMachineOSDiskName(t, virtualMachineName, resourceGroupName, subscriptionID)\n\tassert.Equal(t, expectedOSDiskName, actualOSDiskName)\n\n\t// Check the VM Managed Disk exists in the list of all VM Managed Disks.\n\tactualManagedDiskNames := azure.GetVirtualMachineManagedDisks(t, virtualMachineName, resourceGroupName, subscriptionID)\n\tassert.Contains(t, actualManagedDiskNames, expectedDiskName)\n\n\t// Check the Managed Disk count of the VM.\n\texpectedManagedDiskCount := 1\n\tassert.Equal(t, expectedManagedDiskCount, len(actualManagedDiskNames))\n\n\t// Check the Disk Type of the Managed Disk of the VM.\n\t// This does not apply to VHD disks saved under a storage account.\n\tactualDisk := azure.GetDisk(t, expectedDiskName, resourceGroupName, subscriptionID)\n\tactualDiskType := actualDisk.Sku.Name\n\tassert.Equal(t, compute.DiskStorageAccountTypes(expectedDiskType), actualDiskType)\n}\n\n// These tests check the underlying Virtual Network, Network Interface and associated Public IP Address.\n// The following Terratest Azure modules are utilized in addition to the compute module:\n// - networkinterface\n// - publicaddress\n// - virtualnetwork\n// See the terraform_azure_network_example_test.go for other related tests.\nfunc testNetworkOfVM(t *testing.T, terraformOptions *terraform.Options, subscriptionID string) {\n\t// Run `terraform output` to get the values of output variables.\n\tresourceGroupName := terraform.Output(t, terraformOptions, \"resource_group_name\")\n\tvirtualMachineName := terraform.Output(t, terraformOptions, \"vm_name\")\n\texpectedVNetName := terraform.Output(t, terraformOptions, \"virtual_network_name\")\n\texpectedSubnetName := terraform.Output(t, terraformOptions, \"subnet_name\")\n\texpectedPublicAddressName := terraform.Output(t, terraformOptions, \"public_ip_name\")\n\texpectedNicName := terraform.Output(t, terraformOptions, \"network_interface_name\")\n\texpectedPrivateIPAddress := terraform.Output(t, terraformOptions, \"private_ip\")\n\n\t// VirtualNetwork and Subnet tests\n\t// Check the Subnet exists in the Virtual Network.\n\tactualVnetSubnets := azure.GetVirtualNetworkSubnets(t, expectedVNetName, resourceGroupName, subscriptionID)\n\tassert.NotNil(t, actualVnetSubnets[expectedVNetName])\n\n\t// Check the Private IP is in the Subnet Range.\n\tactualVMNicIPInSubnet := azure.CheckSubnetContainsIP(t, expectedPrivateIPAddress, expectedSubnetName, expectedVNetName, resourceGroupName, subscriptionID)\n\tassert.True(t, actualVMNicIPInSubnet)\n\n\t// Network Interface Card tests\n\t// Check the VM Network Interface exists in the list of all VM Network Interfaces.\n\tactualNics := azure.GetVirtualMachineNics(t, virtualMachineName, resourceGroupName, subscriptionID)\n\tassert.Contains(t, actualNics, expectedNicName)\n\n\t// Check the Network Interface count of the VM.\n\texpectedNICCount := 1\n\tassert.Equal(t, expectedNICCount, len(actualNics))\n\n\t// Check for the Private IP in the NICs IP list.\n\tactualPrivateIPAddress := azure.GetNetworkInterfacePrivateIPs(t, expectedNicName, resourceGroupName, subscriptionID)\n\tassert.Contains(t, actualPrivateIPAddress, expectedPrivateIPAddress)\n\n\t// Public IP Address test\n\t// Check for the Public IP for the NIC. No expected value since it is assigned runtime.\n\tactualPublicIP := azure.GetIPOfPublicIPAddressByName(t, expectedPublicAddressName, resourceGroupName, subscriptionID)\n\tassert.NotNil(t, actualPublicIP)\n\n}\n"
  },
  {
    "path": "test/docker_hello_world_example_test.go",
    "content": "package test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/docker\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestDockerHelloWorldExample(t *testing.T) {\n\t// website::tag::1:: Configure the tag to use on the Docker image.\n\ttag := \"gruntwork/docker-hello-world-example\"\n\tbuildOptions := &docker.BuildOptions{\n\t\tTags: []string{tag},\n\t}\n\n\t// website::tag::2:: Build the Docker image.\n\tdocker.Build(t, \"../examples/docker-hello-world-example\", buildOptions)\n\n\t// website::tag::3:: Run the Docker image, read the text file from it, and make sure it contains the expected output.\n\topts := &docker.RunOptions{Command: []string{\"cat\", \"/test.txt\"}}\n\toutput := docker.Run(t, tag, opts)\n\tassert.Equal(t, \"Hello, World!\", output)\n}\n"
  },
  {
    "path": "test/docker_stdout_example_test.go",
    "content": "package test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/docker\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestDockerComposeStdoutExample(t *testing.T) {\n\tt.Parallel()\n\tdockerComposeFile := \"../examples/docker-compose-stdout-example/docker-compose.yml\"\n\n\t// Run the build step first so that the build output doesn't go to stdout during the compose step.\n\tdocker.RunDockerCompose(\n\t\tt,\n\t\t&docker.Options{},\n\t\t\"-f\",\n\t\tdockerComposeFile,\n\t\t\"build\",\n\t)\n\n\t// Run the Docker image, read the stdout from it, and make sure it contains the expected output.\n\t// The script must be run using `run bash_script` rather than `up`, so that the echo output from the script\n\t// is the only thing that outputs to stdout.\n\toutput := docker.RunDockerComposeAndGetStdOut(\n\t\tt,\n\t\t&docker.Options{},\n\t\t\"-f\",\n\t\tdockerComposeFile,\n\t\t\"run\",\n\t\t\"bash_script\",\n\t)\n\n\tassert.Contains(t, output, \"stdout: message\")\n\tassert.NotContains(t, output, \"stderr: error\")\n}\n"
  },
  {
    "path": "test/fixtures/copy-folder-contents/full-copy/.hidden-file.txt",
    "content": "hidden"
  },
  {
    "path": "test/fixtures/copy-folder-contents/full-copy/.terraform-version",
    "content": "0.15.5"
  },
  {
    "path": "test/fixtures/copy-folder-contents/full-copy/foo.txt",
    "content": "foo"
  },
  {
    "path": "test/fixtures/copy-folder-contents/full-copy/subfolder/.hidden-folder/baz.txt",
    "content": "baz"
  },
  {
    "path": "test/fixtures/copy-folder-contents/full-copy/subfolder/bar.txt",
    "content": "bar"
  },
  {
    "path": "test/fixtures/copy-folder-contents/no-hidden-files/foo.txt",
    "content": "foo"
  },
  {
    "path": "test/fixtures/copy-folder-contents/no-hidden-files/subfolder/bar.txt",
    "content": "bar"
  },
  {
    "path": "test/fixtures/copy-folder-contents/no-hidden-files-no-terraform-files/.terraform-version",
    "content": "0.15.5"
  },
  {
    "path": "test/fixtures/copy-folder-contents/no-hidden-files-no-terraform-files/foo.txt",
    "content": "foo"
  },
  {
    "path": "test/fixtures/copy-folder-contents/no-hidden-files-no-terraform-files/subfolder/bar.txt",
    "content": "bar"
  },
  {
    "path": "test/fixtures/copy-folder-contents/no-state-files/terragrunt.hcl",
    "content": "locals {\n  foo = \"bar\"\n}"
  },
  {
    "path": "test/fixtures/copy-folder-contents/original/.hidden-file.txt",
    "content": "hidden"
  },
  {
    "path": "test/fixtures/copy-folder-contents/original/.terraform-version",
    "content": "0.15.5"
  },
  {
    "path": "test/fixtures/copy-folder-contents/original/foo.txt",
    "content": "foo"
  },
  {
    "path": "test/fixtures/copy-folder-contents/original/subfolder/.hidden-folder/baz.txt",
    "content": "baz"
  },
  {
    "path": "test/fixtures/copy-folder-contents/original/subfolder/bar.txt",
    "content": "bar"
  },
  {
    "path": "test/fixtures/copy-folder-contents/symlinks/foo.txt",
    "content": "foo"
  },
  {
    "path": "test/fixtures/copy-folder-contents/symlinks/subfolder/bar.txt",
    "content": "bar"
  },
  {
    "path": "test/fixtures/copy-folder-contents/symlinks-broken/foo.txt",
    "content": "foo"
  },
  {
    "path": "test/fixtures/copy-folder-contents/symlinks-broken/subfolder/bar.txt",
    "content": "bar"
  },
  {
    "path": "test/fixtures/copy-folder-contents/terragrunt-files/terragrunt.hcl",
    "content": "locals {\n  foo = \"bar\"\n}"
  },
  {
    "path": "test/fixtures/docker/Dockerfile",
    "content": "# A \"Hello, World\" Docker image used in automated tests for the docker.Build command.\nFROM alpine:3.7 as step1\nARG text1\nRUN echo $text1 > text.txt\nCMD [\"cat\", \"text.txt\"]\n\nFROM step1\nARG text\nRUN echo $text > text.txt\nCMD [\"cat\", \"text.txt\"]"
  },
  {
    "path": "test/fixtures/docker-compose-with-buildkit/Dockerfile",
    "content": "# A \"Hello, World\" Docker image used in automated tests for the docker.Build command.\nFROM ubuntu:20.04 as with-secrets\n\nRUN --mount=type=secret,id=github-token echo \"$(cat /run/secrets/github-token)\" > text.txt\nCOPY ./bash_script.sh /usr/local/bin/bash_script.sh"
  },
  {
    "path": "test/fixtures/docker-compose-with-buildkit/bash_script.sh",
    "content": "#!/bin/bash\nset -e\n\ncat text.txt\n"
  },
  {
    "path": "test/fixtures/docker-compose-with-buildkit/docker-compose.yml",
    "content": "services:\n  test-docker-image:\n    build:\n      context: .\n      secrets:\n        - github-token\n    entrypoint: bash_script.sh\n\nsecrets:\n  github-token:\n    environment: GITHUB_OAUTH_TOKEN"
  },
  {
    "path": "test/fixtures/docker-compose-with-custom-project-name/docker-compose.yml",
    "content": "services:\n  test-docker-image:\n    image: busybox\n"
  },
  {
    "path": "test/fixtures/docker-with-buildkit/Dockerfile",
    "content": "# A \"Hello, World\" Docker image used in automated tests for the docker.Build command.\nFROM ubuntu:20.04 as with-secrets\n\nRUN --mount=type=secret,id=github-token echo \"$(cat /run/secrets/github-token)\" > text.txt\nCMD [\"cat\", \"text.txt\"]"
  },
  {
    "path": "test/fixtures/helm/keda-values.yaml",
    "content": "metricsServer:\n  replicaCount: 3\noperator:\n  name: keda-operator\n  replicaCount: 3\npodAnnotations:\n  keda:\n    sidecar.istio.io/inject: \"false\"\n  metricsAdapter:\n    sidecar.istio.io/inject: \"false\"\npodDisruptionBudget:\n  metricServer:\n    minAvailable: 1\n  operator:\n    minAvailable: 1\nresources:\n  metricServer:\n    limits:\n      cpu: 100m\n      memory: 1234Mi\n    requests:\n      cpu: 50m\n      memory: 128Mi\n  operator:\n    limits:\n      cpu: 100m\n      memory: 1111Mi\n    requests:\n      cpu: 50m\n      memory: 888Mi\n"
  },
  {
    "path": "test/fixtures/terraform-backend/backend.hcl",
    "content": "path=\"backend.tfstate\""
  },
  {
    "path": "test/fixtures/terraform-backend/main.tf",
    "content": "terraform {\n  backend \"local\" {}\n}\n\noutput \"test\" {\n  value = \"Hello, World\"\n}\n"
  },
  {
    "path": "test/fixtures/terraform-basic-configuration/main.tf",
    "content": "variable \"cnt\" {}\n\nresource \"null_resource\" \"test\" {\n  count = var.cnt\n}\n"
  },
  {
    "path": "test/fixtures/terraform-no-error/main.tf",
    "content": "output \"test\" {\n  value = \"Hello, World\"\n}"
  },
  {
    "path": "test/fixtures/terraform-not-idempotent/main.tf",
    "content": "resource \"null_resource\" \"test\" {\n  triggers = {\n    time = timestamp()\n  }\n}\n"
  },
  {
    "path": "test/fixtures/terraform-null/main.tf",
    "content": "variable \"foo\" {\n  type = object({\n    nullable_string    = string\n    nonnullable_string = string\n  })\n}\n\noutput \"foo\" {\n  value = var.foo\n}\n\noutput \"bar\" {\n  value = var.foo.nullable_string == null ? \"I AM NULL\" : null\n}\n"
  },
  {
    "path": "test/fixtures/terraform-output/output.tf",
    "content": "output \"bool\" {\n  value = true\n}\n\noutput \"string\" {\n  value = \"This is a string.\"\n}\n\noutput \"number\" {\n  value = 3.14\n}\n\noutput \"number1\" {\n  value = 3\n}\n\noutput \"unicode_string\" {\n  value = \"söme chäräcter\"\n}\n"
  },
  {
    "path": "test/fixtures/terraform-output-all/output.tf",
    "content": "output \"stars\" {\n  value = [\n    \"Sirius\",\n    \"Rigel\",\n    \"Betelgeuse\",\n  ]\n}\n\noutput \"our_star\" {\n  value = \"Sun\"\n}\n\noutput \"constellations\" {\n  value = {\n    Gemini  = \"Pollux\",\n    Scorpio = \"Antares\",\n    Taurus  = \"Aldebaran\",\n    Virgo   = \"Spica\",\n  }\n}\n\noutput \"magnitudes\" {\n  value = {\n    Sirius  = -1.46,\n    Canopus = -0.72,\n    Antares = 0.96,\n  }\n}\n"
  },
  {
    "path": "test/fixtures/terraform-output-list/output.tf",
    "content": "output \"giant_steps\" {\n  value = [\n    \"John Coltrane\",\n    \"Tommy Flanagan\",\n    \"Paul Chambers\",\n    \"Art Taylor\",\n  ]\n}\n\noutput \"not_a_list\" {\n  value = \"This is not a list.\"\n}\n"
  },
  {
    "path": "test/fixtures/terraform-output-listofobjects/output.tf",
    "content": "output \"list_of_maps\" {\n  value = [\n    {\n      one   = 1\n      two   = \"two\"\n      three = \"three\"\n      more = {\n        four = 4\n        five = \"five\"\n      }\n    },\n    {\n      one   = \"one\"\n      two   = 2\n      three = 3\n      more = [{\n        four = 4\n        five = \"five\"\n      }]\n    },\n    {\n      one   = \"one\"\n      two   = 2\n      three = 3\n      more  = [\"one\", 2.0, 3.4, [\"one\", 2.0, 3.4], { \"one\" : 2.0, \"three\" : 3.4 }]\n    }\n  ]\n}\n\noutput \"not_list_of_maps\" {\n  value = \"Just a string\"\n}\n"
  },
  {
    "path": "test/fixtures/terraform-output-map/output.tf",
    "content": "output \"mogwai\" {\n  value = {\n    guitar_1 = \"Stuart Braithwaite\"\n    guitar_2 = \"Barry Burns\"\n    bass     = \"Dominic Aitchison\"\n    drums    = \"Martin Bulloch\"\n  }\n}\n\noutput \"not_a_map\" {\n  value = \"This is not a map.\"\n}\n\noutput \"not_a_map_unicode\" {\n  value = \"söme chäräcter\"\n}\n"
  },
  {
    "path": "test/fixtures/terraform-output-mapofobjects/output.tf",
    "content": "output \"map_of_objects\" {\n  value = {\n    somebool  = true\n    somefloat = 1.1\n    one       = 1\n    two       = \"two\"\n    three     = \"three\"\n    nest = {\n      four = 4\n      five = \"five\"\n    }\n    nest_list = [\n      {\n        six   = 6\n        seven = \"seven\"\n      },\n    ]\n  }\n}\n\noutput \"not_map_of_objects\" {\n  value = \"Just a string\"\n}\n"
  },
  {
    "path": "test/fixtures/terraform-output-struct/output.tf",
    "content": "output \"object\" {\n  value = {\n    somebool   = true\n    somefloat  = 0.1\n    someint    = 1\n    somestring = \"two\"\n    somemap = {\n      three = 3\n      four  = \"four\"\n    },\n    listmaps = [\n      {\n        five = 5\n        six  = \"six\"\n      },\n    ]\n    liststrings = [\n      \"seven\",\n      \"eight\",\n      \"nine\",\n    ]\n  }\n}\n\noutput \"list_of_objects\" {\n  value = [\n    {\n      somebool   = true\n      somefloat  = 0.1\n      someint    = 1\n      somestring = \"two\"\n    },\n    {\n      somebool   = false\n      somefloat  = 0.3\n      someint    = 4\n      somestring = \"five\"\n    }\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/terraform-parallelism/main.tf",
    "content": "# This resource just waits for 5 seconds. If we run it with enough parallelism, the whole module should apply in about\n# 5 seconds. If we set parallelism to 1, it should take at least 25 seconds.\nresource \"null_resource\" \"wait\" {\n  count = 5\n\n  triggers = {\n    run_always = timestamp()\n  }\n\n  provisioner \"local-exec\" {\n    command = \"sleep 5\"\n  }\n}"
  },
  {
    "path": "test/fixtures/terraform-validation-valid/main.tf",
    "content": "# This is a test resource that echoes the message specified by var.message\nresource \"null_resource\" \"greet\" {\n  count = 5\n\n  triggers = {\n    run_always = timestamp()\n  }\n\n  provisioner \"local-exec\" {\n    command = \"echo ${var.message}\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/terraform-validation-valid/outputs.tf",
    "content": "output \"message\" {\n  value = var.message\n}\n"
  },
  {
    "path": "test/fixtures/terraform-validation-valid/vars.tf",
    "content": "variable \"message\" {\n  type    = string\n  default = \"Hello, World\"\n}\n"
  },
  {
    "path": "test/fixtures/terraform-with-error/main.tf",
    "content": "resource \"null_resource\" \"fail_on_first_run\" {\n  provisioner \"local-exec\" {\n    command     = \"if [[ -f terraform.tfstate.backup ]]; then echo 'This is not the first run, so exiting successfully' && exit 0; else echo 'This is the first run, exiting with an error' && exit 1; fi\"\n    interpreter = [\"/bin/bash\", \"-c\"]\n  }\n}"
  },
  {
    "path": "test/fixtures/terraform-with-plan-error/main.tf",
    "content": "output \"test\" {\n  value = var.test\n}\n"
  },
  {
    "path": "test/fixtures/terraform-with-warning/main.tf",
    "content": "terraform {\n  required_providers {\n    validation = {\n      source  = \"tlkamp/validation\"\n      version = \"1.1.1\"\n    }\n    null = {\n      source  = \"hashicorp/null\"\n      version = \"3.2.2\"\n    }\n  }\n}\n\n# this data source will produce warning when `condition` is evaluated to `true`\ndata \"validation_warning\" \"warn\" {\n  for_each  = toset([for i in range(10) : format(\"%02d\", i)])\n  condition = true\n  summary   = \"lorem ipsum ${each.value}\"\n  details   = \"lorem ipsum dolor sit amet\"\n}\n\nresource \"null_resource\" \"empty\" {}\n"
  },
  {
    "path": "test/fixtures/terraform-workspace/main.tf",
    "content": "output \"test\" {\n  value = \"Hello, ${terraform.workspace}\"\n}"
  },
  {
    "path": "test/gcp/packer_gcp_basic_example_test.go",
    "content": "//go:build gcp\n// +build gcp\n\n// NOTE: We use build tags to differentiate GCP testing for better isolation and parallelism when executing our tests.\n\npackage test\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/gruntwork-io/terratest/modules/gcp\"\n\t\"github.com/gruntwork-io/terratest/modules/packer\"\n)\n\n// Occasionally, a Packer build may fail due to intermittent issues (e.g., brief network outage or EC2 issue). We try\n// to make our tests resilient to that by specifying those known common errors here and telling our builds to retry if\n// they hit those errors.\nvar DefaultRetryablePackerErrors = map[string]string{\n\t\"Script disconnected unexpectedly\":                                                 \"Occasionally, Packer seems to lose connectivity to AWS, perhaps due to a brief network outage\",\n\t\"can not open /var/lib/apt/lists/archive.ubuntu.com_ubuntu_dists_xenial_InRelease\": \"Occasionally, apt-get fails on ubuntu to update the cache\",\n}\nvar DefaultTimeBetweenPackerRetries = 15 * time.Second\n\n// Regions that support running f1-micro instances\nvar RegionsThatSupportF1Micro = []string{\"us-central1\", \"us-east1\", \"us-west1\", \"europe-west1\"}\n\n// Zones that support running f1-micro instances\nvar ZonesThatSupportF1Micro = []string{\"us-central1-a\", \"us-east1-b\", \"us-west1-a\", \"europe-north1-a\", \"europe-west1-b\", \"europe-central2-a\"}\n\nconst DefaultMaxPackerRetries = 3\n\n// An example of how to test the Packer template in examples/packer-basic-example using Terratest.\nfunc TestPackerGCPBasicExample(t *testing.T) {\n\tt.Parallel()\n\n\t// Get the Project Id to use\n\tprojectID := gcp.GetGoogleProjectIDFromEnvVar(t)\n\n\t// Pick a random GCP zone to test in. This helps ensure your code works in all regions.\n\tzone := gcp.GetRandomZone(t, projectID, ZonesThatSupportF1Micro, nil, nil)\n\n\tpackerOptions := &packer.Options{\n\t\t// The path to where the Packer template is located\n\t\tTemplate: \"../../examples/packer-basic-example/build-gcp.pkr.hcl\",\n\n\t\t// Variables to pass to our Packer build using -var options\n\t\tVars: map[string]string{\n\t\t\t\"gcp_project_id\": projectID,\n\t\t\t\"gcp_zone\":       zone,\n\t\t},\n\n\t\t// Only build the Google Compute Image\n\t\tOnly: \"googlecompute.ubuntu-bionic\",\n\n\t\t// Configure retries for intermittent errors\n\t\tRetryableErrors:    DefaultRetryablePackerErrors,\n\t\tTimeBetweenRetries: DefaultTimeBetweenPackerRetries,\n\t\tMaxRetries:         DefaultMaxPackerRetries,\n\t}\n\n\t// Make sure the Packer build completes successfully\n\timageName := packer.BuildArtifact(t, packerOptions)\n\n\t// Delete the Image after we're done\n\timage := gcp.FetchImage(t, projectID, imageName)\n\tdefer image.DeleteImage(t)\n}\n"
  },
  {
    "path": "test/gcp/terraform_gcp_example_test.go",
    "content": "//go:build gcp\n// +build gcp\n\n// NOTE: We use build tags to differentiate GCP testing for better isolation and parallelism when executing our tests.\n\npackage test\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/gruntwork-io/terratest/modules/gcp\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/gruntwork-io/terratest/modules/retry\"\n\t\"github.com/gruntwork-io/terratest/modules/ssh\"\n\t\"github.com/gruntwork-io/terratest/modules/terraform\"\n\ttest_structure \"github.com/gruntwork-io/terratest/modules/test-structure\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestTerraformGcpExample(t *testing.T) {\n\tt.Parallel()\n\n\texampleDir := test_structure.CopyTerraformFolderToTemp(t, \"../../\", \"examples/terraform-gcp-example\")\n\n\t// Get the Project Id to use\n\tprojectId := gcp.GetGoogleProjectIDFromEnvVar(t)\n\n\t// Create all resources in the following zone\n\tzone := \"us-east1-b\"\n\n\t// Give the example bucket a unique name so we can distinguish it from any other bucket in your GCP account\n\texpectedBucketName := fmt.Sprintf(\"terratest-gcp-example-%s\", strings.ToLower(random.UniqueId()))\n\n\t// Also give the example instance a unique name\n\texpectedInstanceName := fmt.Sprintf(\"terratest-gcp-example-%s\", strings.ToLower(random.UniqueId()))\n\n\t// website::tag::1::Configure Terraform setting path to Terraform code, bucket name, and instance name. Construct\n\t// the terraform options with default retryable errors to handle the most common retryable errors in terraform\n\t// testing.\n\tterraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{\n\t\t// The path to where our Terraform code is located\n\t\tTerraformDir: exampleDir,\n\n\t\t// Variables to pass to our Terraform code using -var options\n\t\tVars: map[string]interface{}{\n\t\t\t\"gcp_project_id\": projectId,\n\t\t\t\"zone\":           zone,\n\t\t\t\"instance_name\":  expectedInstanceName,\n\t\t\t\"bucket_name\":    expectedBucketName,\n\t\t},\n\t})\n\n\t// website::tag::5::At the end of the test, run `terraform destroy` to clean up any resources that were created\n\tdefer terraform.Destroy(t, terraformOptions)\n\n\t// website::tag::2::This will run `terraform init` and `terraform apply` and fail the test if there are any errors\n\tterraform.InitAndApply(t, terraformOptions)\n\n\t// Run `terraform output` to get the value of some of the output variables\n\tbucketURL := terraform.Output(t, terraformOptions, \"bucket_url\")\n\tinstanceName := terraform.Output(t, terraformOptions, \"instance_name\")\n\n\t// website::tag::3::Verify that the new bucket url matches the expected url\n\texpectedURL := fmt.Sprintf(\"gs://%s\", expectedBucketName)\n\tassert.Equal(t, expectedURL, bucketURL)\n\n\t// Verify that the Storage Bucket exists\n\tgcp.AssertStorageBucketExists(t, expectedBucketName)\n\n\t// Add a tag to the Compute Instance\n\tinstance := gcp.FetchInstance(t, projectId, instanceName)\n\tinstance.SetLabels(t, map[string]string{\"testing\": \"testing-tag-value2\"})\n\n\t// Check for the labels within a retry loop as it can sometimes take a while for the\n\t// changes to propagate.\n\tmaxRetries := 12\n\ttimeBetweenRetries := 5 * time.Second\n\texpectedText := \"testing-tag-value2\"\n\n\t// website::tag::4::Check if the GCP instance contains a given tag.\n\tretry.DoWithRetry(t, fmt.Sprintf(\"Checking Instance %s for labels\", instanceName), maxRetries, timeBetweenRetries, func() (string, error) {\n\t\t// Look up the tags for the given Instance ID\n\t\tinstance := gcp.FetchInstance(t, projectId, instanceName)\n\t\tinstanceLabels := instance.GetLabels(t)\n\n\t\ttestingTag, containsTestingTag := instanceLabels[\"testing\"]\n\t\tactualText := strings.TrimSpace(testingTag)\n\t\tif !containsTestingTag {\n\t\t\treturn \"\", fmt.Errorf(\"Expected the tag 'testing' to exist\")\n\t\t}\n\n\t\tif actualText != expectedText {\n\t\t\treturn \"\", fmt.Errorf(\"Expected GetLabelsForComputeInstanceE to return '%s' but got '%s'\", expectedText, actualText)\n\t\t}\n\n\t\treturn \"\", nil\n\t})\n}\n\n// Create a Compute Instance, and attempt to SSH in and run a command.\nfunc TestSshAccessToComputeInstance(t *testing.T) {\n\tt.Parallel()\n\n\texampleDir := test_structure.CopyTerraformFolderToTemp(t, \"../../\", \"examples/terraform-gcp-example\")\n\n\t// Setup values for our Terraform apply\n\tprojectID := gcp.GetGoogleProjectIDFromEnvVar(t)\n\trandomValidGcpName := gcp.RandomValidGcpName()\n\tzone := gcp.GetRandomZone(t, projectID, ZonesThatSupportF1Micro, nil, nil)\n\n\tterraformOptions := &terraform.Options{\n\t\t// The path to where our Terraform code is located\n\t\tTerraformDir: exampleDir,\n\n\t\t// Variables to pass to our Terraform code using -var options\n\t\tVars: map[string]interface{}{\n\t\t\t\"gcp_project_id\": projectID,\n\t\t\t\"instance_name\":  randomValidGcpName,\n\t\t\t\"bucket_name\":    randomValidGcpName,\n\t\t\t\"zone\":           zone,\n\t\t},\n\t}\n\n\t// At the end of the test, run `terraform destroy` to clean up any resources that were created\n\tdefer terraform.Destroy(t, terraformOptions)\n\n\t// This will run `terraform init` and `terraform apply` and fail the test if there are any errors\n\tterraform.InitAndApply(t, terraformOptions)\n\n\t// Run `terraform output` to get the value of an output variable\n\tpublicIp := terraform.Output(t, terraformOptions, \"public_ip\")\n\n\t// Attempt to SSH and execute the command\n\tinstance := gcp.FetchInstance(t, projectID, randomValidGcpName)\n\n\tsampleText := \"Hello World\"\n\tsshUsername := \"terratest\"\n\n\tkeyPair := ssh.GenerateRSAKeyPair(t, 2048)\n\tinstance.AddSshKey(t, sshUsername, keyPair.PublicKey)\n\n\thost := ssh.Host{\n\t\tHostname:    publicIp,\n\t\tSshKeyPair:  keyPair,\n\t\tSshUserName: sshUsername,\n\t}\n\n\tmaxRetries := 20\n\tsleepBetweenRetries := 3 * time.Second\n\n\tretry.DoWithRetry(t, \"Attempting to SSH\", maxRetries, sleepBetweenRetries, func() (string, error) {\n\t\toutput, err := ssh.CheckSshCommandE(t, host, fmt.Sprintf(\"echo '%s'\", sampleText))\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\tif strings.TrimSpace(sampleText) != strings.TrimSpace(output) {\n\t\t\treturn \"\", fmt.Errorf(\"Expected: %s. Got: %s\\n\", sampleText, output)\n\t\t}\n\n\t\treturn \"\", nil\n\t})\n}\n"
  },
  {
    "path": "test/gcp/terraform_gcp_hello_world_example_test.go",
    "content": "//go:build gcp\n// +build gcp\n\n// NOTE: We use build tags to differentiate GCP testing for better isolation and parallelism when executing our tests.\n\npackage test\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/gcp\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/gruntwork-io/terratest/modules/terraform\"\n)\n\nfunc TestTerraformGcpHelloWorldExample(t *testing.T) {\n\tt.Parallel()\n\n\t// website::tag::1:: Get the Project Id to use\n\tprojectId := gcp.GetGoogleProjectIDFromEnvVar(t)\n\n\t// website::tag::2:: Give the example instance a unique name\n\tinstanceName := fmt.Sprintf(\"gcp-hello-world-example-%s\", strings.ToLower(random.UniqueId()))\n\n\t// website::tag::6:: Construct the terraform options with default retryable errors to handle the most common\n\t// retryable errors in terraform testing.\n\tterraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{\n\t\t// website::tag::3:: The path to where our Terraform code is located\n\t\tTerraformDir: \"../../examples/terraform-gcp-hello-world-example\",\n\n\t\t// website::tag::4:: Variables to pass to our Terraform code using -var options\n\t\tVars: map[string]interface{}{\n\t\t\t\"instance_name\": instanceName,\n\t\t},\n\n\t\t// website::tag::5:: Variables to pass to our Terraform code using TF_VAR_xxx environment variables\n\t\tEnvVars: map[string]string{\n\t\t\t\"GOOGLE_CLOUD_PROJECT\": projectId,\n\t\t},\n\t})\n\n\t// website::tag::8:: At the end of the test, run `terraform destroy` to clean up any resources that were created\n\tdefer terraform.Destroy(t, terraformOptions)\n\n\t// website::tag::7:: Run `terraform init` and `terraform apply`. Fail the test if there are any errors.\n\tterraform.InitAndApply(t, terraformOptions)\n}\n"
  },
  {
    "path": "test/gcp/terraform_gcp_ig_example_test.go",
    "content": "//go:build gcp\n// +build gcp\n\n// NOTE: We use build tags to differentiate GCP testing for better isolation and parallelism when executing our tests.\n\npackage test\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/gruntwork-io/terratest/modules/gcp\"\n\t\"github.com/gruntwork-io/terratest/modules/retry\"\n\t\"github.com/gruntwork-io/terratest/modules/terraform\"\n\ttest_structure \"github.com/gruntwork-io/terratest/modules/test-structure\"\n)\n\nfunc TestTerraformGcpInstanceGroupExample(t *testing.T) {\n\tt.Parallel()\n\n\texampleDir := test_structure.CopyTerraformFolderToTemp(t, \"../../\", \"examples/terraform-gcp-ig-example\")\n\n\t// Setup values for our Terraform apply\n\tprojectId := gcp.GetGoogleProjectIDFromEnvVar(t)\n\n\tregion := gcp.GetRandomRegion(t, projectId, RegionsThatSupportF1Micro, nil)\n\n\trandomValidGcpName := gcp.RandomValidGcpName()\n\tclusterSize := 3\n\n\tterraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{\n\t\t// The path to where our Terraform code instances located\n\t\tTerraformDir: exampleDir,\n\n\t\t// Variables to pass to our Terraform code using -var options\n\t\tVars: map[string]interface{}{\n\t\t\t\"gcp_project_id\": projectId,\n\t\t\t\"gcp_region\":     region,\n\t\t\t\"cluster_name\":   randomValidGcpName,\n\t\t},\n\t})\n\n\t// At the end of the test, run `terraform destroy` to clean up any resources that were created\n\tdefer terraform.Destroy(t, terraformOptions)\n\n\t// This will run `terraform init` and `terraform apply` and fail the test if there are any errors\n\tterraform.InitAndApply(t, terraformOptions)\n\n\tinstanceGroupName := terraform.Output(t, terraformOptions, \"instance_group_name\")\n\n\tinstanceGroup := gcp.FetchRegionalInstanceGroup(t, projectId, region, instanceGroupName)\n\n\t// Validate that GetInstances() returns a non-zero number of Instances\n\tmaxRetries := 100\n\tsleepBetweenRetries := 3 * time.Second\n\n\tretry.DoWithRetry(t, \"Attempting to fetch Instances from Instance Group\", maxRetries, sleepBetweenRetries, func() (string, error) {\n\t\tinstances, err := instanceGroup.GetInstancesE(t, projectId)\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"Failed to get Instances: %s\", err)\n\t\t}\n\n\t\tif len(instances) != clusterSize {\n\t\t\treturn \"\", fmt.Errorf(\"Expected to find exactly %d Compute Instances in Instance Group but found %d.\", clusterSize, len(instances))\n\t\t}\n\n\t\treturn \"\", nil\n\t})\n\n\t// Validate that we get the right number of IP addresses\n\tretry.DoWithRetry(t, \"Attempting to fetch Public IP addresses from Instance Group\", maxRetries, sleepBetweenRetries, func() (string, error) {\n\t\tips, err := instanceGroup.GetPublicIpsE(t, projectId)\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"Failed to get public IPs from Instance Group\")\n\t\t}\n\n\t\tif len(ips) != clusterSize {\n\t\t\treturn \"\", fmt.Errorf(\"Expected to get exactly %d public IP addresses but found %d.\", clusterSize, len(ips))\n\t\t}\n\n\t\treturn \"\", nil\n\t})\n}\n"
  },
  {
    "path": "test/helm_basic_example_integration_test.go",
    "content": "//go:build kubeall || helm\n// +build kubeall helm\n\n// NOTE: we have build tags to differentiate kubernetes tests from non-kubernetes tests, and further differentiate helm\n// tests. This is done because minikube is heavy and can interfere with docker related tests in terratest. Similarly,\n// helm can overload the minikube system and thus interfere with the other kubernetes tests. To avoid overloading the\n// system, we run the kubernetes tests and helm tests separately from the others.\n\npackage test\n\nimport (\n\t\"crypto/tls\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/gruntwork-io/terratest/modules/helm\"\n\thttp_helper \"github.com/gruntwork-io/terratest/modules/http-helper\"\n\t\"github.com/gruntwork-io/terratest/modules/k8s\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n)\n\n// This file contains examples of how to use terratest to test helm charts by deploying the chart and verifying the\n// deployment by hitting the service endpoint.\nfunc TestHelmBasicExampleDeployment(t *testing.T) {\n\tt.Parallel()\n\n\t// Path to the helm chart we will test\n\thelmChartPath, err := filepath.Abs(\"../examples/helm-basic-example\")\n\trequire.NoError(t, err)\n\n\t// To ensure we can reuse the resource config on the same cluster to test different scenarios, we setup a unique\n\t// namespace for the resources for this test.\n\t// Note that namespaces must be lowercase.\n\tnamespaceName := fmt.Sprintf(\"helm-basic-example-%s\", strings.ToLower(random.UniqueId()))\n\n\t// Setup the kubectl config and context. Here we choose to use the defaults, which is:\n\t// - HOME/.kube/config for the kubectl config file\n\t// - Current context of the kubectl config file\n\tkubectlOptions := k8s.NewKubectlOptions(\"\", \"\", namespaceName)\n\n\tk8s.CreateNamespace(t, kubectlOptions, namespaceName)\n\t// ... and make sure to delete the namespace at the end of the test\n\tdefer k8s.DeleteNamespace(t, kubectlOptions, namespaceName)\n\n\t// Setup the args. For this test, we will set the following input values:\n\t// - containerImageRepo=nginx\n\t// - containerImageTag=1.15.8\n\toptions := &helm.Options{\n\t\tKubectlOptions: kubectlOptions,\n\t\tSetValues: map[string]string{\n\t\t\t\"containerImageRepo\": \"nginx\",\n\t\t\t\"containerImageTag\":  \"1.15.8\",\n\t\t},\n\t\tExtraArgs: map[string][]string{\n\t\t\t\"install\": []string{\"--wait\", \"--timeout\", \"1m30s\"},\n\t\t},\n\t}\n\n\t// We generate a unique release name so that we can refer to after deployment.\n\t// By doing so, we can schedule the delete call here so that at the end of the test, we run\n\t// `helm delete RELEASE_NAME` to clean up any resources that were created.\n\treleaseName := fmt.Sprintf(\n\t\t\"nginx-service-%s\",\n\t\tstrings.ToLower(random.UniqueId()),\n\t)\n\tdefer helm.Delete(t, options, releaseName, true)\n\n\t// Deploy the chart using `helm install`. Note that we use the version without `E`, since we want to assert the\n\t// install succeeds without any errors.\n\thelm.Install(t, options, helmChartPath, releaseName)\n\n\t// Now let's verify the deployment. We will get the service endpoint and try to access it.\n\n\t// First we need to get the service name. We will use domain knowledge of the chart here, where the name is\n\t// RELEASE_NAME-CHART_NAME\n\tserviceName := fmt.Sprintf(\"%s-helm-basic-example\", releaseName)\n\n\t// Next we wait until the service is available. This will wait up to 10 seconds for the service to become available,\n\t// to ensure that we can access it.\n\tk8s.WaitUntilServiceAvailable(t, kubectlOptions, serviceName, 10, 1*time.Second)\n\n\t// Now we open a tunnel to port forward service port to localhost\n\ttunnel := k8s.NewTunnel(\n\t\tkubectlOptions, k8s.ResourceTypeService, serviceName, 0, 80)\n\tdefer tunnel.Close()\n\ttunnel.ForwardPort(t)\n\t// Get endpoint\n\tendpoint := tunnel.Endpoint()\n\t// Setup a TLS configuration to submit with the helper, a blank struct is acceptable\n\ttlsConfig := tls.Config{}\n\n\t// Test the endpoint for up to 5 minutes. This will only fail if we timeout waiting for the service to return a 200\n\t// response.\n\thttp_helper.HttpGetWithRetryWithCustomValidation(\n\t\tt,\n\t\tfmt.Sprintf(\"http://%s\", endpoint),\n\t\t&tlsConfig,\n\t\t30,\n\t\t10*time.Second,\n\t\tfunc(statusCode int, body string) bool {\n\t\t\treturn statusCode == http.StatusOK\n\t\t},\n\t)\n}\n"
  },
  {
    "path": "test/helm_basic_example_template_test.go",
    "content": "//go:build kubeall || helm\n// +build kubeall helm\n\n// **NOTE**: we have build tags to differentiate kubernetes tests from non-kubernetes tests, and further differentiate helm\n// tests. This is done because minikube is heavy and can interfere with docker related tests in terratest. Similarly, helm\n// can overload the minikube system and thus interfere with the other kubernetes tests. Specifically, many of the tests\n// start to fail with `connection refused` errors from `minikube`. To avoid overloading the system, we run the kubernetes\n// tests and helm tests separately from the others. This may not be necessary if you have a sufficiently powerful machine.\n// We recommend at least 4 cores and 16GB of RAM if you want to run all the tests together.\n\npackage test\n\nimport (\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n\tappsv1 \"k8s.io/api/apps/v1\"\n\n\t\"github.com/gruntwork-io/terratest/modules/helm\"\n\t\"github.com/gruntwork-io/terratest/modules/k8s\"\n\t\"github.com/gruntwork-io/terratest/modules/logger\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n)\n\n// This file contains examples of how to use terratest to test helm chart template logic by rendering the templates\n// using `helm template`, and then reading in the rendered templates.\n// There are two tests:\n// - TestHelmBasicExampleTemplateRenderedDeployment: An example of how to read in the rendered object and check the\n//   computed values.\n// - TestHelmBasicExampleTemplateRequiredTemplateArgs: An example of how to check that the required args are indeed\n//   required for the template to render.\n\n// An example of how to verify the rendered template object of a Helm Chart given various inputs.\nfunc TestHelmBasicExampleTemplateRenderedDeployment(t *testing.T) {\n\tt.Parallel()\n\n\t// Path to the helm chart we will test\n\thelmChartPath, err := filepath.Abs(\"../examples/helm-basic-example\")\n\treleaseName := \"helm-basic\"\n\trequire.NoError(t, err)\n\n\t// Since we aren't deploying any resources, there is no need to setup kubectl authentication or helm home.\n\n\t// Set up the namespace; confirm that the template renders the expected value for the namespace.\n\tnamespaceName := \"medieval-\" + strings.ToLower(random.UniqueId())\n\tlogger.Logf(t, \"Namespace: %s\\n\", namespaceName)\n\n\t// Setup the args. For this test, we will set the following input values:\n\t// - containerImageRepo=nginx\n\t// - containerImageTag=1.15.8\n\toptions := &helm.Options{\n\t\tSetValues: map[string]string{\n\t\t\t\"containerImageRepo\": \"nginx\",\n\t\t\t\"containerImageTag\":  \"1.15.8\",\n\t\t},\n\t\tKubectlOptions: k8s.NewKubectlOptions(\"\", \"\", namespaceName),\n\t}\n\n\t// Run RenderTemplate to render the template and capture the output. Note that we use the version without `E`, since\n\t// we want to assert that the template renders without any errors.\n\t// Additionally, although we know there is only one yaml file in the template, we deliberately path a templateFiles\n\t// arg to demonstrate how to select individual templates to render.\n\toutput := helm.RenderTemplate(t, options, helmChartPath, releaseName, []string{\"templates/deployment.yaml\"})\n\n\t// Now we use kubernetes/client-go library to render the template output into the Deployment struct. This will\n\t// ensure the Deployment resource is rendered correctly.\n\tvar deployment appsv1.Deployment\n\thelm.UnmarshalK8SYaml(t, output, &deployment)\n\n\t// Verify the namespace matches the expected supplied namespace.\n\trequire.Equal(t, namespaceName, deployment.Namespace)\n\n\t// Finally, we verify the deployment pod template spec is set to the expected container image value\n\texpectedContainerImage := \"nginx:1.15.8\"\n\tdeploymentContainers := deployment.Spec.Template.Spec.Containers\n\trequire.Equal(t, len(deploymentContainers), 1)\n\trequire.Equal(t, deploymentContainers[0].Image, expectedContainerImage)\n}\n\n// An example of how to verify required values for a helm chart.\nfunc TestHelmBasicExampleTemplateRequiredTemplateArgs(t *testing.T) {\n\tt.Parallel()\n\n\t// Path to the helm chart we will test\n\thelmChartPath, err := filepath.Abs(\"../examples/helm-basic-example\")\n\treleaseName := \"helm-basic\"\n\trequire.NoError(t, err)\n\n\t// Since we aren't deploying any resources, there is no need to setup kubectl authentication, helm home, or\n\t// namespaces\n\n\t// Here, we use a table driven test to iterate through all the required values as subtests. You can learn more about\n\t// go subtests here: https://blog.golang.org/subtests\n\t// The struct captures the inputs that we will pass to helm template and a human friendly name so we can identify it\n\t// in the test output. In this case, each test case will be a complete values input except for one of the required\n\t// values missing, to test that neglecting a required value will cause the template rendering to fail.\n\ttestCases := []struct {\n\t\tname   string\n\t\tvalues map[string]string\n\t}{\n\t\t{\n\t\t\t\"MissingContainerImageRepo\",\n\t\t\tmap[string]string{\"containerImageTag\": \"1.15.8\"},\n\t\t},\n\t\t{\n\t\t\t\"MissingContainerImageTag\",\n\t\t\tmap[string]string{\"containerImageRepo\": \"nginx\"},\n\t\t},\n\t}\n\n\t// Now we iterate over each test case and spawn a sub test\n\tfor _, testCase := range testCases {\n\t\t// Here, we capture the range variable and force it into the scope of this block. If we don't do this, when the\n\t\t// subtest switches contexts (because of t.Parallel), the testCase value will have been updated by the for loop\n\t\t// and will be the next testCase!\n\t\ttestCase := testCase\n\n\t\t// The actual sub test spawning. We name the sub test using the human friendly name. Note that we name the sub\n\t\t// test T struct to subT to make it clear which T struct corresponds to which test. However, in most cases you\n\t\t// will not reference the main test T so you can name it the same.\n\t\tt.Run(testCase.name, func(subT *testing.T) {\n\t\t\tsubT.Parallel()\n\n\t\t\t// Now we try rendering the template, but verify we get an error\n\t\t\toptions := &helm.Options{SetValues: testCase.values}\n\t\t\t_, err := helm.RenderTemplateE(t, options, helmChartPath, releaseName, []string{})\n\t\t\trequire.Error(t, err)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "test/helm_dependency_example_template_test.go",
    "content": "//go:build kubeall || helm\n// +build kubeall helm\n\n// **NOTE**: we have build tags to differentiate kubernetes tests from non-kubernetes tests, and further differentiate helm\n// tests. This is done because minikube is heavy and can interfere with docker related tests in terratest. Similarly, helm\n// can overload the minikube system and thus interfere with the other kubernetes tests. Specifically, many of the tests\n// start to fail with `connection refused` errors from `minikube`. To avoid overloading the system, we run the kubernetes\n// tests and helm tests separately from the others. This may not be necessary if you have a sufficiently powerful machine.\n// We recommend at least 4 cores and 16GB of RAM if you want to run all the tests together.\n\npackage test\n\nimport (\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n\tappsv1 \"k8s.io/api/apps/v1\"\n\n\t\"github.com/gruntwork-io/terratest/modules/helm\"\n\t\"github.com/gruntwork-io/terratest/modules/k8s\"\n\t\"github.com/gruntwork-io/terratest/modules/logger\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n)\n\n// This file contains examples of how to use terratest to test helm chart template logic by rendering the templates\n// using `helm template`, and then reading in the rendered templates.\n// There are two tests:\n// - TestHelmBasicExampleTemplateRenderedDeployment: An example of how to read in the rendered object and check the\n//   computed values.\n// - TestHelmBasicExampleTemplateRequiredTemplateArgs: An example of how to check that the required args are indeed\n//   required for the template to render.\n\n// An example of how to verify the rendered template object of a Helm Chart given various inputs.\nfunc TestHelmDependencyExampleTemplateRenderedDeployment(t *testing.T) {\n\tt.Parallel()\n\n\t// Path to the helm chart we will test\n\thelmChartPath, err := filepath.Abs(\"../examples/helm-dependency-example\")\n\treleaseName := \"helm-dependency\"\n\trequire.NoError(t, err)\n\n\t// Since we aren't deploying any resources, there is no need to setup kubectl authentication or helm home.\n\n\t// Set up the namespace; confirm that the template renders the expected value for the namespace.\n\tnamespaceName := \"medieval-\" + strings.ToLower(random.UniqueId())\n\tlogger.Logf(t, \"Namespace: %s\\n\", namespaceName)\n\n\t// Setup the args. For this test, we will set the following input values:\n\t// - containerImageRepo=nginx\n\t// - containerImageTag=1.15.8\n\toptions := &helm.Options{\n\t\tSetValues: map[string]string{\n\t\t\t\"containerImageRepo\":       \"nginx\",\n\t\t\t\"containerImageTag\":        \"1.15.8\",\n\t\t\t\"basic.containerImageRepo\": \"nginx\",\n\t\t\t\"basic.containerImageTag\":  \"1.15.8\",\n\t\t},\n\t\tKubectlOptions:    k8s.NewKubectlOptions(\"\", \"\", namespaceName),\n\t\tBuildDependencies: true,\n\t}\n\n\ttestCases := []struct {\n\t\tname         string\n\t\ttemplateName string\n\t}{\n\t\t{\n\t\t\t\"dependent chart\",\n\t\t\t\"templates/deployment.yaml\",\n\t\t},\n\t\t{\n\t\t\t\"basic chart\",\n\t\t\t\"charts/basic/templates/deployment.yaml\",\n\t\t},\n\t}\n\n\tfor _, testCase := range testCases {\n\t\ttestCase := testCase\n\t\tt.Run(testCase.name, func(subT *testing.T) {\n\t\t\t// subT.Parallel()\n\t\t\t// Run RenderTemplate to render the template and capture the output. Note that we use the version without `E`, since\n\t\t\t// we want to assert that the template renders without any errors.\n\t\t\t// Additionally, although we know there is only one yaml file in the template, we deliberately path a templateFiles\n\t\t\t// arg to demonstrate how to select individual templates to render.\n\t\t\toutput := helm.RenderTemplate(t, options, helmChartPath, releaseName, []string{testCase.templateName})\n\n\t\t\t// Now we use kubernetes/client-go library to render the template output into the Deployment struct. This will\n\t\t\t// ensure the Deployment resource is rendered correctly.\n\t\t\tvar deployment appsv1.Deployment\n\t\t\thelm.UnmarshalK8SYaml(t, output, &deployment)\n\n\t\t\t// Verify the namespace matches the expected supplied namespace.\n\t\t\trequire.Equal(t, namespaceName, deployment.Namespace)\n\n\t\t\t// Finally, we verify the deployment pod template spec is set to the expected container image value\n\t\t\texpectedContainerImage := \"nginx:1.15.8\"\n\t\t\tdeploymentContainers := deployment.Spec.Template.Spec.Containers\n\t\t\trequire.Equal(t, len(deploymentContainers), 1)\n\t\t\trequire.Equal(t, deploymentContainers[0].Image, expectedContainerImage)\n\n\t\t})\n\t}\n}\n\n// An example of how to verify required values for a helm chart.\nfunc TestHelmDependencyExampleTemplateRequiredTemplateArgs(t *testing.T) {\n\tt.Parallel()\n\n\t// Path to the helm chart we will test\n\thelmChartPath, err := filepath.Abs(\"../examples/helm-dependency-example\")\n\treleaseName := \"helm-dependency\"\n\trequire.NoError(t, err)\n\n\t// Since we aren't deploying any resources, there is no need to setup kubectl authentication, helm home, or\n\t// namespaces\n\n\t// Here, we use a table driven test to iterate through all the required values as subtests. You can learn more about\n\t// go subtests here: https://blog.golang.org/subtests\n\t// The struct captures the inputs that we will pass to helm template and a human friendly name so we can identify it\n\t// in the test output. In this case, each test case will be a complete values input except for one of the required\n\t// values missing, to test that neglecting a required value will cause the template rendering to fail.\n\ttestCases := []struct {\n\t\tname   string\n\t\tvalues map[string]string\n\t}{\n\t\t{\n\t\t\t\"MissingContainerImageRepo in dependent chart\",\n\t\t\tmap[string]string{\n\t\t\t\t\"containerImageTag\":        \"1.15.8\",\n\t\t\t\t\"basic.containerImageRepo\": \"nginx\",\n\t\t\t\t\"basic.containerImageTag\":  \"1.15.8\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"MissingContainerImageRepo in basic chart\",\n\t\t\tmap[string]string{\n\t\t\t\t\"basic.containerImageTag\": \"1.15.8\",\n\t\t\t\t\"containerImageRepo\":      \"nginx\",\n\t\t\t\t\"containerImageTag\":       \"1.15.8\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"MissingContainerImageTag in dependent chart\",\n\t\t\tmap[string]string{\n\t\t\t\t\"containerImageRepo\":       \"nginx\",\n\t\t\t\t\"basic.containerImageRepo\": \"nginx\",\n\t\t\t\t\"basic.containerImageTag\":  \"1.15.8\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"MissingContainerImageTag in basic chart\",\n\t\t\tmap[string]string{\n\t\t\t\t\"basic.containerImageRepo\": \"nginx\",\n\t\t\t\t\"containerImageRepo\":       \"nginx\",\n\t\t\t\t\"containerImageTag\":        \"1.15.8\",\n\t\t\t},\n\t\t},\n\t}\n\n\t// Now we iterate over each test case and spawn a sub test\n\tfor _, testCase := range testCases {\n\t\t// Here, we capture the range variable and force it into the scope of this block. If we don't do this, when the\n\t\t// subtest switches contexts (because of t.Parallel), the testCase value will have been updated by the for loop\n\t\t// and will be the next testCase!\n\t\ttestCase := testCase\n\n\t\t// The actual sub test spawning. We name the sub test using the human friendly name. Note that we name the sub\n\t\t// test T struct to subT to make it clear which T struct corresponds to which test. However, in most cases you\n\t\t// will not reference the main test T so you can name it the same.\n\t\tt.Run(testCase.name, func(subT *testing.T) {\n\t\t\t// subT.Parallel()\n\n\t\t\t// Now we try rendering the template, but verify we get an error\n\t\t\toptions := &helm.Options{SetValues: testCase.values}\n\t\t\t_, err := helm.RenderTemplateE(t, options, helmChartPath, releaseName, []string{})\n\t\t\trequire.Error(t, err)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "test/helm_keda_remote_example_template_snapshot_test.go",
    "content": "//go:build kubeall || helm\n// +build kubeall helm\n\n// **NOTE**: we have build tags to differentiate kubernetes tests from non-kubernetes tests, and further differentiate helm\n// tests. This is done because minikube is heavy and can interfere with docker related tests in terratest. Similarly, helm\n// can overload the minikube system and thus interfere with the other kubernetes tests. Specifically, many of the tests\n// start to fail with `connection refused` errors from `minikube`. To avoid overloading the system, we run the kubernetes\n// tests and helm tests separately from the others. This may not be necessary if you have a sufficiently powerful machine.\n// We recommend at least 4 cores and 16GB of RAM if you want to run all the tests together.\n\npackage test\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n\tappsv1 \"k8s.io/api/apps/v1\"\n\n\t\"github.com/gruntwork-io/terratest/modules/helm\"\n\t\"github.com/gruntwork-io/terratest/modules/k8s\"\n\t\"github.com/gruntwork-io/terratest/modules/logger\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n)\n\n// This file contains an example of how to use terratest to test *remote* helm chart template logic by rendering the templates\n// using `helm template`, and then reading in the rendered templates.\n// - TestHelmKedaRemoteExampleTemplateRenderedDeployment: An example of how to read in the rendered object and check the\n//   computed values.\n\n// An example of how to verify the rendered template object of a Helm Chart given various inputs.\nfunc TestHelmKedaRemoteExampleTemplateRenderedDeploymentDump(t *testing.T) {\n\n\t// chart name\n\treleaseName := \"keda\"\n\n\t// Set up the namespace; confirm that the template renders the expected value for the namespace.\n\tnamespaceName := \"medieval-\" + strings.ToLower(random.UniqueId())\n\tlogger.Logf(t, \"Namespace: %s\\n\", namespaceName)\n\n\t// Setup the args. For this test, we will set the following input values:\n\toptions := &helm.Options{\n\t\tSetValues: map[string]string{\n\t\t\t\"metricsServer.replicaCount\":           \"999\",\n\t\t\t\"resources.metricServer.limits.memory\": \"1234Mi\",\n\t\t},\n\t\tKubectlOptions: k8s.NewKubectlOptions(\"\", \"\", namespaceName),\n\t\tLogger:         logger.Discard,\n\t}\n\n\t// Run RenderTemplate to render the *remote* template and capture the output. Note that we use the version without `E`, since\n\t// we want to assert that the template renders without any errors.\n\t// Additionally, we path a the templateFile for which we are setting test values to\n\t// demonstrate how to select individual templates to render.\n\toutput := helm.RenderRemoteTemplate(t, options, \"https://kedacore.github.io/charts\", releaseName, []string{\"templates/metrics-server/deployment.yaml\"})\n\n\t// Now we use kubernetes/client-go library to render the template output into the Deployment struct. This will\n\t// ensure the Deployment resource is rendered correctly.\n\tvar deployment appsv1.Deployment\n\thelm.UnmarshalK8SYaml(t, output, &deployment)\n\n\t// Verify the namespace matches the expected supplied namespace.\n\trequire.Equal(t, namespaceName, deployment.Namespace)\n\n\t// Finally, we verify the deployment pod template spec is set to the expected container image value\n\tvar expectedMetricsServerReplica int32\n\texpectedMetricsServerReplica = 999\n\tdeploymentMetricsServerReplica := *deployment.Spec.Replicas\n\trequire.Equal(t, expectedMetricsServerReplica, deploymentMetricsServerReplica)\n\n\t// write chart manifest to a local filesystem directory\n\thelm.UpdateSnapshot(t, options, output, releaseName)\n}\n\n// An example of how to verify the rendered template object of a Helm Chart given various inputs.\nfunc TestHelmKedaRemoteExampleTemplateRenderedDeploymentDiff(t *testing.T) {\n\n\t// chart name\n\treleaseName := \"keda\"\n\n\t// Set up the namespace; confirm that the template renders the expected value for the namespace.\n\tnamespaceName := \"medieval-\" + strings.ToLower(random.UniqueId())\n\tlogger.Logf(t, \"Namespace: %s\\n\", namespaceName)\n\n\t// Setup the args. For this test, we will set the following input values:\n\toptions := &helm.Options{\n\t\tSetValues: map[string]string{\n\t\t\t\"metricsServer.replicaCount\":           \"666\",\n\t\t\t\"resources.metricServer.limits.memory\": \"4321Mi\",\n\t\t},\n\t\tKubectlOptions: k8s.NewKubectlOptions(\"\", \"\", namespaceName),\n\t\tLogger:         logger.Discard,\n\t}\n\n\t// Run RenderTemplate to render the *remote* template and capture the output. Note that we use the version without `E`, since\n\t// we want to assert that the template renders without any errors.\n\t// Additionally, we path a the templateFile for which we are setting test values to\n\t// demonstrate how to select individual templates to render.\n\toutput := helm.RenderRemoteTemplate(t, options, \"https://kedacore.github.io/charts\", releaseName, []string{\"templates/metrics-server/deployment.yaml\"})\n\n\t// Now we use kubernetes/client-go library to render the template output into the Deployment struct. This will\n\t// ensure the Deployment resource is rendered correctly.\n\tvar deployment appsv1.Deployment\n\thelm.UnmarshalK8SYaml(t, output, &deployment)\n\n\t// Verify the namespace matches the expected supplied namespace.\n\trequire.Equal(t, namespaceName, deployment.Namespace)\n\n\t// Finally, we verify the deployment pod template spec is set to the expected container image value\n\tvar expectedMetricsServerReplica int32\n\texpectedMetricsServerReplica = 666\n\tdeploymentMetricsServerReplica := *deployment.Spec.Replicas\n\trequire.Equal(t, expectedMetricsServerReplica, deploymentMetricsServerReplica)\n\n\t// run the diff and assert the number of diffs\n\trequire.Equal(t, 4, helm.DiffAgainstSnapshot(t, options, output, releaseName))\n}\n\n// An example of how to store a snapshot of the current manaifest for future comparison\nfunc TestHelmKedaRemoteExampleTemplateRenderedPackageDump(t *testing.T) {\n\n\t// chart name\n\treleaseName := \"keda\"\n\n\t// Set up the namespace; confirm that the template renders the expected value for the namespace.\n\tnamespaceName := \"medieval-\" + strings.ToLower(random.UniqueId())\n\tlogger.Logf(t, \"Namespace: %s\\n\", namespaceName)\n\n\t// Setup the args. For this test, we will set the following input values:\n\toptions := &helm.Options{\n\t\tSetValues: map[string]string{\n\t\t\t\"metricsServer.replicaCount\":           \"999\",\n\t\t\t\"resources.metricServer.limits.memory\": \"1234Mi\",\n\t\t},\n\t\tKubectlOptions: k8s.NewKubectlOptions(\"\", \"\", namespaceName),\n\t\tLogger:         logger.Discard,\n\t}\n\n\t// Run RenderTemplate to render the *remote* template and capture the output. Note that we use the version without `E`, since\n\t// we want to assert that the template renders without any errors.\n\t// Additionally, we path a the templateFile for which we are setting test values to\n\t// demonstrate how to select individual templates to render.\n\toutput := helm.RenderRemoteTemplate(t, options, \"https://kedacore.github.io/charts\", releaseName, []string{})\n\n\t// write chart manifest to a local filesystem directory\n\thelm.UpdateSnapshot(t, options, output, releaseName)\n}\n\n// An example of how to verify the current helm k8s manifest against a previous snapshot\nfunc TestHelmKedaRemoteExampleTemplateRenderedPackageDiff(t *testing.T) {\n\n\t// chart name\n\treleaseName := \"keda\"\n\n\t// Set up the namespace; confirm that the template renders the expected value for the namespace.\n\tnamespaceName := \"medieval-\" + strings.ToLower(random.UniqueId())\n\tlogger.Logf(t, \"Namespace: %s\\n\", namespaceName)\n\n\t// Setup the args. For this test, we will set the following input values:\n\toptions := &helm.Options{\n\t\tSetValues: map[string]string{\n\t\t\t\"metricsServer.replicaCount\":           \"666\",\n\t\t\t\"resources.metricServer.limits.memory\": \"4321Mi\",\n\t\t},\n\t\tKubectlOptions: k8s.NewKubectlOptions(\"\", \"\", namespaceName),\n\t\tLogger:         logger.Discard,\n\t}\n\n\t// Run RenderTemplate to render the *remote* template and capture the output. Note that we use the version without `E`, since\n\t// we want to assert that the template renders without any errors.\n\t// Additionally, we path a the templateFile for which we are setting test values to\n\t// demonstrate how to select individual templates to render.\n\toutput := helm.RenderRemoteTemplate(t, options, \"https://kedacore.github.io/charts\", releaseName, []string{})\n\n\t// run the diff and assert the number of diffs matches the number of diffs in the snapshot\n\t// (namespace diffs + intentional value changes for replicas/memory)\n\trequire.Equal(t, 27, helm.DiffAgainstSnapshot(t, options, output, releaseName))\n}\n"
  },
  {
    "path": "test/helm_keda_remote_example_template_test.go",
    "content": "//go:build kubeall || helm\n// +build kubeall helm\n\n// **NOTE**: we have build tags to differentiate kubernetes tests from non-kubernetes tests, and further differentiate helm\n// tests. This is done because minikube is heavy and can interfere with docker related tests in terratest. Similarly, helm\n// can overload the minikube system and thus interfere with the other kubernetes tests. Specifically, many of the tests\n// start to fail with `connection refused` errors from `minikube`. To avoid overloading the system, we run the kubernetes\n// tests and helm tests separately from the others. This may not be necessary if you have a sufficiently powerful machine.\n// We recommend at least 4 cores and 16GB of RAM if you want to run all the tests together.\n\npackage test\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n\tappsv1 \"k8s.io/api/apps/v1\"\n\n\t\"github.com/gruntwork-io/terratest/modules/helm\"\n\t\"github.com/gruntwork-io/terratest/modules/k8s\"\n\t\"github.com/gruntwork-io/terratest/modules/logger\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n)\n\n// This file contains an example of how to use terratest to test *remote* helm chart template logic by rendering the templates\n// using `helm template`, and then reading in the rendered templates.\n// - TestHelmKedaRemoteExampleTemplateRenderedDeployment: An example of how to read in the rendered object and check the\n//   computed values.\n\n// An example of how to verify the rendered template object of a Helm Chart given various inputs.\nfunc TestHelmKedaRemoteExampleTemplateRenderedDeployment(t *testing.T) {\n\tt.Parallel()\n\n\t// chart name\n\treleaseName := \"keda\"\n\n\t// Set up the namespace; confirm that the template renders the expected value for the namespace.\n\tnamespaceName := \"medieval-\" + strings.ToLower(random.UniqueId())\n\tlogger.Logf(t, \"Namespace: %s\\n\", namespaceName)\n\n\t// Setup the args. For this test, we will set the following input values:\n\toptions := &helm.Options{\n\t\tSetValues: map[string]string{\n\t\t\t\"metricsServer.replicaCount\":           \"999\",\n\t\t\t\"resources.metricServer.limits.memory\": \"1234Mi\",\n\t\t},\n\t\tKubectlOptions: k8s.NewKubectlOptions(\"\", \"\", namespaceName),\n\t\tLogger:         logger.Discard,\n\t}\n\n\t// Run RenderTemplate to render the *remote* template and capture the output. Note that we use the version without `E`, since\n\t// we want to assert that the template renders without any errors.\n\t// Additionally, we path a the templateFile for which we are setting test values to\n\t// demonstrate how to select individual templates to render.\n\toutput := helm.RenderRemoteTemplate(t, options, \"https://kedacore.github.io/charts\", releaseName, []string{\"templates/metrics-server/deployment.yaml\"})\n\n\t// Now we use kubernetes/client-go library to render the template output into the Deployment struct. This will\n\t// ensure the Deployment resource is rendered correctly.\n\tvar deployment appsv1.Deployment\n\thelm.UnmarshalK8SYaml(t, output, &deployment)\n\n\t// Verify the namespace matches the expected supplied namespace.\n\trequire.Equal(t, namespaceName, deployment.Namespace)\n\n\t// Finally, we verify the deployment pod template spec is set to the expected container image value\n\tvar expectedMetricsServerReplica int32\n\texpectedMetricsServerReplica = 999\n\tdeploymentMetricsServerReplica := *deployment.Spec.Replicas\n\trequire.Equal(t, expectedMetricsServerReplica, deploymentMetricsServerReplica)\n\texpectedContainerRLM := \"1234Mi\"\n\tdeploymentContainers := deployment.Spec.Template.Spec.Containers\n\trequire.Equal(t, len(deploymentContainers), 1)\n\tcurrentContainerRLM := deploymentContainers[0].Resources.Limits.Memory().String()\n\trequire.Equal(t, currentContainerRLM, expectedContainerRLM)\n}\n\n// An example of how to verify the rendered template object of a Helm Chart given input from a `values.yaml` file.\nfunc TestHelmKedaRemoteExampleTemplateRenderedValuesFileFixtureDeployment(t *testing.T) {\n\tt.Parallel()\n\n\t// chart name\n\treleaseName := \"keda\"\n\n\t// Set up the namespace; confirm that the template renders the expected value for the namespace.\n\tnamespaceName := \"medieval-\" + strings.ToLower(random.UniqueId())\n\tlogger.Logf(t, \"Namespace: %s\\n\", namespaceName)\n\toptions := &helm.Options{\n\t\tValuesFiles:    []string{\"./fixtures/helm/keda-values.yaml\"},\n\t\tKubectlOptions: k8s.NewKubectlOptions(\"\", \"\", namespaceName),\n\t\tLogger:         logger.Discard,\n\t}\n\n\t// Run RenderTemplate to render the *remote* template and capture the output. Note that we use the version without `E`, since\n\t// we want to assert that the template renders without any errors.\n\t// Additionally, we path a the templateFile for which we are setting test values to\n\t// demonstrate how to select individual templates to render.\n\toutput := helm.RenderRemoteTemplate(t, options, \"https://kedacore.github.io/charts\", releaseName, []string{\"templates/metrics-server/deployment.yaml\"})\n\n\t// Now we use kubernetes/client-go library to render the template output into the Deployment struct. This will\n\t// ensure the Deployment resource is rendered correctly.\n\tvar deployment appsv1.Deployment\n\thelm.UnmarshalK8SYaml(t, output, &deployment)\n\n\t// Verify the namespace matches the expected supplied namespace.\n\trequire.Equal(t, namespaceName, deployment.Namespace)\n\n\t// Finally, we verify the deployment pod template spec is set to the expected value\n\tvar expectedMetricsServerReplica int32\n\texpectedMetricsServerReplica = 3\n\tdeploymentMetricsServerReplica := *deployment.Spec.Replicas\n\trequire.Equal(t, expectedMetricsServerReplica, deploymentMetricsServerReplica)\n\texpectedContainerRLM := \"1234Mi\"\n\tdeploymentContainers := deployment.Spec.Template.Spec.Containers\n\trequire.Equal(t, len(deploymentContainers), 1)\n\tcurrentContainerRLM := deploymentContainers[0].Resources.Limits.Memory().String()\n\trequire.Equal(t, currentContainerRLM, expectedContainerRLM)\n}\n"
  },
  {
    "path": "test/helm_log_redirect_integration_test.go",
    "content": "//go:build kubeall || helm\n// +build kubeall helm\n\n// **NOTE**: we have build tags to differentiate kubernetes tests from non-kubernetes tests, and further differentiate helm\n// tests. This is done because minikube is heavy and can interfere with docker related tests in terratest. Similarly, helm\n// can overload the minikube system and thus interfere with the other kubernetes tests. Specifically, many of the tests\n// start to fail with `connection refused` errors from `minikube`. To avoid overloading the system, we run the kubernetes\n// tests and helm tests separately from the others. This may not be necessary if you have a sufficiently powerful machine.\n// We recommend at least 4 cores and 16GB of RAM if you want to run all the tests together.\n\npackage test\n\nimport (\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/logger\"\n\ttftesting \"github.com/gruntwork-io/terratest/modules/testing\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/gruntwork-io/terratest/modules/helm\"\n\t\"github.com/gruntwork-io/terratest/modules/k8s\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n)\n\n// Example how to redirect helm logs to custom logger\nfunc TestHelmLogsRedirect(t *testing.T) {\n\tt.Parallel()\n\n\t// Path to the helm chart we will test\n\thelmChartPath, err := filepath.Abs(\"../examples/helm-basic-example\")\n\trequire.NoError(t, err)\n\n\t// Namespace to deploy helm chart\n\tnamespaceName := fmt.Sprintf(\"helm-logs-%s\", strings.ToLower(random.UniqueId()))\n\n\t// Setup the kubectl config and context. Here we choose to use the defaults, which is:\n\t// - HOME/.kube/config for the kubectl config file\n\t// - Current context of the kubectl config file\n\tkubectlOptions := k8s.NewKubectlOptions(\"\", \"\", namespaceName)\n\n\tk8s.CreateNamespace(t, kubectlOptions, namespaceName)\n\tdefer k8s.DeleteNamespace(t, kubectlOptions, namespaceName)\n\n\tcustomLogger := helmLogger{}\n\n\toptions := &helm.Options{\n\t\tKubectlOptions: kubectlOptions,\n\t\tSetValues: map[string]string{\n\t\t\t\"containerImageRepo\": \"nginx\",\n\t\t\t\"containerImageTag\":  \"1.15.8\",\n\t\t},\n\t\tLogger: logger.New(&customLogger),\n\t}\n\n\t// Generate a unique release to avoid conflicts with other tests\n\treleaseName := fmt.Sprintf(\n\t\t\"nginx-service-%s\",\n\t\tstrings.ToLower(random.UniqueId()),\n\t)\n\tdefer helm.Delete(t, options, releaseName, true)\n\n\thelm.Install(t, options, helmChartPath, releaseName)\n\n\t// Validate that logs were redirected to custom logger\n\trequire.Contains(t, customLogger.logs, releaseName)\n\trequire.Contains(t, customLogger.logs, \"STATUS: deployed\")\n}\n\ntype helmLogger struct {\n\tlogs string\n}\n\nfunc (c *helmLogger) Logf(t tftesting.TestingT, format string, args ...interface{}) {\n\tc.logs = fmt.Sprintf(\"%s\\n%s\", c.logs, fmt.Sprintf(format, args...))\n}\n"
  },
  {
    "path": "test/kubernetes_basic_example_logs_test.go",
    "content": "//go:build kubeall || kubernetes\n// +build kubeall kubernetes\n\n// NOTE: we have build tags to differentiate kubernetes tests from non-kubernetes tests. This is done because minikube\n// is heavy and can interfere with docker related tests in terratest. Specifically, many of the tests start to fail with\n// `connection refused` errors from `minikube`. To avoid overloading the system, we run the kubernetes tests and helm\n// tests separately from the others. This may not be necessary if you have a sufficiently powerful machine.  We\n// recommend at least 4 cores and 16GB of RAM if you want to run all the tests together.\n\npackage test\n\nimport (\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/gruntwork-io/terratest/modules/k8s\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\tv1 \"k8s.io/api/core/v1\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n)\n\nfunc setupLogsTest(t *testing.T) (*k8s.KubectlOptions, v1.Pod) {\n\tt.Parallel()\n\n\t// Path to the Kubernetes resource config we will test\n\tkubeResourcePath, err := filepath.Abs(\"../examples/kubernetes-basic-example/podinfo-daemonset.yml\")\n\trequire.NoError(t, err)\n\n\t// To ensure we can reuse the resource config on the same cluster to test different scenarios, we setup a unique\n\t// namespace for the resources for this test.\n\t// Note that namespaces must be lowercase.\n\tnamespaceName := strings.ToLower(random.UniqueId())\n\n\t// Setup the kubectl config and context. Here we choose to use the defaults, which is:\n\t// - HOME/.kube/config for the kubectl config file\n\t// - Current context of the kubectl config file\n\toptions := k8s.NewKubectlOptions(\"\", \"\", namespaceName)\n\n\tk8s.CreateNamespace(t, options, namespaceName)\n\t// ... and make sure to delete the namespace at the end of the test\n\tdefer k8s.DeleteNamespace(t, options, namespaceName)\n\n\t// At the end of the test, run `kubectl delete -f RESOURCE_CONFIG` to clean up any resources that were created.\n\tdefer k8s.KubectlDelete(t, options, kubeResourcePath)\n\n\t// This will run `kubectl apply -f RESOURCE_CONFIG` and fail the test if there are any errors\n\tk8s.KubectlApply(t, options, kubeResourcePath)\n\n\t// Wait for at least 1 Pod to be ready from the DaemonSet\n\tretries := 10\n\tsleep := time.Second * 1\n\tfor i := 1; i < retries; i++ {\n\t\tpodsReady := k8s.GetDaemonSet(t, options, \"podinfo-deamonset\").Status.NumberReady\n\t\tif podsReady > 0 {\n\t\t\tbreak\n\t\t}\n\t\ttime.Sleep(sleep)\n\t}\n\n\t// listOptions are used to select the pods with label app=podinfo\n\tlistOptions := new(metav1.ListOptions)\n\tlistOptions.LabelSelector = \"app=podinfo\"\n\n\t// Get a list of Pods. The pods are not guaranteed to be in running state.\n\tpods := k8s.ListPods(t, options, *listOptions)\n\n\t// Check that we did not timeout waiting for the Pod of the DaemonSet to be ready\n\trequire.Greater(t, len(pods), 0)\n\n\tpod := pods[0]\n\n\t// Wait fot the pod to be started and ready\n\tk8s.WaitUntilPodAvailable(t, options, pod.Name, 5, 10*time.Second)\n\n\treturn options, pod\n}\n\nfunc TestKubernetesBasicExampleLogsCheckWithContainerName(t *testing.T) {\n\toptions, pod := setupLogsTest(t)\n\tlogs := k8s.GetPodLogs(t, options, &pod, \"podinfo\")\n\n\trequire.Contains(t, logs, \"Starting podinfo\")\n}\n\nfunc TestKubernetesBasicExampleLogsCheckWithNoContainerName(t *testing.T) {\n\toptions, pod := setupLogsTest(t)\n\tlogs := k8s.GetPodLogs(t, options, &pod, \"\")\n\n\trequire.Contains(t, logs, \"Starting podinfo\")\n}\n"
  },
  {
    "path": "test/kubernetes_basic_example_service_check_test.go",
    "content": "//go:build kubeall || kubernetes\n// +build kubeall kubernetes\n\n// NOTE: we have build tags to differentiate kubernetes tests from non-kubernetes tests. This is done because minikube\n// is heavy and can interfere with docker related tests in terratest. Specifically, many of the tests start to fail with\n// `connection refused` errors from `minikube`. To avoid overloading the system, we run the kubernetes tests and helm\n// tests separately from the others. This may not be necessary if you have a sufficiently powerful machine.  We\n// recommend at least 4 cores and 16GB of RAM if you want to run all the tests together.\n\npackage test\n\nimport (\n\t\"crypto/tls\"\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/require\"\n\n\thttp_helper \"github.com/gruntwork-io/terratest/modules/http-helper\"\n\t\"github.com/gruntwork-io/terratest/modules/k8s\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n)\n\n// An example of how to do more expanded verification of the Kubernetes resource config in examples/kubernetes-basic-example using Terratest.\nfunc TestKubernetesBasicExampleServiceCheck(t *testing.T) {\n\tt.Parallel()\n\n\t// Path to the Kubernetes resource config we will test\n\tkubeResourcePath, err := filepath.Abs(\"../examples/kubernetes-basic-example/nginx-deployment.yml\")\n\trequire.NoError(t, err)\n\n\t// To ensure we can reuse the resource config on the same cluster to test different scenarios, we setup a unique\n\t// namespace for the resources for this test.\n\t// Note that namespaces must be lowercase.\n\tnamespaceName := strings.ToLower(random.UniqueId())\n\n\t// Setup the kubectl config and context. Here we choose to use the defaults, which is:\n\t// - HOME/.kube/config for the kubectl config file\n\t// - Current context of the kubectl config file\n\toptions := k8s.NewKubectlOptions(\"\", \"\", namespaceName)\n\n\tk8s.CreateNamespace(t, options, namespaceName)\n\t// ... and make sure to delete the namespace at the end of the test\n\tdefer k8s.DeleteNamespace(t, options, namespaceName)\n\n\t// At the end of the test, run `kubectl delete -f RESOURCE_CONFIG` to clean up any resources that were created.\n\tdefer k8s.KubectlDelete(t, options, kubeResourcePath)\n\n\t// This will run `kubectl apply -f RESOURCE_CONFIG` and fail the test if there are any errors\n\tk8s.KubectlApply(t, options, kubeResourcePath)\n\n\t// This will wait up to 10 seconds for the service to become available, to ensure that we can access it.\n\tk8s.WaitUntilServiceAvailable(t, options, \"nginx-service\", 10, 1*time.Second)\n\n\t// Now we verify that the service will successfully boot and start serving requests\n\tservice := k8s.GetService(t, options, \"nginx-service\")\n\tendpoint := k8s.GetServiceEndpoint(t, options, service, 80)\n\n\t// Setup a TLS configuration to submit with the helper, a blank struct is acceptable\n\ttlsConfig := tls.Config{}\n\n\t// Test the endpoint for up to 5 minutes. This will only fail if we timeout waiting for the service to return a 200\n\t// response.\n\thttp_helper.HttpGetWithRetryWithCustomValidation(\n\t\tt,\n\t\tfmt.Sprintf(\"http://%s\", endpoint),\n\t\t&tlsConfig,\n\t\t30,\n\t\t10*time.Second,\n\t\tfunc(statusCode int, body string) bool {\n\t\t\treturn statusCode == 200\n\t\t},\n\t)\n}\n"
  },
  {
    "path": "test/kubernetes_basic_example_test.go",
    "content": "//go:build kubeall || kubernetes\n// +build kubeall kubernetes\n\n// NOTE: we have build tags to differentiate kubernetes tests from non-kubernetes tests. This is done because minikube\n// is heavy and can interfere with docker related tests in terratest. Specifically, many of the tests start to fail with\n// `connection refused` errors from `minikube`. To avoid overloading the system, we run the kubernetes tests and helm\n// tests separately from the others. This may not be necessary if you have a sufficiently powerful machine.  We\n// recommend at least 4 cores and 16GB of RAM if you want to run all the tests together.\n\npackage test\n\nimport (\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/gruntwork-io/terratest/modules/k8s\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n)\n\n// An example of how to test the Kubernetes resource config in examples/kubernetes-basic-example using Terratest.\nfunc TestKubernetesBasicExample(t *testing.T) {\n\tt.Parallel()\n\n\t// website::tag::1::Path to the Kubernetes resource config we will test\n\tkubeResourcePath, err := filepath.Abs(\"../examples/kubernetes-basic-example/nginx-deployment.yml\")\n\trequire.NoError(t, err)\n\n\t// To ensure we can reuse the resource config on the same cluster to test different scenarios, we setup a unique\n\t// namespace for the resources for this test.\n\t// Note that namespaces must be lowercase.\n\tnamespaceName := fmt.Sprintf(\"kubernetes-basic-example-%s\", strings.ToLower(random.UniqueId()))\n\n\t// website::tag::2::Setup the kubectl config and context.\n\t// Here we choose to use the defaults, which is:\n\t// - HOME/.kube/config for the kubectl config file\n\t// - Current context of the kubectl config file\n\t// - Random namespace\n\toptions := k8s.NewKubectlOptions(\"\", \"\", namespaceName)\n\n\tk8s.CreateNamespace(t, options, namespaceName)\n\t// website::tag::5::Make sure to delete the namespace at the end of the test\n\tdefer k8s.DeleteNamespace(t, options, namespaceName)\n\n\t// website::tag::6::At the end of the test, run `kubectl delete -f RESOURCE_CONFIG` to clean up any resources that were created.\n\tdefer k8s.KubectlDelete(t, options, kubeResourcePath)\n\n\t// website::tag::3::Apply kubectl with 'kubectl apply -f RESOURCE_CONFIG' command.\n\t// This will run `kubectl apply -f RESOURCE_CONFIG` and fail the test if there are any errors\n\tk8s.KubectlApply(t, options, kubeResourcePath)\n\n\t// website::tag::4::Check if NGINX service was deployed successfully.\n\t// This will get the service resource and verify that it exists and was retrieved successfully. This function will\n\t// fail the test if the there is an error retrieving the service resource from Kubernetes.\n\tservice := k8s.GetService(t, options, \"nginx-service\")\n\trequire.Equal(t, service.Name, \"nginx-service\")\n}\n"
  },
  {
    "path": "test/kubernetes_hello_world_example_test.go",
    "content": "//go:build kubeall || kubernetes\n// +build kubeall kubernetes\n\n// NOTE: See the notes in the other Kubernetes example tests for why this build tag is included.\n\npackage test\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\t\"time\"\n\n\thttp_helper \"github.com/gruntwork-io/terratest/modules/http-helper\"\n\t\"github.com/gruntwork-io/terratest/modules/k8s\"\n)\n\nfunc TestKubernetesHelloWorldExample(t *testing.T) {\n\tt.Parallel()\n\n\t// website::tag::1:: Path to the Kubernetes resource config we will test.\n\tkubeResourcePath := \"../examples/kubernetes-hello-world-example/hello-world-deployment.yml\"\n\n\t// website::tag::2:: Setup the kubectl config and context.\n\toptions := k8s.NewKubectlOptions(\"\", \"\", \"default\")\n\n\t// website::tag::6:: At the end of the test, run \"kubectl delete\" to clean up any resources that were created.\n\tdefer k8s.KubectlDelete(t, options, kubeResourcePath)\n\n\t// website::tag::3:: Run `kubectl apply` to deploy. Fail the test if there are any errors.\n\tk8s.KubectlApply(t, options, kubeResourcePath)\n\n\t// website::tag::4:: Verify the service is available and get the URL for it.\n\tk8s.WaitUntilServiceAvailable(t, options, \"hello-world-service\", 10, 1*time.Second)\n\tservice := k8s.GetService(t, options, \"hello-world-service\")\n\turl := fmt.Sprintf(\"http://%s\", k8s.GetServiceEndpoint(t, options, service, 5000))\n\n\t// website::tag::5:: Make an HTTP request to the URL and make sure it returns a 200 OK with the body \"Hello, World!\".\n\thttp_helper.HttpGetWithRetry(t, url, nil, 200, \"Hello, World!\", 30, 3*time.Second)\n}\n"
  },
  {
    "path": "test/kubernetes_kustomize_example_test.go",
    "content": "//go:build kubeall || kubernetes\n// +build kubeall kubernetes\n\n// NOTE: we have build tags to differentiate kubernetes tests from non-kubernetes tests. This is done because minikube\n// is heavy and can interfere with docker related tests in terratest. Specifically, many of the tests start to fail with\n// `connection refused` errors from `minikube`. To avoid overloading the system, we run the kubernetes tests and helm\n// tests separately from the others. This may not be necessary if you have a sufficiently powerful machine.  We\n// recommend at least 4 cores and 16GB of RAM if you want to run all the tests together.\n\npackage test\n\nimport (\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/gruntwork-io/terratest/modules/k8s\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n)\n\n// An example of how to test the Kubernetes resource config in examples/kubernetes-kustomize-example using Terratest.\nfunc TestKubernetesKustomizeExample(t *testing.T) {\n\tt.Parallel()\n\n\t// website::tag::1::Path to the Kubernetes resource config we will test\n\tkubeResourcePath, err := filepath.Abs(\"../examples/kubernetes-kustomize-example/\")\n\trequire.NoError(t, err)\n\n\t// To ensure we can reuse the resource config on the same cluster to test different scenarios, we setup a unique\n\t// namespace for the resources for this test.\n\t// Note that namespaces must be lowercase.\n\tnamespaceName := fmt.Sprintf(\"kubernetes-kustomize-example-%s\", strings.ToLower(random.UniqueId()))\n\n\t// website::tag::2::Setup the kubectl config and context.\n\t// Here we choose to use the defaults, which is:\n\t// - HOME/.kube/config for the kubectl config file\n\t// - Current context of the kubectl config file\n\t// - Random namespace\n\toptions := k8s.NewKubectlOptions(\"\", \"\", namespaceName)\n\n\tk8s.CreateNamespace(t, options, namespaceName)\n\t// website::tag::5::Make sure to delete the namespace at the end of the test\n\tdefer k8s.DeleteNamespace(t, options, namespaceName)\n\n\t// website::tag::6::At the end of the test, run `kubectl delete -f RESOURCE_CONFIG` to clean up any resources that were created.\n\tdefer k8s.KubectlDeleteFromKustomize(t, options, kubeResourcePath)\n\n\t// website::tag::3::Apply kubectl with 'kubectl apply -f RESOURCE_CONFIG' command.\n\t// This will run `kubectl apply -f RESOURCE_CONFIG` and fail the test if there are any errors\n\tk8s.KubectlApplyFromKustomize(t, options, kubeResourcePath)\n\n\t// website::tag::4::Check if NGINX service was deployed successfully.\n\t// This will get the service resource and verify that it exists and was retrieved successfully. This function will\n\t// fail the test if the there is an error retrieving the service resource from Kubernetes.\n\tservice := k8s.GetService(t, options, \"nginx-service\")\n\trequire.Equal(t, service.Name, \"nginx-service\")\n}\n"
  },
  {
    "path": "test/kubernetes_rbac_example_test.go",
    "content": "//go:build kubeall || kubernetes\n// +build kubeall kubernetes\n\n// NOTE: we have build tags to differentiate kubernetes tests from non-kubernetes tests. This is done because minikube\n// is heavy and can interfere with docker related tests in terratest. Specifically, many of the tests start to fail with\n// `connection refused` errors from `minikube`. To avoid overloading the system, we run the kubernetes tests and helm\n// tests separately from the others. This may not be necessary if you have a sufficiently powerful machine.  We\n// recommend at least 4 cores and 16GB of RAM if you want to run all the tests together.\n\npackage test\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n\tauthv1 \"k8s.io/api/authorization/v1\"\n\n\t\"github.com/gruntwork-io/terratest/modules/k8s\"\n)\n\n// An example of how to test the Kubernetes resource config in examples/kubernetes-rbac-example using Terratest,\n// including whether or not the permissions are set correctly.\nfunc TestKubernetesRBACExample(t *testing.T) {\n\tt.Parallel()\n\n\t// These are pulled from the kubernetes resource config\n\tconst serviceAccountName = \"terratest-rbac-example-service-account\"\n\tconst namespaceName = \"terratest-rbac-example-namespace\"\n\n\t// Path to the Kubernetes resource config we will test\n\tkubeResourcePath, err := filepath.Abs(\"../examples/kubernetes-rbac-example/namespace-service-account.yml\")\n\trequire.NoError(t, err)\n\n\t// Setup the kubectl config and context. Here we choose to create a new one because we will be manipulating the\n\t// entries to be able to add a new authentication option.\n\ttmpConfigPath := k8s.CopyHomeKubeConfigToTemp(t)\n\tdefer os.Remove(tmpConfigPath)\n\toptions := k8s.NewKubectlOptions(\"\", tmpConfigPath, namespaceName)\n\n\t// At the end of the test, run `kubectl delete -f RESOURCE_CONFIG` to clean up any resources that were created.\n\tdefer k8s.KubectlDelete(t, options, kubeResourcePath)\n\n\t// This will run `kubectl apply -f RESOURCE_CONFIG` and fail the test if there are any errors\n\tk8s.KubectlApply(t, options, kubeResourcePath)\n\n\t// Retrieve authentication token for the newly created ServiceAccount\n\ttoken := k8s.GetServiceAccountAuthToken(t, options, serviceAccountName)\n\n\t// Now update the configuration to add a new context that can be used to make requests as that service account\n\trequire.NoError(t, k8s.AddConfigContextForServiceAccountE(\n\t\tt,\n\t\toptions,\n\t\tserviceAccountName, // for this test we will name the context after the ServiceAccount\n\t\tserviceAccountName,\n\t\ttoken,\n\t))\n\tserviceAccountKubectlOptions := k8s.NewKubectlOptions(serviceAccountName, tmpConfigPath, namespaceName)\n\n\t// At this point all requests made with serviceAccountKubectlOptions will be auth'd as that ServiceAccount. So let's\n\t// verify that! We will check:\n\t// - we can't access the kube-system namespace\n\tadminListPodAction := authv1.ResourceAttributes{\n\t\tNamespace: \"kube-system\",\n\t\tVerb:      \"list\",\n\t\tResource:  \"pod\",\n\t}\n\trequire.False(t, k8s.CanIDo(t, serviceAccountKubectlOptions, adminListPodAction))\n\t// - we can access the namespace the service account is in\n\tnamespaceListPodAction := authv1.ResourceAttributes{\n\t\tNamespace: namespaceName,\n\t\tVerb:      \"list\",\n\t\tResource:  \"pod\",\n\t}\n\trequire.True(t, k8s.CanIDo(t, serviceAccountKubectlOptions, namespaceListPodAction))\n}\n"
  },
  {
    "path": "test/kubernetes_rest_config_example_test.go",
    "content": "//go:build kubeall || kubernetes\n// +build kubeall kubernetes\n\n// NOTE: we have build tags to differentiate kubernetes tests from non-kubernetes tests. This is done because minikube\n// is heavy and can interfere with docker related tests in terratest. Specifically, many of the tests start to fail with\n// `connection refused` errors from `minikube`. To avoid overloading the system, we run the kubernetes tests and helm\n// tests separately from the others. This may not be necessary if you have a sufficiently powerful machine.  We\n// recommend at least 4 cores and 16GB of RAM if you want to run all the tests together.\n\npackage test\n\nimport (\n\t\"fmt\"\n\t\"os/user\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n\t\"k8s.io/client-go/tools/clientcmd\"\n\n\t\"github.com/gruntwork-io/terratest/modules/k8s\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n)\n\nfunc TestKubernetesRestConfigBasicExampleConfig(t *testing.T) {\n\tt.Parallel()\n\n\t// website::tag::1::Path to the Kubernetes resource config we will test\n\tkubeResourcePath, err := filepath.Abs(\"../examples/kubernetes-basic-example/nginx-deployment.yml\")\n\trequire.NoError(t, err)\n\n\t// To ensure we can reuse the resource config on the same cluster to test different scenarios, we setup a unique\n\t// namespace for the resources for this test.\n\t// Note that namespaces must be lowercase.\n\tnamespaceName := fmt.Sprintf(\"kubernetes-basic-example-%s\", strings.ToLower(random.UniqueId()))\n\n\tusr, err := user.Current()\n\tif err != nil {\n\t\trequire.NoError(t, err)\n\t}\n\n\t// Construct the path to the kubeconfig file\n\tkubeconfigPath := filepath.Join(usr.HomeDir, \".kube\", \"config\")\n\n\t// Generate rest.Config from kubeconfig file\n\tconfig, err := clientcmd.BuildConfigFromFlags(\"\", kubeconfigPath)\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\n\t// website::tag::2:: Setup the kubectl config and context.\n\toptions := k8s.NewKubectlOptionsWithRestConfig(config, namespaceName)\n\n\tk8s.CreateNamespace(t, options, namespaceName)\n\t// website::tag::5::Make sure to delete the namespace at the end of the test\n\tdefer k8s.DeleteNamespace(t, options, namespaceName)\n\n\t// website::tag::6::At the end of the test, run `kubectl delete -f RESOURCE_CONFIG` to clean up any resources that were created.\n\tdefer k8s.KubectlDelete(t, options, kubeResourcePath)\n\n\t// website::tag::3::Apply kubectl with 'kubectl apply -f RESOURCE_CONFIG' command.\n\t// This will run `kubectl apply -f RESOURCE_CONFIG` and fail the test if there are any errors\n\tk8s.KubectlApply(t, options, kubeResourcePath)\n\n\t// website::tag::4::Check if NGINX service was deployed successfully.\n\t// This will get the service resource and verify that it exists and was retrieved successfully. This function will\n\t// fail the test if the there is an error retrieving the service resource from Kubernetes.\n\tservice := k8s.GetService(t, options, \"nginx-service\")\n\trequire.Equal(t, service.Name, \"nginx-service\")\n}\n"
  },
  {
    "path": "test/packer_basic_example_test.go",
    "content": "package test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\t\"github.com/aws/aws-sdk-go-v2/service/ec2\"\n\t\"github.com/aws/aws-sdk-go-v2/service/ec2/types\"\n\tterratest_aws \"github.com/gruntwork-io/terratest/modules/aws\"\n\t\"github.com/gruntwork-io/terratest/modules/packer\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// Occasionally, a Packer build may fail due to intermittent issues (e.g., brief network outage or EC2 issue). We try\n// to make our tests resilient to that by specifying those known common errors here and telling our builds to retry if\n// they hit those errors.\nvar DefaultRetryablePackerErrors = map[string]string{\n\t\"Script disconnected unexpectedly\":                                                 \"Occasionally, Packer seems to lose connectivity to AWS, perhaps due to a brief network outage\",\n\t\"can not open /var/lib/apt/lists/archive.ubuntu.com_ubuntu_dists_xenial_InRelease\": \"Occasionally, apt-get fails on ubuntu to update the cache\",\n\t\"error while running command: exit status 1;\":                                      \"Occasionally, package installation inside the image seems to fail due to several reasons such as it's being missing from package repository\",\n}\nvar DefaultTimeBetweenPackerRetries = 15 * time.Second\n\nconst DefaultMaxPackerRetries = 3\n\n// An example of how to test the Packer template in examples/packer-basic-example using Terratest.\nfunc TestPackerBasicExample(t *testing.T) {\n\tt.Parallel()\n\n\t// Pick a random AWS region to test in. This helps ensure your code works in all regions.\n\tawsRegion := terratest_aws.GetRandomStableRegion(t, nil, nil)\n\n\t// Some AWS regions are missing certain instance types, so pick an available type based on the region we picked\n\tinstanceType := terratest_aws.GetRecommendedInstanceType(t, awsRegion, []string{\"t2.micro, t3.micro\", \"t2.small\", \"t3.small\"})\n\n\t// website::tag::1::Read Packer's template and set AWS Region variable.\n\tpackerOptions := &packer.Options{\n\t\t// The path to where the Packer template is located\n\t\tTemplate: \"../examples/packer-basic-example/build.pkr.hcl\",\n\n\t\t// Variables to pass to our Packer build using -var options\n\t\tVars: map[string]string{\n\t\t\t\"aws_region\":    awsRegion,\n\t\t\t\"ami_base_name\": fmt.Sprintf(\"%s\", random.UniqueId()),\n\t\t\t\"instance_type\": instanceType,\n\t\t},\n\n\t\t// Only build the AWS AMI\n\t\tOnly: \"amazon-ebs.ubuntu-example\",\n\n\t\t// Configure retries for intermittent errors\n\t\tRetryableErrors:    DefaultRetryablePackerErrors,\n\t\tTimeBetweenRetries: DefaultTimeBetweenPackerRetries,\n\t\tMaxRetries:         DefaultMaxPackerRetries,\n\t}\n\n\t// website::tag::2::Build artifacts from Packer's template.\n\t// Make sure the Packer build completes successfully\n\tamiID := packer.BuildArtifact(t, packerOptions)\n\n\t// website::tag::4::Remove AMI after test.\n\t// Clean up the AMI after we're done\n\tdefer terratest_aws.DeleteAmiAndAllSnapshots(t, awsRegion, amiID)\n\n\t// Check if AMI is shared/not shared with account\n\trequestingAccount := terratest_aws.CanonicalAccountId\n\trandomAccount := \"123456789012\" // Random Account\n\tec2Client := terratest_aws.NewEc2Client(t, awsRegion)\n\tShareAmi(t, amiID, requestingAccount, ec2Client)\n\taccountsWithLaunchPermissions := terratest_aws.GetAccountsWithLaunchPermissionsForAmi(t, awsRegion, amiID)\n\tassert.NotContains(t, accountsWithLaunchPermissions, randomAccount)\n\tassert.Contains(t, accountsWithLaunchPermissions, requestingAccount)\n\n\t// website::tag::3::Check AMI's properties.\n\t// Check if AMI is private\n\tamiIsPublic := terratest_aws.GetAmiPubliclyAccessible(t, awsRegion, amiID)\n\tassert.False(t, amiIsPublic)\n}\n\n// An example of how to test the Packer template in examples/packer-basic-example using Terratest\n// with the VarFiles option. This test generates a temporary *.json file containing the value\n// for the `aws_region` variable.\nfunc TestPackerBasicExampleWithVarFile(t *testing.T) {\n\tt.Parallel()\n\n\t// Pick a random AWS region to test in. This helps ensure your code works in all regions.\n\tawsRegion := terratest_aws.GetRandomStableRegion(t, nil, nil)\n\n\t// Some AWS regions are missing certain instance types, so pick an available type based on the region we picked\n\tinstanceType := terratest_aws.GetRecommendedInstanceType(t, awsRegion, []string{\"t2.micro, t3.micro\", \"t2.small\", \"t3.small\"})\n\n\t// Create temporary packer variable file to store aws region\n\tvarFile, err := os.CreateTemp(\"\", \"*.json\")\n\trequire.NoError(t, err, \"Did not expect temp file creation to cause error\")\n\n\t// Be sure to clean up temp file\n\tdefer os.Remove(varFile.Name())\n\n\t// Write the vars we need to a temporary json file\n\tvarFileContent := []byte(fmt.Sprintf(`{\"aws_region\": \"%s\", \"instance_type\": \"%s\"}`, awsRegion, instanceType))\n\t_, err = varFile.Write(varFileContent)\n\trequire.NoError(t, err, \"Did not expect writing to temp file %s to cause error\", varFile.Name())\n\n\tpackerOptions := &packer.Options{\n\t\t// The path to where the Packer template is located\n\t\tTemplate: \"../examples/packer-basic-example/build.pkr.hcl\",\n\n\t\t// Variable file to pass to our Packer build using -var-file option\n\t\tVarFiles: []string{\n\t\t\tvarFile.Name(),\n\t\t},\n\n\t\t// Environment settings to avoid plugin conflicts\n\t\tEnv: map[string]string{\n\t\t\t\"PACKER_PLUGIN_PATH\": \"../examples/packer-basic-example/.packer.d/plugins\",\n\t\t},\n\n\t\t// Only build the AWS AMI\n\t\tOnly: \"amazon-ebs.ubuntu-example\",\n\n\t\t// Configure retries for intermittent errors\n\t\tRetryableErrors:    DefaultRetryablePackerErrors,\n\t\tTimeBetweenRetries: DefaultTimeBetweenPackerRetries,\n\t\tMaxRetries:         DefaultMaxPackerRetries,\n\t}\n\n\t// Make sure the Packer build completes successfully\n\tamiID := packer.BuildArtifact(t, packerOptions)\n\n\t// Clean up the AMI after we're done\n\tdefer terratest_aws.DeleteAmiAndAllSnapshots(t, awsRegion, amiID)\n\n\t// Check if AMI is shared/not shared with account\n\trequestingAccount := terratest_aws.CanonicalAccountId\n\trandomAccount := \"123456789012\" // Random Account\n\tec2Client := terratest_aws.NewEc2Client(t, awsRegion)\n\tShareAmi(t, amiID, requestingAccount, ec2Client)\n\taccountsWithLaunchPermissions := terratest_aws.GetAccountsWithLaunchPermissionsForAmi(t, awsRegion, amiID)\n\tassert.NotContains(t, accountsWithLaunchPermissions, randomAccount)\n\tassert.Contains(t, accountsWithLaunchPermissions, requestingAccount)\n\n\t// Check if AMI is private\n\tamiIsPublic := terratest_aws.GetAmiPubliclyAccessible(t, awsRegion, amiID)\n\tassert.False(t, amiIsPublic)\n}\n\nfunc TestPackerMultipleConcurrentAmis(t *testing.T) {\n\tt.Parallel()\n\n\t// Build a map of 3 randomId <-> packer.Options, in 3 random AWS Regions\n\t// then build all of these AMIs in parallel and make sure that there are\n\t// no errors.\n\tvar identifierToOptions = map[string]*packer.Options{}\n\tfor i := 0; i < 3; i++ {\n\t\t// Pick a random AWS region to test in. This helps ensure your code works in all regions.\n\t\tawsRegion := terratest_aws.GetRandomStableRegion(t, nil, nil)\n\n\t\t// Some AWS regions are missing certain instance types, so pick an available type based on the region we picked\n\t\tinstanceType := terratest_aws.GetRecommendedInstanceType(t, awsRegion, []string{\"t2.micro, t3.micro\", \"t2.small\", \"t3.small\"})\n\n\t\tpackerOptions := &packer.Options{\n\t\t\t// The path to where the Packer template is located\n\t\t\tTemplate: \"../examples/packer-basic-example/build.pkr.hcl\",\n\n\t\t\t// Variables to pass to our Packer build using -var options\n\t\t\tVars: map[string]string{\n\t\t\t\t\"aws_region\":    awsRegion,\n\t\t\t\t\"ami_base_name\": fmt.Sprintf(\"%s\", random.UniqueId()),\n\t\t\t\t\"instance_type\": instanceType,\n\t\t\t},\n\n\t\t\t// Only build the AWS AMI\n\t\t\tOnly: \"amazon-ebs.ubuntu-example\",\n\n\t\t\t// Configure retries for intermittent errors\n\t\t\tRetryableErrors:    DefaultRetryablePackerErrors,\n\t\t\tTimeBetweenRetries: DefaultTimeBetweenPackerRetries,\n\t\t\tMaxRetries:         DefaultMaxPackerRetries,\n\t\t}\n\n\t\tidentifierToOptions[random.UniqueId()] = packerOptions\n\t}\n\n\tresultMap := packer.BuildArtifacts(t, identifierToOptions)\n\n\t// Clean up the AMIs after we're done\n\tfor key, amiId := range resultMap {\n\t\tawsRegion := identifierToOptions[key].Vars[\"aws_region\"]\n\t\tterratest_aws.DeleteAmiAndAllSnapshots(t, awsRegion, amiId)\n\t}\n}\n\nfunc ShareAmi(t *testing.T, amiID string, accountID string, ec2Client *ec2.Client) {\n\tinput := &ec2.ModifyImageAttributeInput{\n\t\tImageId: aws.String(amiID),\n\t\tLaunchPermission: &types.LaunchPermissionModifications{\n\t\t\tAdd: []types.LaunchPermission{\n\t\t\t\t{\n\t\t\t\t\tUserId: aws.String(accountID),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\t_, err := ec2Client.ModifyImageAttribute(context.Background(), input)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n"
  },
  {
    "path": "test/packer_docker_example_test.go",
    "content": "package test\n\nimport (\n\t\"crypto/tls\"\n\t\"fmt\"\n\t\"strconv\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/gruntwork-io/terratest/modules/docker\"\n\thttp_helper \"github.com/gruntwork-io/terratest/modules/http-helper\"\n\t\"github.com/gruntwork-io/terratest/modules/packer\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n)\n\n// An example of how to test the Packer template in examples/packer-docker-example completely locally using Terratest\n// and Docker.\nfunc TestPackerDockerExampleLocal(t *testing.T) {\n\tt.Parallel()\n\n\t// website::tag::1::Configure Packer to build Docker image.\n\tpackerOptions := &packer.Options{\n\t\t// The path to where the Packer template is located\n\t\tTemplate: \"../examples/packer-docker-example/build.pkr.hcl\",\n\n\t\t// Only build the Docker image for local testing\n\t\tOnly: \"docker.ubuntu-docker\",\n\n\t\t// Configure retries for intermittent errors\n\t\tRetryableErrors:    DefaultRetryablePackerErrors,\n\t\tTimeBetweenRetries: DefaultTimeBetweenPackerRetries,\n\t\tMaxRetries:         DefaultMaxPackerRetries,\n\t}\n\n\t// website::tag::2::Build the Docker image using Packer\n\tpacker.BuildArtifact(t, packerOptions)\n\n\tserverPort := 8080\n\texpectedServerText := fmt.Sprintf(\"Hello, %s!\", random.UniqueId())\n\n\tdockerOptions := &docker.Options{\n\t\t// website::tag::3::Set path to 'docker-compose.yml' and environment variables to run Docker image.\n\t\t// Directory where docker-compose.yml lives\n\t\tWorkingDir: \"../examples/packer-docker-example\",\n\n\t\t// Configure the port the web app will listen on and the text it will return using environment variables\n\t\tEnvVars: map[string]string{\n\t\t\t\"SERVER_PORT\": strconv.Itoa(serverPort),\n\t\t\t\"SERVER_TEXT\": expectedServerText,\n\t\t},\n\t}\n\n\t// website::tag::6::Make sure to shut down the Docker container at the end of the test.\n\tdefer docker.RunDockerCompose(t, dockerOptions, \"down\")\n\n\t// website::tag::4::Run Docker Compose to fire up the web app. We run it in the background (-d) so it doesn't block this test.\n\tdocker.RunDockerCompose(t, dockerOptions, \"up\", \"-d\")\n\n\t// It can take a few seconds for the Docker container boot up, so retry a few times\n\tmaxRetries := 5\n\ttimeBetweenRetries := 2 * time.Second\n\turl := fmt.Sprintf(\"http://localhost:%d\", serverPort)\n\n\t// Setup a TLS configuration to submit with the helper, a blank struct is acceptable\n\ttlsConfig := tls.Config{}\n\n\t// website::tag::5::Verify that we get back a 200 OK with the expected text\n\thttp_helper.HttpGetWithRetry(t, url, &tlsConfig, 200, expectedServerText, maxRetries, timeBetweenRetries)\n}\n"
  },
  {
    "path": "test/packer_hello_world_example_test.go",
    "content": "package test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/docker\"\n\t\"github.com/gruntwork-io/terratest/modules/packer\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestPackerHelloWorldExample(t *testing.T) {\n\tpackerOptions := &packer.Options{\n\t\t// website::tag::1:: The path to where the Packer template is located\n\t\tTemplate: \"../examples/packer-hello-world-example/build.pkr.hcl\",\n\t}\n\n\t// website::tag::2:: Build the Packer template. This template will create a Docker image.\n\tpacker.BuildArtifact(t, packerOptions)\n\n\t// website::tag::3:: Run the Docker image, read the text file from it, and make sure it contains the expected output.\n\topts := &docker.RunOptions{\n\t\tCommand:  []string{\"cat\", \"/test.txt\"},\n\t\tPlatform: \"linux/amd64\",\n\t}\n\n\toutput := docker.Run(t, \"gruntwork/packer-hello-world-example\", opts)\n\tassert.Equal(t, \"Hello, World!\", output)\n}\n"
  },
  {
    "path": "test/packer_oci_example_test.go",
    "content": "package test\n\nimport (\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/oci\"\n\t\"github.com/gruntwork-io/terratest/modules/packer\"\n)\n\n// An example of how to test the Packer template in examples/packer-basic-example using Terratest.\nfunc TestPackerOciExample(t *testing.T) {\n\tt.Parallel()\n\n\t// The Terratest CI environment does not yet have CI creds set up, so we skip these tests for now\n\t// https://github.com/gruntwork-io/terratest/issues/160\n\tif os.Getenv(\"CIRCLECI\") != \"\" {\n\t\tt.Skip(\"The build is running on CircleCI, so skipping OCI tests.\")\n\t}\n\n\tcompartmentID := oci.GetRootCompartmentID(t)\n\tbaseImageID := oci.GetMostRecentImageID(t, compartmentID, \"Canonical Ubuntu\", \"18.04\")\n\tavailabilityDomain := oci.GetRandomAvailabilityDomain(t, compartmentID)\n\tsubnetID := oci.GetRandomSubnetID(t, compartmentID, availabilityDomain)\n\tpassPhrase := oci.GetPassPhraseFromEnvVar()\n\n\tpackerOptions := &packer.Options{\n\t\t// The path to where the Packer template is located\n\t\tTemplate: \"../examples/packer-basic-example/build.pkr.hcl\",\n\n\t\t// Variables to pass to our Packer build using -var options\n\t\tVars: map[string]string{\n\t\t\t\"oci_compartment_ocid\":    compartmentID,\n\t\t\t\"oci_base_image_ocid\":     baseImageID,\n\t\t\t\"oci_availability_domain\": availabilityDomain,\n\t\t\t\"oci_subnet_ocid\":         subnetID,\n\t\t\t\"oci_pass_phrase\":         passPhrase,\n\t\t},\n\n\t\t// Only build an OCI image\n\t\tOnly: \"oracle-oci\",\n\n\t\t// Configure retries for intermittent errors\n\t\tRetryableErrors:    DefaultRetryablePackerErrors,\n\t\tTimeBetweenRetries: DefaultTimeBetweenPackerRetries,\n\t\tMaxRetries:         DefaultMaxPackerRetries,\n\t}\n\n\t// Make sure the Packer build completes successfully\n\tocid := packer.BuildArtifact(t, packerOptions)\n\n\t// Delete the OCI image after we're done\n\tdefer oci.DeleteImage(t, ocid)\n}\n"
  },
  {
    "path": "test/terraform_aws_dynamodb_example_test.go",
    "content": "package test\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\tawsSDK \"github.com/aws/aws-sdk-go-v2/aws\"\n\t\"github.com/aws/aws-sdk-go-v2/service/dynamodb/types\"\n\t\"github.com/gruntwork-io/terratest/modules/aws\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/gruntwork-io/terratest/modules/terraform\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\n// An example of how to test the Terraform module in examples/terraform-aws-dynamodb-example using Terratest.\nfunc TestTerraformAwsDynamoDBExample(t *testing.T) {\n\tt.Parallel()\n\n\t// Pick a random AWS region to test in. This helps ensure your code works in all regions.\n\tawsRegion := aws.GetRandomStableRegion(t, nil, nil)\n\n\t// Set up expected values to be checked later\n\texpectedTableName := fmt.Sprintf(\"terratest-aws-dynamodb-example-table-%s\", random.UniqueId())\n\texpectedKmsKeyArn := aws.GetCmkArn(t, awsRegion, \"alias/aws/dynamodb\")\n\texpectedKeySchema := []types.KeySchemaElement{\n\t\t{AttributeName: awsSDK.String(\"userId\"), KeyType: types.KeyTypeHash},\n\t\t{AttributeName: awsSDK.String(\"department\"), KeyType: types.KeyTypeRange},\n\t}\n\texpectedTags := []types.Tag{\n\t\t{Key: awsSDK.String(\"Environment\"), Value: awsSDK.String(\"production\")},\n\t}\n\n\t// Construct the terraform options with default retryable errors to handle the most common retryable errors in\n\t// terraform testing.\n\tterraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{\n\t\t// The path to where our Terraform code is located\n\t\tTerraformDir: \"../examples/terraform-aws-dynamodb-example\",\n\n\t\t// Variables to pass to our Terraform code using -var options\n\t\tVars: map[string]interface{}{\n\t\t\t\"table_name\": expectedTableName,\n\t\t\t\"region\":     awsRegion,\n\t\t},\n\t})\n\n\t// At the end of the test, run `terraform destroy` to clean up any resources that were created\n\tdefer terraform.Destroy(t, terraformOptions)\n\n\t// This will run `terraform init` and `terraform apply` and fail the test if there are any errors\n\tterraform.InitAndApply(t, terraformOptions)\n\n\t// Look up the DynamoDB table by name\n\ttable := aws.GetDynamoDBTable(t, awsRegion, expectedTableName)\n\n\tassert.Equal(t, \"ACTIVE\", string(table.TableStatus))\n\tassert.ElementsMatch(t, expectedKeySchema, table.KeySchema)\n\n\t// Verify server-side encryption configuration\n\tassert.Equal(t, expectedKmsKeyArn, awsSDK.ToString(table.SSEDescription.KMSMasterKeyArn))\n\tassert.Equal(t, \"ENABLED\", string(table.SSEDescription.Status))\n\tassert.Equal(t, \"KMS\", string(table.SSEDescription.SSEType))\n\n\t// Verify TTL configuration\n\tttl := aws.GetDynamoDBTableTimeToLive(t, awsRegion, expectedTableName)\n\tassert.Equal(t, \"expires\", awsSDK.ToString(ttl.AttributeName))\n\tassert.Equal(t, \"ENABLED\", string(ttl.TimeToLiveStatus))\n\n\t// Verify resource tags\n\ttags := aws.GetDynamoDbTableTags(t, awsRegion, expectedTableName)\n\tassert.ElementsMatch(t, expectedTags, tags)\n}\n"
  },
  {
    "path": "test/terraform_aws_ec2_windows_test.go",
    "content": "package test\n\nimport (\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/aws\"\n\t\"github.com/gruntwork-io/terratest/modules/packer\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/gruntwork-io/terratest/modules/terraform\"\n\ttest_structure \"github.com/gruntwork-io/terratest/modules/test-structure\"\n)\n\nfunc TestWindowsInstance(t *testing.T) {\n\t// Uncomment any of the following to skip that section during the test\n\t//os.Setenv(\"SKIP_setup\", \"true\")\n\t//os.Setenv(\"SKIP_build_ami\", \"true\")\n\t//os.Setenv(\"SKIP_deploy\", \"true\")\n\t//os.Setenv(\"SKIP_validate\", \"true\")\n\t//os.Setenv(\"SKIP_cleanup\", \"true\")\n\n\tworkingDir := filepath.Join(\".\", \"stages\", t.Name())\n\ttestBasePath := test_structure.CopyTerraformFolderToTemp(t, \"..\", \"examples/terraform-aws-ec2-windows-example\")\n\n\ttest_structure.RunTestStage(t, \"setup\", func() {\n\t\tuniqueID := random.UniqueId()\n\t\tregion := aws.GetRandomRegion(t, []string{}, []string{})\n\t\troleName := fmt.Sprintf(\"%s-test-role\", uniqueID)\n\n\t\tinstanceType := aws.GetRecommendedInstanceType(t, region, []string{\"t2.micro, t3.micro\", \"t2.small\", \"t3.small\"})\n\t\ttest_structure.SaveString(t, workingDir, \"region\", region)\n\t\ttest_structure.SaveString(t, workingDir, \"uniqueID\", uniqueID)\n\t\ttest_structure.SaveString(t, workingDir, \"instanceType\", instanceType)\n\t\ttest_structure.SaveString(t, workingDir, \"roleName\", roleName)\n\t})\n\n\ttest_structure.RunTestStage(t, \"build_ami\", func() {\n\t\tregion := test_structure.LoadString(t, workingDir, \"region\")\n\t\tinstanceType := test_structure.LoadString(t, workingDir, \"instanceType\")\n\t\troleName := test_structure.LoadString(t, workingDir, \"roleName\")\n\n\t\tvarsMap := make(map[string]string)\n\n\t\tvarsMap[\"instance_type\"] = instanceType\n\t\tvarsMap[\"region\"] = region\n\t\tpackerOptions := &packer.Options{\n\t\t\tTemplate: filepath.Join(testBasePath, \"packer/build.pkr.hcl\"),\n\t\t\tVars:     varsMap,\n\t\t}\n\n\t\tamiID := packer.BuildArtifact(t, packerOptions)\n\n\t\ttest_structure.SaveString(t, workingDir, \"amiID\", amiID)\n\n\t\tterratestOptions := &terraform.Options{\n\t\t\tTerraformDir: testBasePath,\n\t\t\tVars:         make(map[string]interface{}),\n\t\t}\n\n\t\tterratestOptions.Vars[\"ami\"] = amiID\n\t\tterratestOptions.Vars[\"region\"] = region\n\t\tterratestOptions.Vars[\"iam_role_name\"] = roleName\n\t\ttest_structure.SaveTerraformOptions(t, workingDir, terratestOptions)\n\t})\n\n\tdefer test_structure.RunTestStage(t, \"cleanup\", func() {\n\t\tterratestOptions := test_structure.LoadTerraformOptions(t, workingDir)\n\t\tterraform.Destroy(t, terratestOptions)\n\t})\n\n\ttest_structure.RunTestStage(t, \"deploy\", func() {\n\t\tterratestOptions := test_structure.LoadTerraformOptions(t, workingDir)\n\t\tterraform.InitAndApply(t, terratestOptions)\n\t})\n\n}\n"
  },
  {
    "path": "test/terraform_aws_ecs_example_test.go",
    "content": "package test\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/aws/aws-sdk-go-v2/service/ecs/types\"\n\t\"github.com/gruntwork-io/terratest/modules/aws\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/gruntwork-io/terratest/modules/terraform\"\n\n\tawsSDK \"github.com/aws/aws-sdk-go-v2/aws\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\n// An example of how to test the Terraform module in examples/terraform-aws-ecs-example using Terratest.\nfunc TestTerraformAwsEcsExample(t *testing.T) {\n\tt.Parallel()\n\n\texpectedClusterName := fmt.Sprintf(\"terratest-aws-ecs-example-cluster-%s\", random.UniqueId())\n\texpectedServiceName := fmt.Sprintf(\"terratest-aws-ecs-example-service-%s\", random.UniqueId())\n\n\t// Pick a random AWS region to test in. This helps ensure your code works in all regions.\n\tawsRegion := aws.GetRandomStableRegion(t, []string{\"us-east-1\", \"eu-west-1\"}, nil)\n\n\t// Construct the terraform options with default retryable errors to handle the most common retryable errors in\n\t// terraform testing.\n\tterraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{\n\t\t// The path to where our Terraform code is located\n\t\tTerraformDir: \"../examples/terraform-aws-ecs-example\",\n\n\t\t// Variables to pass to our Terraform code using -var options\n\t\tVars: map[string]interface{}{\n\t\t\t\"cluster_name\": expectedClusterName,\n\t\t\t\"service_name\": expectedServiceName,\n\t\t\t\"region\":       awsRegion,\n\t\t},\n\t})\n\n\t// At the end of the test, run `terraform destroy` to clean up any resources that were created\n\tdefer terraform.Destroy(t, terraformOptions)\n\n\t// This will run `terraform init` and `terraform apply` and fail the test if there are any errors\n\tterraform.InitAndApply(t, terraformOptions)\n\n\t// Run `terraform output` to get the value of an output variable\n\ttaskDefinition := terraform.Output(t, terraformOptions, \"task_definition\")\n\n\t// Look up the ECS cluster by name\n\tcluster := aws.GetEcsCluster(t, awsRegion, expectedClusterName)\n\n\tassert.Equal(t, int32(1), cluster.ActiveServicesCount)\n\n\t// Look up the ECS service by name\n\tservice := aws.GetEcsService(t, awsRegion, expectedClusterName, expectedServiceName)\n\n\tassert.Equal(t, int32(0), service.DesiredCount)\n\tassert.Equal(t, types.LaunchTypeFargate, service.LaunchType)\n\n\t// Look up the ECS task definition by ARN\n\ttask := aws.GetEcsTaskDefinition(t, awsRegion, taskDefinition)\n\n\tassert.Equal(t, \"256\", awsSDK.ToString(task.Cpu))\n\tassert.Equal(t, \"512\", awsSDK.ToString(task.Memory))\n\tassert.Equal(t, types.NetworkModeAwsvpc, task.NetworkMode)\n}\n"
  },
  {
    "path": "test/terraform_aws_example_plan_test.go",
    "content": "package test\n\nimport (\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/aws\"\n\t\"github.com/gruntwork-io/terratest/modules/k8s\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/gruntwork-io/terratest/modules/terraform\"\n\ttest_structure \"github.com/gruntwork-io/terratest/modules/test-structure\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\n// An example of how to test the Terraform module in examples/terraform-aws-example using Terratest.\nfunc TestTerraformAwsExamplePlan(t *testing.T) {\n\tt.Parallel()\n\n\t// Make a copy of the terraform module to a temporary directory. This allows running multiple tests in parallel\n\t// against the same terraform module.\n\texampleFolder := test_structure.CopyTerraformFolderToTemp(t, \"../\", \"examples/terraform-aws-example\")\n\n\t// Give this EC2 Instance a unique ID for a name tag so we can distinguish it from any other EC2 Instance running\n\t// in your AWS account\n\texpectedName := fmt.Sprintf(\"terratest-aws-example-%s\", random.UniqueId())\n\n\t// Pick a random AWS region to test in. This helps ensure your code works in all regions.\n\tawsRegion := aws.GetRandomStableRegion(t, nil, nil)\n\n\t// Some AWS regions are missing certain instance types, so pick an available type based on the region we picked\n\tinstanceType := aws.GetRecommendedInstanceType(t, awsRegion, []string{\"t2.micro, t3.micro\", \"t2.small\", \"t3.small\"})\n\n\t// website::tag::1::Configure Terraform setting path to Terraform code, EC2 instance name, and AWS Region. We also\n\t// configure the options with default retryable errors to handle the most common retryable errors encountered in\n\t// terraform testing.\n\tplanFilePath := filepath.Join(exampleFolder, \"plan.out\")\n\tterraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{\n\t\t// The path to where our Terraform code is located\n\t\tTerraformDir: \"../examples/terraform-aws-example\",\n\n\t\t// Variables to pass to our Terraform code using -var options\n\t\tVars: map[string]interface{}{\n\t\t\t\"instance_name\": expectedName,\n\t\t\t\"instance_type\": instanceType,\n\t\t},\n\n\t\t// Environment variables to set when running Terraform\n\t\tEnvVars: map[string]string{\n\t\t\t\"AWS_DEFAULT_REGION\": awsRegion,\n\t\t},\n\n\t\t// Configure a plan file path so we can introspect the plan and make assertions about it.\n\t\tPlanFilePath: planFilePath,\n\t})\n\n\t// website::tag::2::Run `terraform init`, `terraform plan`, and `terraform show` and fail the test if there are any errors\n\tplan := terraform.InitAndPlanAndShowWithStruct(t, terraformOptions)\n\n\t// website::tag::3::Use the go struct to introspect the plan values.\n\tterraform.RequirePlannedValuesMapKeyExists(t, plan, \"aws_instance.example\")\n\tec2Resource := plan.ResourcePlannedValuesMap[\"aws_instance.example\"]\n\tec2Tags := ec2Resource.AttributeValues[\"tags\"].(map[string]interface{})\n\tassert.Equal(t, map[string]interface{}{\"Name\": expectedName}, ec2Tags)\n\n\t// website::tag::4::Alternatively, you can get the direct JSON output and use jsonpath to extract the data.\n\t// jsonpath only returns lists.\n\tvar jsonEC2Tags []map[string]interface{}\n\tjsonOut := terraform.InitAndPlanAndShow(t, terraformOptions)\n\tk8s.UnmarshalJSONPath(\n\t\tt,\n\t\t[]byte(jsonOut),\n\t\t\"{ .planned_values.root_module.resources[0].values.tags }\",\n\t\t&jsonEC2Tags,\n\t)\n\tassert.Equal(t, map[string]interface{}{\"Name\": expectedName}, jsonEC2Tags[0])\n}\n"
  },
  {
    "path": "test/terraform_aws_example_test.go",
    "content": "package test\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/aws\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/gruntwork-io/terratest/modules/terraform\"\n\ttest_structure \"github.com/gruntwork-io/terratest/modules/test-structure\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\n// An example of how to test the Terraform module in examples/terraform-aws-example using Terratest.\nfunc TestTerraformAwsExample(t *testing.T) {\n\tt.Parallel()\n\n\t// Make a copy of the terraform module to a temporary directory. This allows running multiple tests in parallel\n\t// against the same terraform module.\n\texampleFolder := test_structure.CopyTerraformFolderToTemp(t, \"../\", \"examples/terraform-aws-example\")\n\n\t// Give this EC2 Instance a unique ID for a name tag so we can distinguish it from any other EC2 Instance running\n\t// in your AWS account\n\texpectedName := fmt.Sprintf(\"terratest-aws-example-%s\", random.UniqueId())\n\n\t// Pick a random AWS region to test in. This helps ensure your code works in all regions.\n\tawsRegion := aws.GetRandomStableRegion(t, nil, nil)\n\n\t// Some AWS regions are missing certain instance types, so pick an available type based on the region we picked\n\tinstanceType := aws.GetRecommendedInstanceType(t, awsRegion, []string{\"t2.micro\", \"t3.micro\"})\n\n\t// website::tag::1::Configure Terraform setting path to Terraform code, EC2 instance name, and AWS Region. We also\n\t// configure the options with default retryable errors to handle the most common retryable errors encountered in\n\t// terraform testing.\n\tterraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{\n\t\t// The path to where our Terraform code is located\n\t\tTerraformDir: exampleFolder,\n\n\t\t// Variables to pass to our Terraform code using -var options\n\t\tVars: map[string]interface{}{\n\t\t\t\"instance_name\": expectedName,\n\t\t\t\"instance_type\": instanceType,\n\t\t},\n\n\t\t// Environment variables to set when running Terraform\n\t\tEnvVars: map[string]string{\n\t\t\t\"AWS_DEFAULT_REGION\": awsRegion,\n\t\t},\n\t})\n\n\t// website::tag::4::At the end of the test, run `terraform destroy` to clean up any resources that were created\n\tdefer terraform.Destroy(t, terraformOptions)\n\n\t// website::tag::2::Run `terraform init` and `terraform apply` and fail the test if there are any errors\n\tterraform.InitAndApply(t, terraformOptions)\n\n\t// Run `terraform output` to get the value of an output variable\n\tinstanceID := terraform.Output(t, terraformOptions, \"instance_id\")\n\n\taws.AddTagsToResource(t, awsRegion, instanceID, map[string]string{\"testing\": \"testing-tag-value\"})\n\n\t// Look up the tags for the given Instance ID\n\tinstanceTags := aws.GetTagsForEc2Instance(t, awsRegion, instanceID)\n\n\t// website::tag::3::Check if the EC2 instance with a given tag and name is set.\n\ttestingTag, containsTestingTag := instanceTags[\"testing\"]\n\tassert.True(t, containsTestingTag)\n\tassert.Equal(t, \"testing-tag-value\", testingTag)\n\n\t// Verify that our expected name tag is one of the tags\n\tnameTag, containsNameTag := instanceTags[\"Name\"]\n\tassert.True(t, containsNameTag)\n\tassert.Equal(t, expectedName, nameTag)\n}\n"
  },
  {
    "path": "test/terraform_aws_hello_world_example_test.go",
    "content": "package test\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\t\"time\"\n\n\thttp_helper \"github.com/gruntwork-io/terratest/modules/http-helper\"\n\n\t\"github.com/gruntwork-io/terratest/modules/terraform\"\n)\n\nfunc TestTerraformAwsHelloWorldExample(t *testing.T) {\n\tt.Parallel()\n\n\t// website::tag::2:: Construct the terraform options with default retryable errors to handle the most common\n\t// retryable errors in terraform testing.\n\tterraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{\n\t\t// website::tag::1:: The path to where our Terraform code is located\n\t\tTerraformDir: \"../examples/terraform-aws-hello-world-example\",\n\t})\n\n\t// website::tag::6:: At the end of the test, run `terraform destroy` to clean up any resources that were created.\n\tdefer terraform.Destroy(t, terraformOptions)\n\n\t// website::tag::3:: Run `terraform init` and `terraform apply`. Fail the test if there are any errors.\n\tterraform.InitAndApply(t, terraformOptions)\n\n\t// website::tag::4:: Run `terraform output` to get the IP of the instance\n\tpublicIp := terraform.Output(t, terraformOptions, \"public_ip\")\n\n\t// website::tag::5:: Make an HTTP request to the instance and make sure we get back a 200 OK with the body \"Hello, World!\"\n\turl := fmt.Sprintf(\"http://%s:8080\", publicIp)\n\thttp_helper.HttpGetWithRetry(t, url, nil, 200, \"Hello, World!\", 30, 5*time.Second)\n}\n"
  },
  {
    "path": "test/terraform_aws_lambda_example_test.go",
    "content": "package test\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/aws\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/gruntwork-io/terratest/modules/shell\"\n\t\"github.com/gruntwork-io/terratest/modules/terraform\"\n\ttest_structure \"github.com/gruntwork-io/terratest/modules/test-structure\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// An example of how to test the Terraform module in examples/terraform-aws-lambda-example using Terratest.\nfunc TestTerraformAwsLambdaExample(t *testing.T) {\n\tt.Parallel()\n\n\t// Make a copy of the terraform module to a temporary directory. This allows running multiple tests in parallel\n\t// against the same terraform module.\n\texampleFolder := test_structure.CopyTerraformFolderToTemp(t, \"../\", \"examples/terraform-aws-lambda-example\")\n\n\terr := buildLambdaBinary(t, exampleFolder)\n\trequire.NoError(t, err)\n\n\t// Give this lambda function a unique ID for a name so we can distinguish it from any other lambdas\n\t// in your AWS account\n\tfunctionName := fmt.Sprintf(\"terratest-aws-lambda-example-%s\", random.UniqueId())\n\n\t// Pick a random AWS region to test in. This helps ensure your code works in all regions.\n\tawsRegion := aws.GetRandomStableRegion(t, nil, nil)\n\n\t// Construct the terraform options with default retryable errors to handle the most common retryable errors in\n\t// terraform testing.\n\tterraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{\n\t\t// The path to where our Terraform code is located\n\t\tTerraformDir: exampleFolder,\n\n\t\t// Variables to pass to our Terraform code using -var options\n\t\tVars: map[string]interface{}{\n\t\t\t\"function_name\": functionName,\n\t\t\t\"region\":        awsRegion,\n\t\t},\n\t})\n\n\t// At the end of the test, run `terraform destroy` to clean up any resources that were created\n\tdefer terraform.Destroy(t, terraformOptions)\n\n\t// This will run `terraform init` and `terraform apply` and fail the test if there are any errors\n\tterraform.InitAndApply(t, terraformOptions)\n\n\t// Invoke the function, so we can test its output\n\tresponse := aws.InvokeFunction(t, awsRegion, functionName, ExampleFunctionPayload{ShouldFail: false, Echo: \"hi!\"})\n\n\t// This function just echos it's input as a JSON string when `ShouldFail` is `false``\n\tassert.Equal(t, `\"hi!\"`, string(response))\n\n\t// Invoke the function, this time causing it to error and capturing the error\n\t_, err = aws.InvokeFunctionE(t, awsRegion, functionName, ExampleFunctionPayload{ShouldFail: true, Echo: \"hi!\"})\n\n\t// Function-specific errors have their own special return\n\tfunctionError, ok := err.(*aws.FunctionError)\n\trequire.True(t, ok)\n\n\t// Make sure the function-specific error comes back\n\tassert.Contains(t, string(functionError.Payload), \"failed to handle\")\n}\n\nfunc buildLambdaBinary(t *testing.T, tempDir string) error {\n\tcmd := shell.Command{\n\t\tCommand: \"go\",\n\t\tArgs: []string{\n\t\t\t\"build\",\n\t\t\t\"-o\",\n\t\t\ttempDir + \"/src/bootstrap\",\n\t\t\ttempDir + \"/src/bootstrap.go\",\n\t\t},\n\t\tEnv: map[string]string{\n\t\t\t\"GOOS\":        \"linux\",\n\t\t\t\"GOARCH\":      \"amd64\",\n\t\t\t\"CGO_ENABLED\": \"0\",\n\t\t},\n\t}\n\n\t_, err := shell.RunCommandAndGetOutputE(t, cmd)\n\treturn err\n}\n\n// Another example of how to test the Terraform module in\n// examples/terraform-aws-lambda-example using Terratest, this time with\n// the aws.InvokeFunctionWithParams.\nfunc TestTerraformAwsLambdaWithParamsExample(t *testing.T) {\n\tt.Parallel()\n\n\t// Make a copy of the terraform module to a temporary directory. This allows running multiple tests in parallel\n\t// against the same terraform module.\n\texampleFolder := test_structure.CopyTerraformFolderToTemp(t, \"../\", \"examples/terraform-aws-lambda-example\")\n\n\terr := buildLambdaBinary(t, exampleFolder)\n\trequire.NoError(t, err)\n\n\t// Give this lambda function a unique ID for a name so we can distinguish it from any other lambdas\n\t// in your AWS account\n\tfunctionName := fmt.Sprintf(\"terratest-aws-lambda-withparams-example-%s\", random.UniqueId())\n\n\t// Pick a random AWS region to test in. This helps ensure your code works in all regions.\n\tawsRegion := aws.GetRandomStableRegion(t, nil, nil)\n\n\t// Construct the terraform options with default retryable errors to handle the most common retryable errors in\n\t// terraform testing.\n\tterraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{\n\t\t// The path to where our Terraform code is located\n\t\tTerraformDir: exampleFolder,\n\n\t\t// Variables to pass to our Terraform code using -var options\n\t\tVars: map[string]interface{}{\n\t\t\t\"function_name\": functionName,\n\t\t\t\"region\":        awsRegion,\n\t\t},\n\t})\n\n\t// At the end of the test, run `terraform destroy` to clean up any resources that were created\n\tdefer terraform.Destroy(t, terraformOptions)\n\n\t// This will run `terraform init` and `terraform apply` and fail the test if there are any errors\n\tterraform.InitAndApply(t, terraformOptions)\n\n\t// Call InvokeFunctionWithParms with an InvocationType of \"DryRun\".\n\t// A \"DryRun\" invocation does not execute the function, so the example\n\t// test function will not be checking the payload.\n\tvar invocationType aws.InvocationTypeOption = aws.InvocationTypeDryRun\n\tinput := &aws.LambdaOptions{InvocationType: &invocationType}\n\tout := aws.InvokeFunctionWithParams(t, awsRegion, functionName, input)\n\n\t// With \"DryRun\", there's no message in the output, but there is\n\t// a status code which will have a value of 204 for a successful\n\t// invocation.\n\tassert.Equal(t, int(out.StatusCode), 204)\n\n\t// Invoke the function, this time causing the Lambda to error and\n\t// capturing the error.\n\tinvocationType = aws.InvocationTypeRequestResponse\n\tinput = &aws.LambdaOptions{\n\t\tInvocationType: &invocationType,\n\t\tPayload:        ExampleFunctionPayload{ShouldFail: true, Echo: \"hi!\"},\n\t}\n\tout, err = aws.InvokeFunctionWithParamsE(t, awsRegion, functionName, input)\n\n\t// The Lambda executed, but should have failed.\n\tassert.Error(t, err, \"Unhandled\")\n\n\t// Make sure the function-specific error comes back\n\tassert.Contains(t, string(out.Payload), \"failed to handle\")\n\n\t// Call InvokeFunctionWithParamsE with a LambdaOptions struct that has\n\t// an unsupported InvocationType.  The function should fail.\n\tinvocationType = \"Event\"\n\tinput = &aws.LambdaOptions{\n\t\tInvocationType: &invocationType,\n\t\tPayload:        ExampleFunctionPayload{ShouldFail: false, Echo: \"hi!\"},\n\t}\n\tout, err = aws.InvokeFunctionWithParamsE(t, awsRegion, functionName, input)\n\trequire.NotNil(t, err)\n\tassert.Contains(t, err.Error(), \"LambdaOptions.InvocationType, if specified, must either be \\\"RequestResponse\\\" or \\\"DryRun\\\"\")\n}\n\ntype ExampleFunctionPayload struct {\n\tEcho       string\n\tShouldFail bool\n}\n"
  },
  {
    "path": "test/terraform_aws_network_example_test.go",
    "content": "package test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/aws\"\n\t\"github.com/gruntwork-io/terratest/modules/terraform\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// An example of how to test the Terraform module in examples/terraform-aws-network-example using Terratest.\nfunc TestTerraformAwsNetworkExample(t *testing.T) {\n\tt.Parallel()\n\n\t// Pick a random AWS region to test in. This helps ensure your code works in all regions.\n\tawsRegion := aws.GetRandomStableRegion(t, nil, nil)\n\n\t// Give the VPC and the subnets correct CIDRs\n\tvpcCidr := \"10.10.0.0/16\"\n\tprivateSubnetCidr := \"10.10.1.0/24\"\n\tpublicSubnetCidr := \"10.10.2.0/24\"\n\n\t// Construct the terraform options with default retryable errors to handle the most common retryable errors in\n\t// terraform testing.\n\tterraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{\n\t\t// The path to where our Terraform code is located\n\t\tTerraformDir: \"../examples/terraform-aws-network-example\",\n\n\t\t// Variables to pass to our Terraform code using -var options\n\t\tVars: map[string]interface{}{\n\t\t\t\"main_vpc_cidr\":       vpcCidr,\n\t\t\t\"private_subnet_cidr\": privateSubnetCidr,\n\t\t\t\"public_subnet_cidr\":  publicSubnetCidr,\n\t\t\t\"aws_region\":          awsRegion,\n\t\t},\n\t})\n\n\t// At the end of the test, run `terraform destroy` to clean up any resources that were created\n\tdefer terraform.Destroy(t, terraformOptions)\n\n\t// This will run `terraform init` and `terraform apply` and fail the test if there are any errors\n\tterraform.InitAndApply(t, terraformOptions)\n\n\t// Run `terraform output` to get the value of an output variable\n\tpublicSubnetId := terraform.Output(t, terraformOptions, \"public_subnet_id\")\n\tprivateSubnetId := terraform.Output(t, terraformOptions, \"private_subnet_id\")\n\tvpcId := terraform.Output(t, terraformOptions, \"main_vpc_id\")\n\n\tsubnets := aws.GetSubnetsForVpc(t, vpcId, awsRegion)\n\n\trequire.Equal(t, 2, len(subnets))\n\t// Verify if the network that is supposed to be public is really public\n\tassert.True(t, aws.IsPublicSubnet(t, publicSubnetId, awsRegion))\n\t// Verify if the network that is supposed to be private is really private\n\tassert.False(t, aws.IsPublicSubnet(t, privateSubnetId, awsRegion))\n}\n"
  },
  {
    "path": "test/terraform_aws_rds_example_test.go",
    "content": "package test\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/aws\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/gruntwork-io/terratest/modules/terraform\"\n\ttest_structure \"github.com/gruntwork-io/terratest/modules/test-structure\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\n// An example of how to test the Terraform module in examples/terraform-aws-rds-example using Terratest.\nfunc TestTerraformAwsRdsExample(t *testing.T) {\n\tttable := []struct {\n\t\tname string\n\n\t\tengineName         string\n\t\tmajorEngineVersion string\n\t\tengineFamily       string\n\t\tlicenseModel       string\n\t\tschemaCheck        func(t *testing.T, dbUrl string, dbPort int32, dbUsername string, dbPassword string, expectedSchemaName string) bool\n\t\texpectedOptins     map[struct {\n\t\t\topName  string\n\t\t\tsetName string\n\t\t}]string\n\t\texpectedParameter map[string]string\n\t}{\n\t\t{\n\t\t\tname:               \"mysql\",\n\t\t\tengineName:         \"mysql\",\n\t\t\tmajorEngineVersion: \"5.7\",\n\t\t\tengineFamily:       \"mysql5.7\",\n\t\t\tlicenseModel:       \"general-public-license\",\n\t\t\tschemaCheck: func(t *testing.T, dbUrl string, dbPort int32, dbUsername, dbPassword, expectedSchemaName string) bool {\n\t\t\t\treturn aws.GetWhetherSchemaExistsInRdsMySqlInstance(t, dbUrl, dbPort, dbUsername, dbPassword, expectedSchemaName)\n\t\t\t},\n\t\t\texpectedOptins: map[struct {\n\t\t\t\topName  string\n\t\t\t\tsetName string\n\t\t\t}]string{\n\t\t\t\t{opName: \"MARIADB_AUDIT_PLUGIN\", setName: \"SERVER_AUDIT_EVENTS\"}: \"CONNECT\",\n\t\t\t},\n\t\t\texpectedParameter: map[string]string{\n\t\t\t\t\"general_log\":           \"0\",\n\t\t\t\t\"allow-suspicious-udfs\": \"\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:               \"postgres\",\n\t\t\tengineName:         \"postgres\",\n\t\t\tmajorEngineVersion: \"13\",\n\t\t\tengineFamily:       \"postgres13\",\n\t\t\tlicenseModel:       \"postgresql-license\",\n\t\t\tschemaCheck: func(t *testing.T, dbUrl string, dbPort int32, dbUsername, dbPassword, expectedSchemaName string) bool {\n\t\t\t\treturn aws.GetWhetherSchemaExistsInRdsPostgresInstance(t, dbUrl, dbPort, dbUsername, dbPassword, expectedSchemaName)\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range ttable {\n\t\ttt := tt\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\t// Give this RDS Instance a unique ID for a name tag so we can distinguish it from any other RDS Instance running\n\t\t\t// in your AWS account\n\t\t\texpectedName := fmt.Sprintf(\"terratest-aws-rds-example-%s\", strings.ToLower(random.UniqueId()))\n\t\t\texpectedPort := int32(3306)\n\t\t\texpectedDatabaseName := \"terratest\"\n\t\t\tusername := \"username\"\n\t\t\tpassword := \"password\"\n\t\t\t// Pick a random AWS region to test in. This helps ensure your code works in all regions.\n\t\t\tawsRegion := aws.GetRandomStableRegion(t, nil, nil)\n\t\t\tengineVersion := aws.GetValidEngineVersion(t, awsRegion, tt.engineName, tt.majorEngineVersion)\n\t\t\tinstanceType := aws.GetRecommendedRdsInstanceType(t, awsRegion, tt.engineName, engineVersion, []string{\"db.t2.micro\", \"db.t3.micro\", \"db.t3.small\"})\n\t\t\tmoduleFolder := test_structure.CopyTerraformFolderToTemp(t, \"../\", \"examples/terraform-aws-rds-example\")\n\n\t\t\t// Construct the terraform options with default retryable errors to handle the most common retryable errors in\n\t\t\t// terraform testing.\n\t\t\tterraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{\n\t\t\t\t// The path to where our Terraform code is located\n\t\t\t\tTerraformDir: moduleFolder,\n\n\t\t\t\t// Variables to pass to our Terraform code using -var options\n\t\t\t\t// \"username\" and \"password\" should not be passed from here in a production scenario.\n\t\t\t\tVars: map[string]interface{}{\n\t\t\t\t\t\"name\":                 expectedName,\n\t\t\t\t\t\"engine_name\":          tt.engineName,\n\t\t\t\t\t\"major_engine_version\": tt.majorEngineVersion,\n\t\t\t\t\t\"family\":               tt.engineFamily,\n\t\t\t\t\t\"instance_class\":       instanceType,\n\t\t\t\t\t\"username\":             username,\n\t\t\t\t\t\"password\":             password,\n\t\t\t\t\t\"allocated_storage\":    5,\n\t\t\t\t\t\"license_model\":        tt.licenseModel,\n\t\t\t\t\t\"engine_version\":       engineVersion,\n\t\t\t\t\t\"port\":                 expectedPort,\n\t\t\t\t\t\"database_name\":        expectedDatabaseName,\n\t\t\t\t\t\"region\":               awsRegion,\n\t\t\t\t},\n\t\t\t})\n\n\t\t\t// At the end of the test, run `terraform destroy` to clean up any resources that were created\n\t\t\tdefer terraform.Destroy(t, terraformOptions)\n\n\t\t\t// This will run `terraform init` and `terraform apply` and fail the test if there are any errors\n\t\t\tterraform.InitAndApply(t, terraformOptions)\n\n\t\t\t// Run `terraform output` to get the value of an output variable\n\t\t\tdbInstanceID := terraform.Output(t, terraformOptions, \"db_instance_id\")\n\n\t\t\t// Look up the endpoint address and port of the RDS instance\n\t\t\taddress := aws.GetAddressOfRdsInstance(t, dbInstanceID, awsRegion)\n\t\t\tport := aws.GetPortOfRdsInstance(t, dbInstanceID, awsRegion)\n\t\t\tschemaExistsInRdsInstance := tt.schemaCheck(t, address, port, username, password, expectedDatabaseName)\n\t\t\t// Lookup parameter values. All defined values are strings in the API call response\n\n\t\t\t// Verify that the address is not null\n\t\t\tassert.NotNil(t, address)\n\t\t\t// Verify that the DB instance is listening on the port mentioned\n\t\t\tassert.Equal(t, expectedPort, port)\n\t\t\t// Verify that the table/schema requested for creation is actually present in the database\n\t\t\tassert.True(t, schemaExistsInRdsInstance)\n\n\t\t\t// assert expected parameters\n\t\t\tfor k, v := range tt.expectedParameter {\n\t\t\t\tassert.Equal(t, v, aws.GetParameterValueForParameterOfRdsInstance(t, k, dbInstanceID, awsRegion))\n\t\t\t}\n\n\t\t\t// assert all parameters\n\t\t\tparams := aws.GetAllParametersOfRdsInstance(t, dbInstanceID, awsRegion)\n\t\t\tparamNames := map[string]struct{}{}\n\t\t\tfor _, param := range params {\n\t\t\t\tparamNames[*param.ParameterName] = struct{}{}\n\t\t\t}\n\t\t\tassert.Len(t, paramNames, len(params), \"should return no duplicate parameters\")\n\t\t\tassert.True(t, len(paramNames) > 100)\n\n\t\t\t// assert expected options\n\t\t\tfor k, v := range tt.expectedOptins {\n\t\t\t\t// Lookup option values. All defined values are strings in the API call response\n\t\t\t\tassert.Equal(t, v, aws.GetOptionSettingForOfRdsInstance(t, k.opName, k.setName, dbInstanceID, awsRegion))\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "test/terraform_aws_s3_example_test.go",
    "content": "package test\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/aws\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/gruntwork-io/terratest/modules/terraform\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\n// An example of how to test the Terraform module in examples/terraform-aws-s3-example using Terratest.\nfunc TestTerraformAwsS3Example(t *testing.T) {\n\tt.Parallel()\n\n\t// Give this S3 Bucket a unique ID for a name tag so we can distinguish it from any other Buckets provisioned\n\t// in your AWS account\n\texpectedName := fmt.Sprintf(\"terratest-aws-s3-example-%s\", strings.ToLower(random.UniqueId()))\n\n\t// Give this S3 Bucket an environment to operate as a part of for the purposes of resource tagging\n\texpectedEnvironment := \"Automated Testing\"\n\n\t// Pick a random AWS region to test in. This helps ensure your code works in all regions.\n\tawsRegion := aws.GetRandomStableRegion(t, nil, nil)\n\n\t// Construct the terraform options with default retryable errors to handle the most common retryable errors in\n\t// terraform testing.\n\tterraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{\n\t\t// The path to where our Terraform code is located\n\t\tTerraformDir: \"../examples/terraform-aws-s3-example\",\n\n\t\t// Variables to pass to our Terraform code using -var options\n\t\tVars: map[string]interface{}{\n\t\t\t\"tag_bucket_name\":        expectedName,\n\t\t\t\"tag_bucket_environment\": expectedEnvironment,\n\t\t\t\"with_policy\":            \"true\",\n\t\t\t\"region\":                 awsRegion,\n\t\t},\n\t})\n\n\t// At the end of the test, run `terraform destroy` to clean up any resources that were created\n\tdefer terraform.Destroy(t, terraformOptions)\n\n\t// This will run `terraform init` and `terraform apply` and fail the test if there are any errors\n\tterraform.InitAndApply(t, terraformOptions)\n\n\t// Run `terraform output` to get the value of an output variable\n\tbucketID := terraform.Output(t, terraformOptions, \"bucket_id\")\n\n\t// Verify that our Bucket has versioning enabled\n\tactualStatus := aws.GetS3BucketVersioning(t, awsRegion, bucketID)\n\texpectedStatus := \"Enabled\"\n\tassert.Equal(t, expectedStatus, actualStatus)\n\n\t// Verify that our Bucket has a policy attached\n\taws.AssertS3BucketPolicyExists(t, awsRegion, bucketID)\n\n\t// Verify that our bucket has server access logging TargetBucket set to what's expected\n\tloggingTargetBucket := aws.GetS3BucketLoggingTarget(t, awsRegion, bucketID)\n\texpectedLogsTargetBucket := fmt.Sprintf(\"%s-logs\", bucketID)\n\tloggingObjectTargetPrefix := aws.GetS3BucketLoggingTargetPrefix(t, awsRegion, bucketID)\n\texpectedLogsTargetPrefix := \"TFStateLogs/\"\n\n\tassert.Equal(t, expectedLogsTargetBucket, loggingTargetBucket)\n\tassert.Equal(t, expectedLogsTargetPrefix, loggingObjectTargetPrefix)\n}\n"
  },
  {
    "path": "test/terraform_aws_ssm_example_test.go",
    "content": "package test\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/gruntwork-io/terratest/modules/aws\"\n\t\"github.com/gruntwork-io/terratest/modules/terraform\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestTerraformAwsSsmExample(t *testing.T) {\n\tt.Parallel()\n\tregion := aws.GetRandomStableRegion(t, nil, nil)\n\n\t// Some AWS regions are missing certain instance types, so pick an available type based on the region we picked\n\tinstanceType := aws.GetRecommendedInstanceType(t, region, []string{\"t2.micro, t3.micro\", \"t2.small\", \"t3.small\"})\n\n\tterraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{\n\t\tTerraformDir: \"../examples/terraform-aws-ssm-example\",\n\t\tVars: map[string]interface{}{\n\t\t\t\"region\":        region,\n\t\t\t\"instance_type\": instanceType,\n\t\t},\n\t})\n\tdefer terraform.Destroy(t, terraformOptions)\n\n\tterraform.InitAndApply(t, terraformOptions)\n\n\tinstanceID := terraform.Output(t, terraformOptions, \"instance_id\")\n\ttimeout := 3 * time.Minute\n\n\taws.WaitForSsmInstance(t, region, instanceID, timeout)\n\n\tresult := aws.CheckSsmCommand(t, region, instanceID, \"echo Hello, World\", timeout)\n\trequire.Equal(t, result.Stdout, \"Hello, World\\n\")\n\trequire.Equal(t, result.Stderr, \"\")\n\trequire.Equal(t, int64(0), result.ExitCode)\n\n\tresult, err := aws.CheckSsmCommandE(t, region, instanceID, \"cat /wrong/file\", timeout)\n\trequire.Error(t, err)\n\trequire.Equal(t, \"Failed\", err.Error())\n\trequire.Equal(t, \"cat: /wrong/file: No such file or directory\\nfailed to run commands: exit status 1\", result.Stderr)\n\trequire.Equal(t, \"\", result.Stdout)\n\trequire.Equal(t, int64(1), result.ExitCode)\n}\n"
  },
  {
    "path": "test/terraform_backend_example_test.go",
    "content": "package test\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/aws\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/gruntwork-io/terratest/modules/terraform\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// An example of how to test the Terraform module in examples/terraform-backend-example using Terratest.\nfunc TestTerraformBackendExample(t *testing.T) {\n\tt.Parallel()\n\n\tawsRegion := aws.GetRandomRegion(t, nil, nil)\n\tuniqueId := random.UniqueId()\n\n\t// Create an S3 bucket where we can store state\n\tbucketName := fmt.Sprintf(\"test-terraform-backend-example-%s\", strings.ToLower(uniqueId))\n\tdefer cleanupS3Bucket(t, awsRegion, bucketName)\n\taws.CreateS3Bucket(t, awsRegion, bucketName)\n\n\tkey := fmt.Sprintf(\"%s/terraform.tfstate\", uniqueId)\n\tdata := fmt.Sprintf(\"data-for-test-%s\", uniqueId)\n\n\t// Deploy the module, configuring it to use the S3 bucket as an S3 backend\n\tterraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{\n\t\tTerraformDir: \"../examples/terraform-backend-example\",\n\t\tVars: map[string]interface{}{\n\t\t\t\"foo\": data,\n\t\t},\n\t\tBackendConfig: map[string]interface{}{\n\t\t\t\"bucket\": bucketName,\n\t\t\t\"key\":    key,\n\t\t\t\"region\": awsRegion,\n\t\t},\n\t})\n\n\tdefer terraform.Destroy(t, terraformOptions)\n\tterraform.InitAndApply(t, terraformOptions)\n\n\t// Check a state file actually got stored and contains our data in it somewhere (since that data is used in an\n\t// output of the Terraform code)\n\tcontents := aws.GetS3ObjectContents(t, awsRegion, bucketName, key)\n\trequire.Contains(t, contents, data)\n\n\t// The module doesn't really *do* anything, so we just check a dummy output here and move on\n\tfoo := terraform.OutputRequired(t, terraformOptions, \"foo\")\n\trequire.Equal(t, data, foo)\n}\n\nfunc cleanupS3Bucket(t *testing.T, awsRegion string, bucketName string) {\n\taws.EmptyS3Bucket(t, awsRegion, bucketName)\n\taws.DeleteS3Bucket(t, awsRegion, bucketName)\n}\n"
  },
  {
    "path": "test/terraform_basic_example_regression_test.go",
    "content": "package test\n\nimport (\n\t\"encoding/json\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/gruntwork-io/terratest/modules/terraform\"\n\ttest_structure \"github.com/gruntwork-io/terratest/modules/test-structure\"\n)\n\n// The tests in this folder are not example usage of Terratest. Instead, this is a regression test to ensure the\n// formatting rules work with an actual Terraform call when using more complex structures.\n\nfunc TestTerraformFormatNestedOneLevelList(t *testing.T) {\n\tt.Parallel()\n\n\ttestList := [][]string{\n\t\t[]string{random.UniqueId()},\n\t}\n\n\toptions := GetTerraformOptionsForFormatTests(t)\n\toptions.Vars[\"example_any\"] = testList\n\n\tdefer terraform.Destroy(t, options)\n\tterraform.InitAndApply(t, options)\n\toutputMap := terraform.OutputForKeys(t, options, []string{\"example_any\"})\n\tactualExampleList := outputMap[\"example_any\"]\n\tAssertEqualJson(t, actualExampleList, testList)\n}\n\nfunc TestTerraformFormatNestedTwoLevelList(t *testing.T) {\n\tt.Parallel()\n\n\ttestList := [][][]string{\n\t\t[][]string{[]string{random.UniqueId()}},\n\t}\n\n\toptions := GetTerraformOptionsForFormatTests(t)\n\toptions.Vars[\"example_any\"] = testList\n\n\tdefer terraform.Destroy(t, options)\n\tterraform.InitAndApply(t, options)\n\toutputMap := terraform.OutputForKeys(t, options, []string{\"example_any\"})\n\tactualExampleList := outputMap[\"example_any\"]\n\tAssertEqualJson(t, actualExampleList, testList)\n}\n\nfunc TestTerraformFormatNestedMultipleItems(t *testing.T) {\n\tt.Parallel()\n\n\ttestList := [][]string{\n\t\t[]string{random.UniqueId(), random.UniqueId()},\n\t\t[]string{random.UniqueId(), random.UniqueId(), random.UniqueId()},\n\t}\n\n\toptions := GetTerraformOptionsForFormatTests(t)\n\toptions.Vars[\"example_any\"] = testList\n\n\tdefer terraform.Destroy(t, options)\n\tterraform.InitAndApply(t, options)\n\toutputMap := terraform.OutputForKeys(t, options, []string{\"example_any\"})\n\tactualExampleList := outputMap[\"example_any\"]\n\tAssertEqualJson(t, actualExampleList, testList)\n}\n\nfunc TestTerraformFormatNestedOneLevelMap(t *testing.T) {\n\tt.Parallel()\n\n\ttestMap := map[string]map[string]string{\n\t\t\"test\": map[string]string{\n\t\t\t\"foo\": random.UniqueId(),\n\t\t},\n\t}\n\n\toptions := GetTerraformOptionsForFormatTests(t)\n\toptions.Vars[\"example_any\"] = testMap\n\n\tdefer terraform.Destroy(t, options)\n\tterraform.InitAndApply(t, options)\n\toutputMap := terraform.OutputForKeys(t, options, []string{\"example_any\"})\n\tactualExampleMap := outputMap[\"example_any\"]\n\tAssertEqualJson(t, actualExampleMap, testMap)\n}\n\nfunc TestTerraformFormatNestedTwoLevelMap(t *testing.T) {\n\tt.Parallel()\n\n\ttestMap := map[string]map[string]map[string]string{\n\t\t\"test\": map[string]map[string]string{\n\t\t\t\"foo\": map[string]string{\n\t\t\t\t\"bar\": random.UniqueId(),\n\t\t\t},\n\t\t},\n\t}\n\n\toptions := GetTerraformOptionsForFormatTests(t)\n\toptions.Vars[\"example_any\"] = testMap\n\n\tdefer terraform.Destroy(t, options)\n\tterraform.InitAndApply(t, options)\n\toutputMap := terraform.OutputForKeys(t, options, []string{\"example_any\"})\n\tactualExampleMap := outputMap[\"example_any\"]\n\tAssertEqualJson(t, actualExampleMap, testMap)\n}\n\nfunc TestTerraformFormatNestedMultipleItemsMap(t *testing.T) {\n\tt.Parallel()\n\n\ttestMap := map[string]map[string]string{\n\t\t\"test\": map[string]string{\n\t\t\t\"foo\": random.UniqueId(),\n\t\t\t\"bar\": random.UniqueId(),\n\t\t},\n\t\t\"other\": map[string]string{\n\t\t\t\"baz\": random.UniqueId(),\n\t\t\t\"boo\": random.UniqueId(),\n\t\t},\n\t}\n\n\toptions := GetTerraformOptionsForFormatTests(t)\n\toptions.Vars[\"example_any\"] = testMap\n\n\tdefer terraform.Destroy(t, options)\n\tterraform.InitAndApply(t, options)\n\toutputMap := terraform.OutputForKeys(t, options, []string{\"example_any\"})\n\tactualExampleMap := outputMap[\"example_any\"]\n\tAssertEqualJson(t, actualExampleMap, testMap)\n}\n\nfunc TestTerraformFormatNestedListMap(t *testing.T) {\n\tt.Parallel()\n\n\ttestMap := map[string][]string{\n\t\t\"test\": []string{random.UniqueId(), random.UniqueId()},\n\t}\n\n\toptions := GetTerraformOptionsForFormatTests(t)\n\toptions.Vars[\"example_any\"] = testMap\n\n\tdefer terraform.Destroy(t, options)\n\tterraform.InitAndApply(t, options)\n\toutputMap := terraform.OutputForKeys(t, options, []string{\"example_any\"})\n\tactualExampleMap := outputMap[\"example_any\"]\n\tAssertEqualJson(t, actualExampleMap, testMap)\n}\n\nfunc GetTerraformOptionsForFormatTests(t *testing.T) *terraform.Options {\n\texampleFolder := test_structure.CopyTerraformFolderToTemp(t, \"../\", \"examples/terraform-basic-example\")\n\n\t// Set up terratest to retry on known failures\n\tmaxTerraformRetries := 3\n\tsleepBetweenTerraformRetries := 5 * time.Second\n\tretryableTerraformErrors := map[string]string{\n\t\t// `terraform init` frequently fails in CI due to network issues accessing plugins. The reason is unknown, but\n\t\t// eventually these succeed after a few retries.\n\t\t\".*unable to verify signature.*\":             \"Failed to retrieve plugin due to transient network error.\",\n\t\t\".*unable to verify checksum.*\":              \"Failed to retrieve plugin due to transient network error.\",\n\t\t\".*no provider exists with the given name.*\": \"Failed to retrieve plugin due to transient network error.\",\n\t\t\".*registry service is unreachable.*\":        \"Failed to retrieve plugin due to transient network error.\",\n\t\t\".*connection reset by peer.*\":               \"Failed to retrieve plugin due to transient network error.\",\n\t}\n\n\tterraformOptions := &terraform.Options{\n\t\tTerraformDir:             exampleFolder,\n\t\tVars:                     map[string]interface{}{},\n\t\tNoColor:                  true,\n\t\tRetryableTerraformErrors: retryableTerraformErrors,\n\t\tMaxRetries:               maxTerraformRetries,\n\t\tTimeBetweenRetries:       sleepBetweenTerraformRetries,\n\t}\n\treturn terraformOptions\n}\n\n// The value of the output nested in the outputMap returned by OutputForKeys uses the interface{} type for nested\n// structures. This can't be compared to actual types like [][]string{}, so we instead compare the json versions.\nfunc AssertEqualJson(t *testing.T, actual interface{}, expected interface{}) {\n\tactualJson, err := json.Marshal(actual)\n\trequire.NoError(t, err)\n\texpectedJson, err := json.Marshal(expected)\n\trequire.NoError(t, err)\n\tassert.Equal(t, actualJson, expectedJson)\n}\n"
  },
  {
    "path": "test/terraform_basic_example_test.go",
    "content": "package test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/terraform\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\n// An example of how to test the simple Terraform module in examples/terraform-basic-example using Terratest.\nfunc TestTerraformBasicExample(t *testing.T) {\n\tt.Parallel()\n\n\texpectedText := \"test\"\n\texpectedList := []string{expectedText}\n\texpectedMap := map[string]string{\"expected\": expectedText}\n\n\tterraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{\n\t\t// website::tag::1::Set the path to the Terraform code that will be tested.\n\t\t// The path to where our Terraform code is located\n\t\tTerraformDir: \"../examples/terraform-basic-example\",\n\n\t\t// Variables to pass to our Terraform code using -var options\n\t\tVars: map[string]interface{}{\n\t\t\t\"example\": expectedText,\n\n\t\t\t// We also can see how lists and maps translate between terratest and terraform.\n\t\t\t\"example_list\": expectedList,\n\t\t\t\"example_map\":  expectedMap,\n\t\t},\n\n\t\t// Variables to pass to our Terraform code using -var-file options\n\t\tVarFiles: []string{\"varfile.tfvars\"},\n\n\t\t// Disable colors in Terraform commands so its easier to parse stdout/stderr\n\t\tNoColor: true,\n\t})\n\n\t// website::tag::4::Clean up resources with \"terraform destroy\". Using \"defer\" runs the command at the end of the test, whether the test succeeds or fails.\n\t// At the end of the test, run `terraform destroy` to clean up any resources that were created\n\tdefer terraform.Destroy(t, terraformOptions)\n\n\t// website::tag::2::Run \"terraform init\" and \"terraform apply\".\n\t// This will run `terraform init` and `terraform apply` and fail the test if there are any errors\n\tterraform.InitAndApply(t, terraformOptions)\n\n\t// Run `terraform output` to get the values of output variables\n\tactualTextExample := terraform.Output(t, terraformOptions, \"example\")\n\tactualTextExample2 := terraform.Output(t, terraformOptions, \"example2\")\n\tactualExampleList := terraform.OutputList(t, terraformOptions, \"example_list\")\n\tactualExampleMap := terraform.OutputMap(t, terraformOptions, \"example_map\")\n\n\t// website::tag::3::Check the output against expected values.\n\t// Verify we're getting back the outputs we expect\n\tassert.Equal(t, expectedText, actualTextExample)\n\tassert.Equal(t, expectedText, actualTextExample2)\n\tassert.Equal(t, expectedList, actualExampleList)\n\tassert.Equal(t, expectedMap, actualExampleMap)\n}\n"
  },
  {
    "path": "test/terraform_database_example_test.go",
    "content": "package test\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/gruntwork-io/terratest/modules/database\"\n\t\"github.com/gruntwork-io/terratest/modules/retry\"\n\t\"github.com/gruntwork-io/terratest/modules/terraform\"\n)\n\nfunc TestTerraformDatabaseExample(t *testing.T) {\n\tt.Parallel()\n\n\tterraformOptions := &terraform.Options{\n\t\t// The path to where our Terraform code is located\n\t\tTerraformDir: \"../examples/terraform-database-example\",\n\n\t\t// Variables to pass to our Terraform code using -var options\n\t\tVars: map[string]interface{}{},\n\t}\n\n\t// At the end of the test, run `terraform destroy` to clean up any resources that were created\n\tdefer terraform.Destroy(t, terraformOptions)\n\n\t// This will run `terraform init` and `terraform apply` and fail the test if there are any errors\n\tterraform.InitAndApply(t, terraformOptions)\n\n\t// Setting database configuration, including host, port, username, password and database name\n\tvar dbConfig database.DBConfig\n\tdbConfig.Host = terraform.Output(t, terraformOptions, \"host\")\n\tdbConfig.Port = terraform.Output(t, terraformOptions, \"port\")\n\tdbConfig.User = terraform.Output(t, terraformOptions, \"username\")\n\tdbConfig.Password = terraform.Output(t, terraformOptions, \"password\")\n\tdbConfig.Database = terraform.Output(t, terraformOptions, \"database_name\")\n\n\t// It can take a minute or so for the database to boot up, so retry a few times\n\tmaxRetries := 15\n\ttimeBetweenRetries := 15 * time.Second\n\tdescription := fmt.Sprintf(\"Executing commands on database %s\", dbConfig.Host)\n\n\t// Verify that we can connect to the database and run SQL commands\n\tretry.DoWithRetry(t, description, maxRetries, timeBetweenRetries, func() (string, error) {\n\t\t// Connect to specific database, i.e. postgres\n\t\tdb, err := database.DBConnectionE(t, \"postgres\", dbConfig)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\t// Create a table\n\t\tcreation := \"create table person (id integer, name varchar(30), primary key (id))\"\n\t\tdatabase.DBExecution(t, db, creation)\n\n\t\t// Insert a row\n\t\texpectedID := 12345\n\t\texpectedName := \"azure\"\n\t\tinsertion := fmt.Sprintf(\"insert into person values (%d, '%s')\", expectedID, expectedName)\n\t\tdatabase.DBExecution(t, db, insertion)\n\n\t\t// Query the table and check the output\n\t\tquery := \"select name from person\"\n\t\tdatabase.DBQueryWithValidation(t, db, query, \"azure\")\n\n\t\t// Drop the table\n\t\tdrop := \"drop table person\"\n\t\tdatabase.DBExecution(t, db, drop)\n\t\tfmt.Println(\"Executed SQL commands correctly\")\n\n\t\tdefer db.Close()\n\n\t\treturn \"\", nil\n\t})\n}\n"
  },
  {
    "path": "test/terraform_hello_world_example_test.go",
    "content": "package test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/terraform\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestTerraformHelloWorldExample(t *testing.T) {\n\t// website::tag::2:: Construct the terraform options with default retryable errors to handle the most common\n\t// retryable errors in terraform testing.\n\tterraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{\n\t\t// website::tag::1:: Set the path to the Terraform code that will be tested.\n\t\tTerraformDir: \"../examples/terraform-hello-world-example\",\n\t})\n\n\t// website::tag::5:: Clean up resources with \"terraform destroy\" at the end of the test.\n\tdefer terraform.Destroy(t, terraformOptions)\n\n\t// website::tag::3:: Run \"terraform init\" and \"terraform apply\". Fail the test if there are any errors.\n\tterraform.InitAndApply(t, terraformOptions)\n\n\t// website::tag::4:: Run `terraform output` to get the values of output variables and check they have the expected values.\n\toutput := terraform.Output(t, terraformOptions, \"hello_world\")\n\tassert.Equal(t, \"Hello, World!\", output)\n}\n"
  },
  {
    "path": "test/terraform_http_example_test.go",
    "content": "package test\n\nimport (\n\t\"crypto/tls\"\n\t\"fmt\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/gruntwork-io/terratest/modules/aws\"\n\thttp_helper \"github.com/gruntwork-io/terratest/modules/http-helper\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/gruntwork-io/terratest/modules/terraform\"\n)\n\n// An example of how to test the Terraform module in examples/terraform-http-example using Terratest.\nfunc TestTerraformHttpExample(t *testing.T) {\n\tt.Parallel()\n\n\t// A unique ID we can use to namespace resources so we don't clash with anything already in the AWS account or\n\t// tests running in parallel\n\tuniqueID := random.UniqueId()\n\n\t// Give this EC2 Instance and other resources in the Terraform code a name with a unique ID so it doesn't clash\n\t// with anything else in the AWS account.\n\tinstanceName := fmt.Sprintf(\"terratest-http-example-%s\", uniqueID)\n\n\t// Specify the text the EC2 Instance will return when we make HTTP requests to it.\n\tinstanceText := fmt.Sprintf(\"Hello, %s!\", uniqueID)\n\n\t// Pick a random AWS region to test in. This helps ensure your code works in all regions.\n\tawsRegion := aws.GetRandomStableRegion(t, nil, nil)\n\n\t// Some AWS regions are missing certain instance types, so pick an available type based on the region we picked\n\tinstanceType := aws.GetRecommendedInstanceType(t, awsRegion, []string{\"t2.micro, t3.micro\", \"t2.small\", \"t3.small\"})\n\n\t// Construct the terraform options with default retryable errors to handle the most common retryable errors in\n\t// terraform testing.\n\tterraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{\n\t\t// The path to where our Terraform code is located\n\t\tTerraformDir: \"../examples/terraform-http-example\",\n\n\t\t// Variables to pass to our Terraform code using -var options\n\t\tVars: map[string]interface{}{\n\t\t\t\"aws_region\":    awsRegion,\n\t\t\t\"instance_name\": instanceName,\n\t\t\t\"instance_text\": instanceText,\n\t\t\t\"instance_type\": instanceType,\n\t\t},\n\t})\n\n\t// At the end of the test, run `terraform destroy` to clean up any resources that were created\n\tdefer terraform.Destroy(t, terraformOptions)\n\n\t// This will run `terraform init` and `terraform apply` and fail the test if there are any errors\n\tterraform.InitAndApply(t, terraformOptions)\n\n\t// Run `terraform output` to get the value of an output variable\n\tinstanceURL := terraform.Output(t, terraformOptions, \"instance_url\")\n\n\t// Setup a TLS configuration to submit with the helper, a blank struct is acceptable\n\ttlsConfig := tls.Config{}\n\n\t// It can take a minute or so for the Instance to boot up, so retry a few times\n\tmaxRetries := 30\n\ttimeBetweenRetries := 5 * time.Second\n\n\t// Verify that we get back a 200 OK with the expected instanceText\n\thttp_helper.HttpGetWithRetry(t, instanceURL, &tlsConfig, 200, instanceText, maxRetries, timeBetweenRetries)\n}\n"
  },
  {
    "path": "test/terraform_opa_example_extra_args_test.go",
    "content": "package test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/opa\"\n\t\"github.com/gruntwork-io/terratest/modules/terraform\"\n)\n\n// TestOPAEvalTerraformModuleWithExtraArgs demonstrates how to pass extra command line arguments to OPA,\n// such as --v0-compatible for backwards compatibility with OPA v0.x.\nfunc TestOPAEvalTerraformModuleWithExtraArgs(t *testing.T) {\n\tt.Parallel()\n\n\ttfOpts := &terraform.Options{\n\t\tTerraformDir: \"../examples/terraform-opa-example/pass\",\n\t}\n\n\topaOpts := &opa.EvalOptions{\n\t\tRulePath: \"../examples/terraform-opa-example/policy/enforce_source_v0.rego\",\n\t\tFailMode: opa.FailUndefined,\n\t\t// Pass extra command line arguments to OPA eval subcommand\n\t\tExtraArgs: []string{\"--v0-compatible\"},\n\t}\n\n\t// This will run: opa eval --v0-compatible --fail -i <jsonfile> -d <rulepath> data.enforce_source.allow\n\tterraform.OPAEval(t, tfOpts, opaOpts, \"data.enforce_source.allow\")\n}\n"
  },
  {
    "path": "test/terraform_opa_example_test.go",
    "content": "package test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/opa\"\n\t\"github.com/gruntwork-io/terratest/modules/terraform\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// An example of how to use Terratest to run OPA policy checks on Terraform source code. This will check the module\n// called `pass` against the rego policy `enforce_source` defined in the `terraform-opa-example` folder.\nfunc TestOPAEvalTerraformModulePassesCheck(t *testing.T) {\n\tt.Parallel()\n\n\ttfOpts := &terraform.Options{\n\t\t// website::tag::1:: Set the path to the Terraform code that will be tested.\n\t\tTerraformDir: \"../examples/terraform-opa-example/pass\",\n\t}\n\n\topaOpts := &opa.EvalOptions{\n\t\t// website::tag::2:: Set the path to the OPA policy code that should be used.\n\t\tRulePath: \"../examples/terraform-opa-example/policy/enforce_source.rego\",\n\n\t\t// website::tag::3:: Run OPA in fail mode so that it will exit with non-zero exit code when the result query is undefined.\n\t\tFailMode: opa.FailUndefined,\n\t}\n\n\t// website::tag::4:: Run OPA with the configured options, querying for the allow variable. The OPAEval function automatically expects the check to pass, failing the test if opa eval exits with non-zero exit code.\n\tterraform.OPAEval(t, tfOpts, opaOpts, \"data.enforce_source.allow\")\n}\n\n// An example of how to use Terratest to run OPA policy checks on Terraform source code. This will check the module\n// called `fail` against the rego policy `enforce_source` defined in the `terraform-opa-example` folder and validate\n// that the module fails the OPA checks.\nfunc TestOPAEvalTerraformModuleFailsCheck(t *testing.T) {\n\tt.Parallel()\n\n\t// website::tag::5:: Configure in a similar fashion to the above test, but run against the `fail` example.\n\tpolicyPath := \"../examples/terraform-opa-example/policy/enforce_source.rego\"\n\ttfOpts := &terraform.Options{TerraformDir: \"../examples/terraform-opa-example/fail\"}\n\topaOpts := &opa.EvalOptions{\n\t\tFailMode: opa.FailUndefined,\n\t\tRulePath: policyPath,\n\t}\n\n\t// website::tag::6:: Here we expect the checks to fail, so we use `OPAEvalE` to check the error. Note that on the files that failed, this function will rerun `opa eval` with the query set to `data`, so you can see the values of all the variables in the policy. This is useful for debugging failures.\n\trequire.Error(t, terraform.OPAEvalE(t, tfOpts, opaOpts, \"data.enforce_source.allow\"))\n}\n\n// An example of how to use Terratest to run OPA policy checks on Terraform source code using a remote OPA policy source\n// file. This will check the module called `pass` against the rego policy `enforce_source` defined in the\n// `terraform-opa-example` folder of the terratest repository.\nfunc TestOPAEvalTerraformModuleRemotePolicy(t *testing.T) {\n\tt.Parallel()\n\n\t// Skip this test when using OPA v1.0+ since the main branch may have v0.x syntax\n\t// while the local version requires v1.0+ syntax\n\tt.Skip(\"Skipping remote policy test due to syntax mismatch between local OPA version and remote policy\")\n\n\ttfOpts := &terraform.Options{\n\t\tTerraformDir: \"../examples/terraform-opa-example/pass\",\n\t}\n\topaOpts := &opa.EvalOptions{\n\t\t// This test fetches the policy from the main branch of the terratest repository.\n\t\t// The policy uses OPA v1.0+ compatible syntax.\n\t\tRulePath: \"git::https://github.com/gruntwork-io/terratest.git//examples/terraform-opa-example/policy/enforce_source.rego?ref=main\",\n\t\tFailMode: opa.FailUndefined,\n\t}\n\tterraform.OPAEval(t, tfOpts, opaOpts, \"data.enforce_source.allow\")\n}\n"
  },
  {
    "path": "test/terraform_packer_example_test.go",
    "content": "package test\n\nimport (\n\t\"crypto/tls\"\n\t\"fmt\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/gruntwork-io/terratest/modules/aws\"\n\thttpHelper \"github.com/gruntwork-io/terratest/modules/http-helper\"\n\t\"github.com/gruntwork-io/terratest/modules/logger\"\n\t\"github.com/gruntwork-io/terratest/modules/packer\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/gruntwork-io/terratest/modules/terraform\"\n\ttestStructure \"github.com/gruntwork-io/terratest/modules/test-structure\"\n)\n\n// This is a complicated, end-to-end integration test. It builds the AMI from examples/packer-docker-example,\n// deploys it using the Terraform code on examples/terraform-packer-example, and checks that the web server in the AMI\n// response to requests. The test is broken into \"stages\" so you can skip stages by setting environment variables (e.g.,\n// skip stage \"build_ami\" by setting the environment variable \"SKIP_build_ami=true\"), which speeds up iteration when\n// running this test over and over again locally.\nfunc TestTerraformPackerExample(t *testing.T) {\n\tt.Parallel()\n\n\t// The folder where we have our Terraform code\n\tworkingDir := \"../examples/terraform-packer-example\"\n\n\t// At the end of the test, delete the AMI\n\tdefer testStructure.RunTestStage(t, \"cleanup_ami\", func() {\n\t\tawsRegion := testStructure.LoadString(t, workingDir, \"awsRegion\")\n\t\tdeleteAMI(t, awsRegion, workingDir)\n\t})\n\n\t// At the end of the test, undeploy the web app using Terraform\n\tdefer testStructure.RunTestStage(t, \"cleanup_terraform\", func() {\n\t\tundeployUsingTerraform(t, workingDir)\n\t})\n\n\t// At the end of the test, fetch the most recent syslog entries from each Instance. This can be useful for\n\t// debugging issues without having to manually SSH to the server.\n\tdefer testStructure.RunTestStage(t, \"logs\", func() {\n\t\tawsRegion := testStructure.LoadString(t, workingDir, \"awsRegion\")\n\t\tfetchSyslogForInstance(t, awsRegion, workingDir)\n\t})\n\n\t// Build the AMI for the web app\n\ttestStructure.RunTestStage(t, \"build_ami\", func() {\n\t\t// Pick a random AWS region to test in. This helps ensure your code works in all regions.\n\t\tawsRegion := aws.GetRandomStableRegion(t, nil, nil)\n\t\ttestStructure.SaveString(t, workingDir, \"awsRegion\", awsRegion)\n\t\tbuildAMI(t, awsRegion, workingDir)\n\t})\n\n\t// Deploy the web app using Terraform\n\ttestStructure.RunTestStage(t, \"deploy_terraform\", func() {\n\t\tawsRegion := testStructure.LoadString(t, workingDir, \"awsRegion\")\n\t\tdeployUsingTerraform(t, awsRegion, workingDir)\n\t})\n\n\t// Validate that the web app deployed and is responding to HTTP requests\n\ttestStructure.RunTestStage(t, \"validate\", func() {\n\t\tvalidateInstanceRunningWebServer(t, workingDir)\n\t})\n}\n\n// Build the AMI in packer-docker-example\nfunc buildAMI(t *testing.T, awsRegion string, workingDir string) {\n\t// Some AWS regions are missing certain instance types, so pick an available type based on the region we picked\n\tinstanceType := aws.GetRecommendedInstanceType(t, awsRegion, []string{\"t2.micro, t3.micro\", \"t2.small\", \"t3.small\"})\n\n\tpackerOptions := &packer.Options{\n\t\t// The path to where the Packer template is located\n\t\tTemplate: \"../examples/packer-docker-example/build.pkr.hcl\",\n\n\t\t// Only build the AMI\n\t\tOnly: \"amazon-ebs.ubuntu-ami\",\n\n\t\t// Variables to pass to our Packer build using -var options\n\t\tVars: map[string]string{\n\t\t\t\"aws_region\":    awsRegion,\n\t\t\t\"instance_type\": instanceType,\n\t\t},\n\n\t\t// Configure retries for intermittent errors\n\t\tRetryableErrors:    DefaultRetryablePackerErrors,\n\t\tTimeBetweenRetries: DefaultTimeBetweenPackerRetries,\n\t\tMaxRetries:         DefaultMaxPackerRetries,\n\t}\n\n\t// Save the Packer Options so future test stages can use them\n\ttestStructure.SavePackerOptions(t, workingDir, packerOptions)\n\n\t// Build the AMI\n\tamiID := packer.BuildArtifact(t, packerOptions)\n\n\t// Save the AMI ID so future test stages can use them\n\ttestStructure.SaveAmiId(t, workingDir, amiID)\n}\n\n// Delete the AMI\nfunc deleteAMI(t *testing.T, awsRegion string, workingDir string) {\n\t// Load the AMI ID and Packer Options saved by the earlier build_ami stage\n\tamiID := testStructure.LoadAmiId(t, workingDir)\n\n\taws.DeleteAmi(t, awsRegion, amiID)\n}\n\n// Deploy the terraform-packer-example using Terraform\nfunc deployUsingTerraform(t *testing.T, awsRegion string, workingDir string) {\n\t// A unique ID we can use to namespace resources so we don't clash with anything already in the AWS account or\n\t// tests running in parallel\n\tuniqueID := random.UniqueId()\n\n\t// Give this EC2 Instance and other resources in the Terraform code a name with a unique ID so it doesn't clash\n\t// with anything else in the AWS account.\n\tinstanceName := fmt.Sprintf(\"terratest-http-example-%s\", uniqueID)\n\n\t// Specify the text the EC2 Instance will return when we make HTTP requests to it.\n\tinstanceText := fmt.Sprintf(\"Hello, %s!\", uniqueID)\n\n\t// Some AWS regions are missing certain instance types, so pick an available type based on the region we picked\n\tinstanceType := aws.GetRecommendedInstanceType(t, awsRegion, []string{\"t2.micro, t3.micro\", \"t2.small\", \"t3.small\"})\n\n\t// Load the AMI ID saved by the earlier build_ami stage\n\tamiID := testStructure.LoadAmiId(t, workingDir)\n\n\t// Construct the terraform options with default retryable errors to handle the most common retryable errors in\n\t// terraform testing.\n\tterraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{\n\t\t// The path to where our Terraform code is located\n\t\tTerraformDir: workingDir,\n\n\t\t// Variables to pass to our Terraform code using -var options\n\t\tVars: map[string]interface{}{\n\t\t\t\"aws_region\":    awsRegion,\n\t\t\t\"instance_name\": instanceName,\n\t\t\t\"instance_text\": instanceText,\n\t\t\t\"instance_type\": instanceType,\n\t\t\t\"ami_id\":        amiID,\n\t\t},\n\t})\n\n\t// Save the Terraform Options struct, instance name, and instance text so future test stages can use it\n\ttestStructure.SaveTerraformOptions(t, workingDir, terraformOptions)\n\n\t// This will run `terraform init` and `terraform apply` and fail the test if there are any errors\n\tterraform.InitAndApply(t, terraformOptions)\n}\n\n// Undeploy the terraform-packer-example using Terraform\nfunc undeployUsingTerraform(t *testing.T, workingDir string) {\n\t// Load the Terraform Options saved by the earlier deploy_terraform stage\n\tterraformOptions := testStructure.LoadTerraformOptions(t, workingDir)\n\n\tterraform.Destroy(t, terraformOptions)\n}\n\n// Fetch the most recent syslogs for the instance. This is a handy way to see what happened on the Instance as part of\n// your test log output, without having to re-run the test and manually SSH to the Instance.\nfunc fetchSyslogForInstance(t *testing.T, awsRegion string, workingDir string) {\n\t// Load the Terraform Options saved by the earlier deploy_terraform stage\n\tterraformOptions := testStructure.LoadTerraformOptions(t, workingDir)\n\n\tinstanceID := terraform.OutputRequired(t, terraformOptions, \"instance_id\")\n\tlogs := aws.GetSyslogForInstance(t, instanceID, awsRegion)\n\n\tlogger.Default.Logf(t, \"Most recent syslog for Instance %s:\\n\\n%s\\n\", instanceID, logs)\n}\n\n// Validate the web server has been deployed and is working\nfunc validateInstanceRunningWebServer(t *testing.T, workingDir string) {\n\t// Load the Terraform Options saved by the earlier deploy_terraform stage\n\tterraformOptions := testStructure.LoadTerraformOptions(t, workingDir)\n\n\t// Run `terraform output` to get the value of an output variable\n\tinstanceURL := terraform.Output(t, terraformOptions, \"instance_url\")\n\n\t// Setup a TLS configuration to submit with the helper, a blank struct is acceptable\n\ttlsConfig := tls.Config{}\n\n\t// Figure out what text the instance should return for each request\n\tinstanceText, _ := terraformOptions.Vars[\"instance_text\"].(string)\n\n\t// It can take a minute or so for the Instance to boot up, so retry a few times\n\tmaxRetries := 30\n\ttimeBetweenRetries := 5 * time.Second\n\n\t// Verify that we get back a 200 OK with the expected instanceText\n\thttpHelper.HttpGetWithRetry(t, instanceURL, &tlsConfig, 200, instanceText, maxRetries, timeBetweenRetries)\n}\n"
  },
  {
    "path": "test/terraform_redeploy_example_test.go",
    "content": "package test\n\nimport (\n\t\"crypto/tls\"\n\t\"fmt\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/gruntwork-io/terratest/modules/aws\"\n\thttp_helper \"github.com/gruntwork-io/terratest/modules/http-helper\"\n\t\"github.com/gruntwork-io/terratest/modules/logger\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/gruntwork-io/terratest/modules/retry\"\n\t\"github.com/gruntwork-io/terratest/modules/terraform\"\n\ttest_structure \"github.com/gruntwork-io/terratest/modules/test-structure\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// An example of how to test the Terraform module in examples/terraform-redeploy-example using Terratest. We deploy the\n// Terraform code, check that the load balancer returns the expected response, redeploy the code, and check that the\n// entire time during the redeploy, the load balancer continues returning a valid response and never returns an error\n// (i.e., we validate that zero-downtime deployment works).\n//\n// The test is broken into \"stages\" so you can skip stages by setting environment variables (e.g., skip stage\n// \"deploy_initial\" by setting the environment variable \"SKIP_deploy_initial=true\"), which speeds up iteration when\n// running this test over and over again locally.\nfunc TestTerraformRedeployExample(t *testing.T) {\n\tt.Parallel()\n\n\t// The folder where we have our Terraform code\n\tworkingDir := \"../examples/terraform-redeploy-example\"\n\n\t// Pick a random AWS region to test in. This helps ensure your code works in all regions.\n\ttest_structure.RunTestStage(t, \"pick_region\", func() {\n\t\tawsRegion := aws.GetRandomStableRegion(t, nil, nil)\n\t\t// Save the region, so that we reuse the same region when we skip stages\n\t\ttest_structure.SaveString(t, workingDir, \"region\", awsRegion)\n\t})\n\n\t// At the end of the test, clean up all the resources we created\n\tdefer test_structure.RunTestStage(t, \"teardown\", func() {\n\t\tterraformOptions := test_structure.LoadTerraformOptions(t, workingDir)\n\t\tterraform.Destroy(t, terraformOptions)\n\t})\n\n\t// At the end of the test, fetch the logs from each Instance. This can be useful for\n\t// debugging issues without having to manually SSH to the server.\n\tdefer test_structure.RunTestStage(t, \"logs\", func() {\n\t\tawsRegion := test_structure.LoadString(t, workingDir, \"region\")\n\t\tfetchSyslogForAsg(t, awsRegion, workingDir)\n\t\tfetchFilesFromAsg(t, awsRegion, workingDir)\n\t})\n\n\t// Deploy the web app\n\ttest_structure.RunTestStage(t, \"deploy_initial\", func() {\n\t\tawsRegion := test_structure.LoadString(t, workingDir, \"region\")\n\t\tinitialDeploy(t, awsRegion, workingDir)\n\t})\n\n\t// Validate that the ASG deployed and is responding to HTTP requests\n\ttest_structure.RunTestStage(t, \"validate_initial\", func() {\n\t\tawsRegion := test_structure.LoadString(t, workingDir, \"region\")\n\t\tvalidateAsgRunningWebServer(t, awsRegion, workingDir)\n\t})\n\n\t// Validate that we can deploy a change to the ASG with zero downtime\n\ttest_structure.RunTestStage(t, \"validate_redeploy\", func() {\n\t\tvalidateAsgRedeploy(t, workingDir)\n\t})\n}\n\n// Do the initial deployment of the terraform-redeploy-example\nfunc initialDeploy(t *testing.T, awsRegion string, workingDir string) {\n\t// A unique ID we can use to namespace resources so we don't clash with anything already in the AWS account or\n\t// tests running in parallel\n\tuniqueID := random.UniqueId()\n\n\t// Create a KeyPair we can use later to SSH to each Instance\n\tkeyPair := aws.CreateAndImportEC2KeyPair(t, awsRegion, uniqueID)\n\ttest_structure.SaveEc2KeyPair(t, workingDir, keyPair)\n\n\t// Give the ASG and other resources in the Terraform code a name with a unique ID so it doesn't clash\n\t// with anything else in the AWS account.\n\tname := fmt.Sprintf(\"redeploy-test-%s\", uniqueID)\n\n\t// Specify the text the ASG will return when we make HTTP requests to it.\n\ttext := fmt.Sprintf(\"Hello, %s!\", uniqueID)\n\n\t// Some AWS regions are missing certain instance types, so pick an available type based on the region we picked\n\tinstanceType := aws.GetRecommendedInstanceType(t, awsRegion, []string{\"t2.micro, t3.micro\", \"t2.small\", \"t3.small\"})\n\n\t// Construct the terraform options with default retryable errors to handle the most common retryable errors in\n\t// terraform testing.\n\tterraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{\n\t\t// The path to where our Terraform code is located\n\t\tTerraformDir: workingDir,\n\n\t\t// Variables to pass to our Terraform code using -var options\n\t\tVars: map[string]interface{}{\n\t\t\t\"aws_region\":    awsRegion,\n\t\t\t\"instance_name\": name,\n\t\t\t\"instance_text\": text,\n\t\t\t\"instance_type\": instanceType,\n\t\t\t\"key_pair_name\": keyPair.Name,\n\t\t},\n\t})\n\n\t// Save the Terraform Options struct so future test stages can use it\n\ttest_structure.SaveTerraformOptions(t, workingDir, terraformOptions)\n\n\t// This will run `terraform init` and `terraform apply` and fail the test if there are any errors\n\tterraform.InitAndApply(t, terraformOptions)\n}\n\n// Validate the ASG has been deployed and is working\nfunc validateAsgRunningWebServer(t *testing.T, awsRegion string, workingDir string) {\n\t// Load the Terraform Options saved by the earlier deploy_terraform stage\n\tterraformOptions := test_structure.LoadTerraformOptions(t, workingDir)\n\n\t// Run `terraform output` to get the value of an output variable\n\turl := terraform.Output(t, terraformOptions, \"url\")\n\tasgName := terraform.OutputRequired(t, terraformOptions, \"asg_name\")\n\n\t// Setup a TLS configuration to submit with the helper, a blank struct is acceptable\n\ttlsConfig := tls.Config{}\n\n\t// Wait and verify the ASG is scaled to the desired capacity. It can take a few minutes for the ASG to boot up, so\n\t// retry a few times.\n\tmaxRetries := 30\n\ttimeBetweenRetries := 10 * time.Second\n\taws.WaitForCapacity(t, asgName, awsRegion, maxRetries, timeBetweenRetries)\n\tcapacityInfo := aws.GetCapacityInfoForAsg(t, asgName, awsRegion)\n\tassert.Equal(t, capacityInfo.DesiredCapacity, int64(3))\n\tassert.Equal(t, capacityInfo.CurrentCapacity, int64(3))\n\n\t// Figure out what text the ASG should return for each request\n\texpectedText, _ := terraformOptions.Vars[\"instance_text\"].(string)\n\n\t// Verify that we get back a 200 OK with the expectedText\n\t// It can take a few minutes for the ALB to boot up, so retry a few times\n\thttp_helper.HttpGetWithRetry(t, url, &tlsConfig, 200, expectedText, maxRetries, timeBetweenRetries)\n}\n\n// Validate we can deploy an update to the ASG with zero downtime for users accessing the ALB\nfunc validateAsgRedeploy(t *testing.T, workingDir string) {\n\t// Load the Terraform Options saved by the earlier deploy_terraform stage\n\tterraformOptions := test_structure.LoadTerraformOptions(t, workingDir)\n\n\t// Figure out what text the ASG was returning for each request\n\toriginalText, _ := terraformOptions.Vars[\"instance_text\"].(string)\n\n\t// New text for the ASG to return for each request\n\tnewText := fmt.Sprintf(\"%s-redeploy\", originalText)\n\tterraformOptions.Vars[\"instance_text\"] = newText\n\n\t// Save the updated Terraform Options struct\n\ttest_structure.SaveTerraformOptions(t, workingDir, terraformOptions)\n\n\t// Run `terraform output` to get the value of an output variable\n\turl := terraform.Output(t, terraformOptions, \"url\")\n\n\t// Setup a TLS configuration to submit with the helper, a blank struct is acceptable\n\ttlsConfig := tls.Config{}\n\n\t// Check once per second that the ELB returns a proper response to make sure there is no downtime during deployment\n\telbChecks := retry.DoInBackgroundUntilStopped(t, fmt.Sprintf(\"Check URL %s\", url), 1*time.Second, func() {\n\t\thttp_helper.HttpGetWithCustomValidation(t, url, &tlsConfig, func(statusCode int, body string) bool {\n\t\t\treturn statusCode == 200 && (body == originalText || body == newText)\n\t\t})\n\t})\n\n\t// Redeploy the cluster\n\tterraform.Apply(t, terraformOptions)\n\n\t// Stop checking the ELB\n\telbChecks.Done()\n}\n\n// (Deprecated) See the fetchFilesFromAsg method below for a more powerful solution.\n//\n// Fetch the most recent syslogs for the instances in the ASG. This is a handy way to see what happened on each\n// Instance as part of your test log output, without having to re-run the test and manually SSH to the Instances.\nfunc fetchSyslogForAsg(t *testing.T, awsRegion string, workingDir string) {\n\t// Load the Terraform Options saved by the earlier deploy_terraform stage\n\tterraformOptions := test_structure.LoadTerraformOptions(t, workingDir)\n\n\tasgName := terraform.OutputRequired(t, terraformOptions, \"asg_name\")\n\tasgLogs := aws.GetSyslogForInstancesInAsg(t, asgName, awsRegion)\n\n\tlogger.Logf(t, \"===== First few hundred bytes of syslog for instances in ASG %s =====\\n\\n\", asgName)\n\n\tfor instanceID, logs := range asgLogs {\n\t\tlogger.Logf(t, \"Most recent syslog for Instance %s:\\n\\n%s\\n\", instanceID, logs)\n\t}\n}\n\n// Default syslog location on Ubuntu\nconst syslogPathUbuntu = \"/var/log/syslog\"\n\n// Default location where the User Data script generates an index.html on Ubuntu\nconst indexHtmlUbuntu = \"/index.html\"\n\n// This size is configured in the terraform-redeploy-example itself\nconst asgSize = 3\n\nfunc fetchFilesFromAsg(t *testing.T, awsRegion string, workingDir string) {\n\t// Load the Terraform Options and Key Pair saved by the earlier deploy_terraform stage\n\tterraformOptions := test_structure.LoadTerraformOptions(t, workingDir)\n\tkeyPair := test_structure.LoadEc2KeyPair(t, workingDir)\n\n\tasgName := terraform.OutputRequired(t, terraformOptions, \"asg_name\")\n\tinstanceIdToFilePathToContents := aws.FetchContentsOfFilesFromAsg(t, awsRegion, \"ubuntu\", keyPair, asgName, true, syslogPathUbuntu, indexHtmlUbuntu)\n\n\trequire.Len(t, instanceIdToFilePathToContents, asgSize)\n\n\t// Check that the index.html file on each Instance contains the expected text\n\texpectedText := terraformOptions.Vars[\"instance_text\"]\n\tfor instanceID, filePathToContents := range instanceIdToFilePathToContents {\n\t\trequire.Contains(t, filePathToContents, indexHtmlUbuntu)\n\t\tassert.Equal(t, expectedText, strings.TrimSpace(filePathToContents[indexHtmlUbuntu]), \"Expected %s on instance %s to contain %s\", indexHtmlUbuntu, instanceID, expectedText)\n\t}\n\n\tlogger.Logf(t, \"===== Full contents of syslog for instances in ASG %s =====\\n\\n\", asgName)\n\n\t// Print out the FULL contents of syslog (unlike the deprecated GetSyslogForInstancesInAsg, which only returns the\n\t// first few hundred bytes)\n\tfor instanceID, filePathToContents := range instanceIdToFilePathToContents {\n\t\trequire.Contains(t, filePathToContents, syslogPathUbuntu)\n\t\tlogger.Logf(t, \"Full syslog for Instance %s:\\n\\n%s\\n\", instanceID, filePathToContents[syslogPathUbuntu])\n\t}\n}\n"
  },
  {
    "path": "test/terraform_remote_exec_example_test.go",
    "content": "package test\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/aws\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/gruntwork-io/terratest/modules/ssh\"\n\t\"github.com/gruntwork-io/terratest/modules/terraform\"\n\ttest_structure \"github.com/gruntwork-io/terratest/modules/test-structure\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\n// This test shows how to override the systems local SSH Agent, with an in-process SSH agent, whose keys can be managed\n// from within your tests. This allows you to test Terraform modules which make SSH connections to the created\n// instances, useful for tasks such as provisioning.\nfunc TestTerraformRemoteExecExample(t *testing.T) {\n\tt.Parallel()\n\n\tterraformDirectory := \"../examples/terraform-remote-exec-example\"\n\n\t// At the end of the test, run `terraform destroy` to clean up any resources that were created\n\tdefer test_structure.RunTestStage(t, \"teardown\", func() {\n\t\tterraformOptions := test_structure.LoadTerraformOptions(t, terraformDirectory)\n\t\tkeyPair := test_structure.LoadEc2KeyPair(t, terraformDirectory)\n\n\t\t// destroy terraform resources and delete ec2 key pair\n\t\tterraform.Destroy(t, terraformOptions)\n\t\taws.DeleteEC2KeyPair(t, keyPair)\n\n\t\t// remove testFile, if it exists\n\t\ttestFile := filepath.Join(terraformDirectory, \"public-ip\")\n\t\tif _, err := os.Stat(testFile); err == nil {\n\t\t\tos.Remove(testFile)\n\t\t}\n\t})\n\n\t// Deploy the example\n\ttest_structure.RunTestStage(t, \"setup\", func() {\n\n\t\t// A unique ID we can use to namespace resources so we don't clash with anything already in the AWS account or\n\t\t// tests running in parallel\n\t\tuniqueID := random.UniqueId()\n\n\t\t// Give this EC2 Instance and other resources in the Terraform code a name with a unique ID so it doesn't clash\n\t\t// with anything else in the AWS account.\n\t\tinstanceName := fmt.Sprintf(\"terratest-remote-exec-example-%s\", uniqueID)\n\n\t\t// Pick a random AWS region to test in. This helps ensure your code works in all regions.\n\t\tawsRegion := aws.GetRandomStableRegion(t, nil, nil)\n\n\t\t// Some AWS regions are missing certain instance types, so pick an available type based on the region we picked\n\t\tinstanceType := aws.GetRecommendedInstanceType(t, awsRegion, []string{\"t2.micro, t3.micro\", \"t2.small\", \"t3.small\"})\n\n\t\t// Create an EC2 KeyPair that we can use for SSH access\n\t\tkeyPairName := fmt.Sprintf(\"terratest-remote-exec-example-%s\", uniqueID)\n\t\tkeyPair := aws.CreateAndImportEC2KeyPair(t, awsRegion, keyPairName)\n\n\t\t// start an SSH agent, with our key pair added\n\t\tsshAgent := ssh.SshAgentWithKeyPair(t, keyPair.KeyPair)\n\t\tdefer sshAgent.Stop()\n\n\t\t// Construct the terraform options with default retryable errors to handle the most common retryable errors in\n\t\t// terraform testing.\n\t\tterraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{\n\t\t\t// The path to where our Terraform code is located\n\t\t\tTerraformDir: terraformDirectory,\n\n\t\t\t// Variables to pass to our Terraform code using -var options\n\t\t\tVars: map[string]interface{}{\n\t\t\t\t\"aws_region\":    awsRegion,\n\t\t\t\t\"instance_name\": instanceName,\n\t\t\t\t\"instance_type\": instanceType,\n\t\t\t\t\"key_pair_name\": keyPairName,\n\t\t\t},\n\n\t\t\tSshAgent: sshAgent, // Overrides local SSH agent with our new agent\n\t\t})\n\n\t\t// Save the options and key pair so later test stages can use them\n\t\ttest_structure.SaveTerraformOptions(t, terraformDirectory, terraformOptions)\n\t\ttest_structure.SaveEc2KeyPair(t, terraformDirectory, keyPair)\n\n\t\t// Because of the SshAgent option above, the terraform process will be provided an `SSH_AUTH_SOCK` environment\n\t\t// variable, which will point to the socket file of our in-process `sshAgent` instance:\n\t\tterraform.InitAndApply(t, terraformOptions)\n\n\t\t// save the `public_instance_ip` output variable for later steps\n\t\tpublicIP := terraform.Output(t, terraformOptions, \"public_instance_ip\")\n\t\ttest_structure.SaveString(t, terraformDirectory, \"publicIP\", publicIP)\n\t})\n\n\t// Make sure we can SSH to the public Instance directly from the public Internet and the private Instance by using\n\t// the public Instance as a jump host\n\ttest_structure.RunTestStage(t, \"validate\", func() {\n\t\tpublicIP := test_structure.LoadString(t, terraformDirectory, \"publicIP\")\n\n\t\t// Confirm that the public-ip file that was generated by the provisioner was copied back from the server using\n\t\t// the `scp` command\n\t\ttestFile := filepath.Join(terraformDirectory, \"public-ip\")\n\t\tassert.FileExists(t, testFile)\n\n\t\t// Check that public IP from output matches public IP generated by script on the server\n\t\tb, err := os.ReadFile(testFile)\n\t\tif err != nil {\n\t\t\tfmt.Print(err)\n\t\t}\n\t\tassert.Equal(t, strings.TrimSpace(publicIP), strings.TrimSpace(string(b)))\n\t})\n\n}\n"
  },
  {
    "path": "test/terraform_scp_example_test.go",
    "content": "package test\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/gruntwork-io/terratest/modules/aws\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/gruntwork-io/terratest/modules/retry\"\n\t\"github.com/gruntwork-io/terratest/modules/ssh\"\n\t\"github.com/gruntwork-io/terratest/modules/terraform\"\n\ttest_structure \"github.com/gruntwork-io/terratest/modules/test-structure\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestTerraformScpExample(t *testing.T) {\n\tt.Parallel()\n\n\texampleFolder := test_structure.CopyTerraformFolderToTemp(t, \"../\", \"examples/terraform-asg-scp-example\")\n\n\t// At the end of the test, run `terraform destroy` to clean up any resources that were created\n\tdefer test_structure.RunTestStage(t, \"teardown\", func() {\n\t\tterraformOptions := test_structure.LoadTerraformOptions(t, exampleFolder)\n\t\tterraform.Destroy(t, terraformOptions)\n\n\t\tkeyPair := test_structure.LoadEc2KeyPair(t, exampleFolder)\n\t\taws.DeleteEC2KeyPair(t, keyPair)\n\t})\n\n\t// Deploy the example\n\ttest_structure.RunTestStage(t, \"setup\", func() {\n\t\tterraformOptions, keyPair := createTerraformOptions(t, exampleFolder)\n\n\t\t// Save the options and key pair so later test stages can use them\n\t\ttest_structure.SaveTerraformOptions(t, exampleFolder, terraformOptions)\n\t\ttest_structure.SaveEc2KeyPair(t, exampleFolder, keyPair)\n\n\t\t// This will run `terraform init` and `terraform apply` and fail the test if there are any errors\n\t\tterraform.InitAndApply(t, terraformOptions)\n\t})\n\n\t// Make sure we can SCP a file from an EC2 instance to our local box\n\ttest_structure.RunTestStage(t, \"validate_file\", func() {\n\t\tterraformOptions := test_structure.LoadTerraformOptions(t, exampleFolder)\n\t\tkeyPair := test_structure.LoadEc2KeyPair(t, exampleFolder)\n\n\t\ttestScpFromHost(t, terraformOptions, keyPair)\n\t})\n\n\t// Make sure we can SCP all files in a given remote dir from an EC2 instance to our local box\n\ttest_structure.RunTestStage(t, \"validate_dir\", func() {\n\t\tterraformOptions := test_structure.LoadTerraformOptions(t, exampleFolder)\n\t\tkeyPair := test_structure.LoadEc2KeyPair(t, exampleFolder)\n\n\t\ttestScpDirFromHost(t, terraformOptions, keyPair)\n\t})\n\n\t// Make sure we can SCP all files in a given remote dir from an EC2 instance to our local box\n\ttest_structure.RunTestStage(t, \"validate_asg\", func() {\n\t\tterraformOptions := test_structure.LoadTerraformOptions(t, exampleFolder)\n\t\tkeyPair := test_structure.LoadEc2KeyPair(t, exampleFolder)\n\n\t\ttestScpFromAsg(t, terraformOptions, keyPair, exampleFolder)\n\t})\n\n}\n\nfunc createTerraformOptions(t *testing.T, exampleFolder string) (*terraform.Options, *aws.Ec2Keypair) {\n\t// A unique ID we can use to namespace resources so we don't clash with anything already in the AWS account or\n\t// tests running in parallel\n\tuniqueID := random.UniqueId()\n\n\t// Give this EC2 Instance and other resources in the Terraform code a name with a unique ID so it doesn't clash\n\t// with anything else in the AWS account.\n\tinstanceName := fmt.Sprintf(\"terratest-asg-scp-example-%s\", uniqueID)\n\n\t// Pick a random AWS region to test in. This helps ensure your code works in all regions.\n\tawsRegion := aws.GetRandomStableRegion(t, nil, nil)\n\n\t// Some AWS regions are missing certain instance types, so pick an available type based on the region we picked\n\tinstanceType := aws.GetRecommendedInstanceType(t, awsRegion, []string{\"t2.micro, t3.micro\", \"t2.small\", \"t3.small\"})\n\n\t// Create an EC2 KeyPair that we can use for SSH access\n\tkeyPairName := fmt.Sprintf(\"terratest-asg-scp-example-%s\", uniqueID)\n\tkeyPair := aws.CreateAndImportEC2KeyPair(t, awsRegion, keyPairName)\n\n\t// Construct the terraform options with default retryable errors to handle the most common retryable errors in\n\t// terraform testing.\n\tterraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{\n\t\t// The path to where our Terraform code is located\n\t\tTerraformDir: exampleFolder,\n\n\t\t// Variables to pass to our Terraform code using -var options\n\t\tVars: map[string]interface{}{\n\t\t\t\"aws_region\":    awsRegion,\n\t\t\t\"instance_name\": instanceName,\n\t\t\t\"key_pair_name\": keyPairName,\n\t\t\t\"instance_type\": instanceType,\n\t\t},\n\t})\n\n\treturn terraformOptions, keyPair\n}\n\nfunc testScpDirFromHost(t *testing.T, terraformOptions *terraform.Options, keyPair *aws.Ec2Keypair) {\n\t// Run `terraform output` to get the value of an output variable\n\tawsRegion := terraformOptions.Vars[\"aws_region\"].(string)\n\tasgName := terraform.Output(t, terraformOptions, \"asg_name\")\n\tinstanceIds := aws.GetInstanceIdsForAsg(t, asgName, awsRegion)\n\tpublicInstanceIP := aws.GetPublicIpOfEc2Instance(t, instanceIds[0], awsRegion)\n\n\t// We're going to try to SSH to the instance IP, using the Key Pair we created earlier, and the user \"ubuntu\",\n\t// as we know the Instance is running an Ubuntu AMI that has such a user\n\tsshUserName := \"ubuntu\"\n\tpublicHost := ssh.Host{\n\t\tHostname:    publicInstanceIP,\n\t\tSshKeyPair:  keyPair.KeyPair,\n\t\tSshUserName: sshUserName,\n\t}\n\n\t_, remoteTempFilePath := writeSampleDataToInstance(t, publicInstanceIP, sshUserName, keyPair)\n\tremoteTempFolder := filepath.Dir(remoteTempFilePath)\n\tdefer cleanup(t, publicInstanceIP, sshUserName, keyPair, remoteTempFolder)\n\n\tlocalDestDir := \"/tmp/tempFolder\"\n\n\tvar testcases = []struct {\n\t\tname          string\n\t\toptions       ssh.ScpDownloadOptions\n\t\texpectedFiles int\n\t}{\n\t\t{\n\t\t\t\"GrabAllFiles\",\n\t\t\tssh.ScpDownloadOptions{RemoteHost: publicHost, RemoteDir: remoteTempFolder, LocalDir: filepath.Join(localDestDir, random.UniqueId())},\n\t\t\t2,\n\t\t},\n\t\t{\n\t\t\t\"GrabAllFilesExplicit\",\n\t\t\tssh.ScpDownloadOptions{RemoteHost: publicHost, RemoteDir: remoteTempFolder, LocalDir: filepath.Join(localDestDir, random.UniqueId()), FileNameFilters: []string{\"*\"}},\n\t\t\t2,\n\t\t},\n\t\t{\n\t\t\t\"GrabFilesWithFilter\",\n\t\t\tssh.ScpDownloadOptions{RemoteHost: publicHost, RemoteDir: remoteTempFolder, LocalDir: filepath.Join(localDestDir, random.UniqueId()), FileNameFilters: []string{\"*.baz\"}},\n\t\t\t1,\n\t\t},\n\t}\n\n\tfor _, testCase := range testcases {\n\t\t// The following is necessary to make sure testCase's values don't\n\t\t// get updated due to concurrency within the scope of t.Run(..) below\n\t\ttestCase := testCase\n\n\t\tt.Run(testCase.name, func(t *testing.T) {\n\t\t\terr := ssh.ScpDirFromE(t, testCase.options, false)\n\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"Error copying from remote: %s\", err.Error())\n\t\t\t}\n\n\t\t\texpectedNumFiles := testCase.expectedFiles\n\n\t\t\tfileInfos, err := os.ReadDir(testCase.options.LocalDir)\n\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"Error reading from local dir: %s, due to: %s\", testCase.options.LocalDir, err.Error())\n\t\t\t}\n\n\t\t\tactualNumFilesCopied := len(fileInfos)\n\n\t\t\tif len(fileInfos) != expectedNumFiles {\n\t\t\t\tt.Fatalf(\"Error: expected %d files to be copied. Only found %d\", expectedNumFiles, actualNumFilesCopied)\n\t\t\t}\n\n\t\t\t// Clean up the temp file we created\n\t\t\tos.RemoveAll(testCase.options.LocalDir)\n\t\t})\n\t}\n}\n\nfunc testScpFromHost(t *testing.T, terraformOptions *terraform.Options, keyPair *aws.Ec2Keypair) {\n\t// Run `terraform output` to get the value of an output variable\n\tawsRegion := terraformOptions.Vars[\"aws_region\"].(string)\n\tasgName := terraform.Output(t, terraformOptions, \"asg_name\")\n\tinstanceIds := aws.GetInstanceIdsForAsg(t, asgName, awsRegion)\n\tpublicInstanceIP := aws.GetPublicIpOfEc2Instance(t, instanceIds[0], awsRegion)\n\n\t// We're going to try to SSH to the instance IP, using the Key Pair we created earlier, and the user \"ubuntu\",\n\t// as we know the Instance is running an Ubuntu AMI that has such a user\n\tsshUserName := \"ubuntu\"\n\tpublicHost := ssh.Host{\n\t\tHostname:    publicInstanceIP,\n\t\tSshKeyPair:  keyPair.KeyPair,\n\t\tSshUserName: sshUserName,\n\t}\n\n\trandomData, remoteTempFilePath := writeSampleDataToInstance(t, publicInstanceIP, sshUserName, keyPair)\n\tremoteTempFolder := filepath.Base(remoteTempFilePath)\n\tdefer cleanup(t, publicInstanceIP, sshUserName, keyPair, remoteTempFolder)\n\n\tlocalTempFileName := \"/tmp/test.out\"\n\tlocalFile, err := os.Create(localTempFileName)\n\n\t// Clean up the temp file we created\n\tdefer os.Remove(localTempFileName)\n\n\tif err != nil {\n\t\tt.Fatalf(\"Error: creating local temp file: %s\", err.Error())\n\t}\n\n\tssh.ScpFileFromE(t, publicHost, remoteTempFilePath, localFile, false)\n\n\tbuf, err := os.ReadFile(localTempFileName)\n\n\tif err != nil {\n\t\tt.Fatalf(\"Error: Unable to read local file from disk: %s\", err.Error())\n\t}\n\n\tlocalFileContents := string(buf)\n\n\tif !strings.Contains(localFileContents, randomData) {\n\t\tt.Fatalf(\"Error: unable to find %s in the local file. Local file's contents were: %s\", randomData, localFileContents)\n\t}\n}\n\nfunc testScpFromAsg(t *testing.T, terraformOptions *terraform.Options, keyPair *aws.Ec2Keypair, exampleFolder string) {\n\t// Run `terraform output` to get the value of an output variable\n\tawsRegion := terraformOptions.Vars[\"aws_region\"].(string)\n\tasgName := terraform.Output(t, terraformOptions, \"asg_name\")\n\tinstanceIds := aws.GetInstanceIdsForAsg(t, asgName, awsRegion)\n\tpublicInstanceIP := aws.GetPublicIpOfEc2Instance(t, instanceIds[0], awsRegion)\n\n\t// This is where we'll store the logs from the remote server\n\tlocalDestinationDirectory := filepath.Join(exampleFolder, \"logs\")\n\tsshUserName := \"ubuntu\"\n\n\trandomData, remoteTempFilePath := writeSampleDataToInstance(t, publicInstanceIP, sshUserName, keyPair)\n\tremoteTempFolder, remoteTempFileName := filepath.Split(remoteTempFilePath)\n\tdefer cleanup(t, publicInstanceIP, sshUserName, keyPair, remoteTempFolder)\n\n\t// This is where we will look for the downloaded syslog\n\tlocalSyslogLocation := filepath.Join(localDestinationDirectory, publicInstanceIP, \"testFolder\", remoteTempFileName)\n\n\t//Create a RemoteFileSpecification for our test ASG\n\t//We will specify that we'd like to grab /var/log/syslog\n\t//and store that locally.\n\tspec := aws.RemoteFileSpecification{\n\t\tSshUser:  sshUserName,\n\t\tUseSudo:  true,\n\t\tKeyPair:  keyPair,\n\t\tAsgNames: []string{asgName},\n\t\tRemotePathToFileFilter: map[string][]string{\n\t\t\tremoteTempFolder: {remoteTempFileName},\n\t\t},\n\t\tLocalDestinationDir: localDestinationDirectory,\n\t}\n\n\t// Go and SCP the test file from EC2 instance\n\taws.FetchFilesFromAsgsE(t, awsRegion, spec)\n\n\t// Clean up the temp file we created\n\tdefer os.RemoveAll(localDestinationDirectory)\n\n\t//Read the locally copied syslog\n\tbuf, err := os.ReadFile(localSyslogLocation)\n\n\tif err != nil {\n\t\tt.Fatalf(\"Error: Unable to read local file from disk: %s\", err.Error())\n\t}\n\n\tlocalFileContents := string(buf)\n\n\tassert.Contains(t, localFileContents, randomData)\n}\n\nfunc writeSampleDataToInstance(t *testing.T, publicInstanceIP string, sshUserName string, keyPair *aws.Ec2Keypair) (string, string) {\n\n\t// We're going to try to SSH to the instance IP, using the Key Pair we created earlier, and the user \"ubuntu\",\n\t// as we know the Instance is running an Ubuntu AMI that has such a user\n\tpublicHost := ssh.Host{\n\t\tHostname:    publicInstanceIP,\n\t\tSshKeyPair:  keyPair.KeyPair,\n\t\tSshUserName: sshUserName,\n\t}\n\n\t// It can take a minute or so for the Instance to boot up, so retry a few times\n\tmaxRetries := 30\n\ttimeBetweenRetries := 5 * time.Second\n\tdescription := fmt.Sprintf(\"SSH to public host %s\", publicInstanceIP)\n\tremoteTempFolder := \"/tmp/testFolder\"\n\tremoteTempFilePath := filepath.Join(remoteTempFolder, \"test.foo\")\n\tremoteTempFilePath2 := filepath.Join(remoteTempFolder, \"test.baz\")\n\trandomData := random.UniqueId()\n\n\t// Verify that we can SSH to the Instance and run commands\n\tretry.DoWithRetry(t, description, maxRetries, timeBetweenRetries, func() (string, error) {\n\t\t_, err := ssh.CheckSshCommandE(t, publicHost, fmt.Sprintf(\"mkdir -p %s && touch %s && touch %s && echo \\\"%s\\\" >> %s\", remoteTempFolder, remoteTempFilePath, remoteTempFilePath2, randomData, remoteTempFilePath))\n\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\treturn \"\", nil\n\t})\n\n\treturn randomData, remoteTempFilePath\n}\n\nfunc cleanup(t *testing.T, publicInstanceIP string, sshUserName string, keyPair *aws.Ec2Keypair, folderToClean string) {\n\tpublicHost := ssh.Host{\n\t\tHostname:    publicInstanceIP,\n\t\tSshKeyPair:  keyPair.KeyPair,\n\t\tSshUserName: sshUserName,\n\t}\n\n\tmaxRetries := 30\n\ttimeBetweenRetries := 5 * time.Second\n\tdescription := fmt.Sprintf(\"SSH to public host %s\", publicInstanceIP)\n\n\t// clean up the remote folder as we want may want to run another test case\n\tdefer retry.DoWithRetry(t, description, maxRetries, timeBetweenRetries, func() (string, error) {\n\t\t_, err := ssh.CheckSshCommandE(t,\n\t\t\tpublicHost,\n\t\t\tfmt.Sprintf(\"rm -rf %s\", folderToClean))\n\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\treturn \"\", nil\n\t})\n}\n"
  },
  {
    "path": "test/terraform_ssh_certificate_example_test.go",
    "content": "package test\n\nimport (\n\t\"crypto/rand\"\n\t\"crypto/rsa\"\n\t\"fmt\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\tstdssh \"golang.org/x/crypto/ssh\"\n\n\t\"github.com/gruntwork-io/terratest/modules/aws\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/gruntwork-io/terratest/modules/retry\"\n\t\"github.com/gruntwork-io/terratest/modules/ssh\"\n\t\"github.com/gruntwork-io/terratest/modules/terraform\"\n\ttest_structure \"github.com/gruntwork-io/terratest/modules/test-structure\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// An example of how to test the Terraform module in examples/terraform-ssh-certificate-example using Terratest. The test\n// also shows an example of how to break a test down into \"stages\" so you can skip stages by setting environment\n// variables (e.g., skip stage \"teardown\" by setting the environment variable \"SKIP_teardown=true\"), which speeds up\n// iteration when running this test over and over again locally.\nfunc TestTerraformSshCertificateExample(t *testing.T) {\n\tt.Parallel()\n\n\texampleFolder := test_structure.CopyTerraformFolderToTemp(t, \"../\", \"examples/terraform-ssh-certificate-example\")\n\n\t// At the end of the test, run `terraform destroy` to clean up any resources that were created.\n\tdefer test_structure.RunTestStage(t, \"teardown\", func() {\n\t\tterraformOptions := test_structure.LoadTerraformOptions(t, exampleFolder)\n\t\tterraform.Destroy(t, terraformOptions)\n\t})\n\n\t// Deploy the example.\n\ttest_structure.RunTestStage(t, \"setup\", func() {\n\t\tterraformOptions, keyPair := configureTerraformSshCertificateOptions(t, exampleFolder)\n\n\t\t// Save the options so later test stages can use them.\n\t\ttest_structure.SaveTerraformOptions(t, exampleFolder, terraformOptions)\n\t\ttest_structure.SaveSshKeyPair(t, exampleFolder, keyPair)\n\n\t\t// This will run `terraform init` and `terraform apply` and fail the test if there are any errors.\n\t\tterraform.InitAndApply(t, terraformOptions)\n\t})\n\n\t// Make sure we can SSH to the public instance directly from the public internet.\n\ttest_structure.RunTestStage(t, \"validate\", func() {\n\t\tterraformOptions := test_structure.LoadTerraformOptions(t, exampleFolder)\n\t\tkeypair := test_structure.LoadSshKeyPair(t, exampleFolder)\n\n\t\ttestSSHCertificateToPublicHost(t, terraformOptions, keypair)\n\t})\n}\n\nfunc configureTerraformSshCertificateOptions(t *testing.T, exampleFolder string) (*terraform.Options, *ssh.KeyPair) {\n\t// A unique ID we can use to namespace resources so we don't clash with anything already in the AWS account or\n\t// tests running in parallel.\n\tuniqueID := random.UniqueId()\n\n\t// Give this EC2 instance and other resources in the Terraform code a name with a unique ID so it doesn't clash\n\t// with anything else in the AWS account.\n\tinstanceName := fmt.Sprintf(\"terratest-ssh-certificate-example-%s\", uniqueID)\n\n\t// Pick a random AWS region to test in. This helps ensure your code works in all regions.\n\tawsRegion := aws.GetRandomStableRegion(t, nil, nil)\n\n\t// Some AWS regions are missing certain instance types, so pick an available type based on the region we picked\n\tinstanceType := aws.GetRecommendedInstanceType(t, awsRegion, []string{\"t2.micro, t3.micro\", \"t2.small\", \"t3.small\"})\n\n\t// Create a random ssh certificate\n\tk, err := rsa.GenerateKey(rand.Reader, 4096)\n\trequire.NoError(t, err)\n\tcaSigner, err := stdssh.NewSignerFromKey(k)\n\trequire.NoError(t, err)\n\tkeyPair := ssh.GenerateRSAKeyPair(t, 2048)\n\tpub, _, _, _, err := stdssh.ParseAuthorizedKey([]byte(keyPair.PublicKey))\n\trequire.NoError(t, err)\n\tcert := &stdssh.Certificate{\n\t\tKey:             pub,\n\t\tSerial:          1,\n\t\tCertType:        stdssh.UserCert,\n\t\tKeyId:           \"terratest\",           // identity\n\t\tValidPrincipals: []string{\"terratest\"}, // allowed usernames\n\t\tValidAfter:      uint64(time.Now().Unix()),\n\t\tValidBefore:     uint64(time.Now().Add(365 * 24 * time.Hour).Unix()), // 1 year\n\t\tPermissions: stdssh.Permissions{\n\t\t\tExtensions: map[string]string{\n\t\t\t\t\"permit-pty\": \"\", // allow PTY\n\t\t\t},\n\t\t},\n\t}\n\trequire.NoError(t, cert.SignCert(rand.Reader, caSigner))\n\tkeyPair.PublicKey = string(stdssh.MarshalAuthorizedKey(cert))\n\n\t// Construct the terraform options with default retryable errors to handle the most common retryable errors in\n\t// terraform testing.\n\tterraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{\n\t\t// The path to where our Terraform code is located.\n\t\tTerraformDir: exampleFolder,\n\n\t\t// Variables to pass to our Terraform code using -var options.\n\t\tVars: map[string]interface{}{\n\t\t\t\"aws_region\":        awsRegion,\n\t\t\t\"instance_name\":     instanceName,\n\t\t\t\"instance_type\":     instanceType,\n\t\t\t\"ssh_ca_public_key\": string(stdssh.MarshalAuthorizedKey(caSigner.PublicKey())),\n\t\t},\n\t})\n\n\treturn terraformOptions, keyPair\n}\n\nfunc testSSHCertificateToPublicHost(t *testing.T, terraformOptions *terraform.Options, keyPair *ssh.KeyPair) {\n\t// Run `terraform output` to get the value of an output variable.\n\tpublicInstanceIP := terraform.Output(t, terraformOptions, \"public_instance_ip\")\n\n\t// We're going to try to SSH to the instance IP, using the username and password that will be set up (by\n\t// Terraform's user_data script) in the instance.\n\tpublicHost := ssh.Host{\n\t\tHostname:    publicInstanceIP,\n\t\tSshUserName: \"terratest\",\n\t\tSshKeyPair:  keyPair,\n\t}\n\n\t// It can take a minute or so for the instance to boot up, so retry a few times.\n\tmaxRetries := 30\n\ttimeBetweenRetries := 10 * time.Second\n\tdescription := fmt.Sprintf(\"SSH to public host %s\", publicInstanceIP)\n\n\t// Run a simple echo command on the server.\n\texpectedText := \"Hello, World\"\n\tcommand := fmt.Sprintf(\"echo -n '%s'\", expectedText)\n\n\t// Verify that we can SSH to the instance and run commands.\n\tretry.DoWithRetry(t, description, maxRetries, timeBetweenRetries, func() (string, error) {\n\t\tactualText, err := ssh.CheckSshCommandE(t, publicHost, command)\n\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\tif strings.TrimSpace(actualText) != expectedText {\n\t\t\treturn \"\", fmt.Errorf(\"Expected SSH command to return '%s' but got '%s'\", expectedText, actualText)\n\t\t}\n\n\t\treturn \"\", nil\n\t})\n\n\t// Run a command on the server that results in an error.\n\texpectedText = \"Hello, World\"\n\tcommand = fmt.Sprintf(\"echo -n '%s' && exit 1\", expectedText)\n\tdescription = fmt.Sprintf(\"SSH to public host %s with error command\", publicInstanceIP)\n\n\t// Verify that we can SSH to the instance, run the command which forces an error, and see the output.\n\tretry.DoWithRetry(t, description, maxRetries, timeBetweenRetries, func() (string, error) {\n\t\tactualText, err := ssh.CheckSshCommandE(t, publicHost, command)\n\n\t\tif err == nil {\n\t\t\treturn \"\", fmt.Errorf(\"Expected SSH command to return an error but got none\")\n\t\t}\n\n\t\tif strings.TrimSpace(actualText) != expectedText {\n\t\t\treturn \"\", fmt.Errorf(\"Expected SSH command to return '%s' but got '%s'\", expectedText, actualText)\n\t\t}\n\n\t\treturn \"\", nil\n\t})\n}\n"
  },
  {
    "path": "test/terraform_ssh_example_test.go",
    "content": "package test\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/gruntwork-io/terratest/modules/aws\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/gruntwork-io/terratest/modules/retry\"\n\t\"github.com/gruntwork-io/terratest/modules/ssh\"\n\t\"github.com/gruntwork-io/terratest/modules/terraform\"\n\ttest_structure \"github.com/gruntwork-io/terratest/modules/test-structure\"\n)\n\n// An example of how to test the Terraform module in examples/terraform-ssh-example using Terratest. The test also\n// shows an example of how to break a test down into \"stages\" so you can skip stages by setting environment variables\n// (e.g., skip stage \"teardown\" by setting the environment variable \"SKIP_teardown=true\"), which speeds up iteration\n// when running this test over and over again locally.\nfunc TestTerraformSshExample(t *testing.T) {\n\tt.Parallel()\n\n\texampleFolder := test_structure.CopyTerraformFolderToTemp(t, \"../\", \"examples/terraform-ssh-example\")\n\n\t// At the end of the test, run `terraform destroy` to clean up any resources that were created\n\tdefer test_structure.RunTestStage(t, \"teardown\", func() {\n\t\tterraformOptions := test_structure.LoadTerraformOptions(t, exampleFolder)\n\t\tterraform.Destroy(t, terraformOptions)\n\n\t\tkeyPair := test_structure.LoadEc2KeyPair(t, exampleFolder)\n\t\taws.DeleteEC2KeyPair(t, keyPair)\n\t})\n\n\t// Deploy the example\n\ttest_structure.RunTestStage(t, \"setup\", func() {\n\t\tterraformOptions, keyPair := configureTerraformOptions(t, exampleFolder)\n\n\t\t// Save the options and key pair so later test stages can use them\n\t\ttest_structure.SaveTerraformOptions(t, exampleFolder, terraformOptions)\n\t\ttest_structure.SaveEc2KeyPair(t, exampleFolder, keyPair)\n\n\t\t// This will run `terraform init` and `terraform apply` and fail the test if there are any errors\n\t\tterraform.InitAndApply(t, terraformOptions)\n\t})\n\n\t// Make sure we can SSH to the public Instance directly from the public Internet and the private Instance by using\n\t// the public Instance as a jump host\n\ttest_structure.RunTestStage(t, \"validate\", func() {\n\t\tterraformOptions := test_structure.LoadTerraformOptions(t, exampleFolder)\n\t\tkeyPair := test_structure.LoadEc2KeyPair(t, exampleFolder)\n\n\t\ttestSSHToPublicHost(t, terraformOptions, keyPair)\n\t\ttestSSHToPrivateHost(t, terraformOptions, keyPair)\n\t\ttestSSHAgentToPublicHost(t, terraformOptions, keyPair)\n\t\ttestSSHAgentToPrivateHost(t, terraformOptions, keyPair)\n\t\ttestSCPToPublicHost(t, terraformOptions, keyPair)\n\t})\n\n}\n\nfunc configureTerraformOptions(t *testing.T, exampleFolder string) (*terraform.Options, *aws.Ec2Keypair) {\n\t// A unique ID we can use to namespace resources so we don't clash with anything already in the AWS account or\n\t// tests running in parallel\n\tuniqueID := random.UniqueId()\n\n\t// Give this EC2 Instance and other resources in the Terraform code a name with a unique ID so it doesn't clash\n\t// with anything else in the AWS account.\n\tinstanceName := fmt.Sprintf(\"terratest-ssh-example-%s\", uniqueID)\n\n\t// Pick a random AWS region to test in. This helps ensure your code works in all regions.\n\tawsRegion := aws.GetRandomStableRegion(t, nil, nil)\n\n\t// Some AWS regions are missing certain instance types, so pick an available type based on the region we picked\n\tinstanceType := aws.GetRecommendedInstanceType(t, awsRegion, []string{\"t2.micro, t3.micro\", \"t2.small\", \"t3.small\"})\n\n\t// Create an EC2 KeyPair that we can use for SSH access\n\tkeyPairName := fmt.Sprintf(\"terratest-ssh-example-%s\", uniqueID)\n\tkeyPair := aws.CreateAndImportEC2KeyPair(t, awsRegion, keyPairName)\n\n\t// Construct the terraform options with default retryable errors to handle the most common retryable errors in\n\t// terraform testing.\n\tterraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{\n\t\t// The path to where our Terraform code is located\n\t\tTerraformDir: exampleFolder,\n\n\t\t// Variables to pass to our Terraform code using -var options\n\t\tVars: map[string]interface{}{\n\t\t\t\"aws_region\":    awsRegion,\n\t\t\t\"instance_name\": instanceName,\n\t\t\t\"instance_type\": instanceType,\n\t\t\t\"key_pair_name\": keyPairName,\n\t\t},\n\t})\n\n\treturn terraformOptions, keyPair\n}\n\nfunc testSSHToPublicHost(t *testing.T, terraformOptions *terraform.Options, keyPair *aws.Ec2Keypair) {\n\t// Run `terraform output` to get the value of an output variable\n\tpublicInstanceIP := terraform.Output(t, terraformOptions, \"public_instance_ip\")\n\n\t// We're going to try to SSH to the instance IP, using the Key Pair we created earlier, and the user \"ubuntu\",\n\t// as we know the Instance is running an Ubuntu AMI that has such a user\n\tpublicHost := ssh.Host{\n\t\tHostname:    publicInstanceIP,\n\t\tSshKeyPair:  keyPair.KeyPair,\n\t\tSshUserName: \"ubuntu\",\n\t}\n\n\t// It can take a minute or so for the Instance to boot up, so retry a few times\n\tmaxRetries := 30\n\ttimeBetweenRetries := 5 * time.Second\n\tdescription := fmt.Sprintf(\"SSH to public host %s\", publicInstanceIP)\n\n\t// Run a simple echo command on the server\n\texpectedText := \"Hello, World\"\n\tcommand := fmt.Sprintf(\"echo -n '%s'\", expectedText)\n\n\t// Verify that we can SSH to the Instance and run commands\n\tretry.DoWithRetry(t, description, maxRetries, timeBetweenRetries, func() (string, error) {\n\t\tactualText, err := ssh.CheckSshCommandE(t, publicHost, command)\n\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\tif strings.TrimSpace(actualText) != expectedText {\n\t\t\treturn \"\", fmt.Errorf(\"Expected SSH command to return '%s' but got '%s'\", expectedText, actualText)\n\t\t}\n\n\t\treturn \"\", nil\n\t})\n\n\t// Run a command on the server that results in an error,\n\texpectedText = \"Hello, World\"\n\tcommand = fmt.Sprintf(\"echo -n '%s' && exit 1\", expectedText)\n\tdescription = fmt.Sprintf(\"SSH to public host %s with error command\", publicInstanceIP)\n\n\t// Verify that we can SSH to the Instance, run the command and see the output\n\tretry.DoWithRetry(t, description, maxRetries, timeBetweenRetries, func() (string, error) {\n\n\t\tactualText, err := ssh.CheckSshCommandE(t, publicHost, command)\n\n\t\tif err == nil {\n\t\t\treturn \"\", fmt.Errorf(\"Expected SSH command to return an error but got none\")\n\t\t}\n\n\t\tif strings.TrimSpace(actualText) != expectedText {\n\t\t\treturn \"\", fmt.Errorf(\"Expected SSH command to return '%s' but got '%s'\", expectedText, actualText)\n\t\t}\n\n\t\treturn \"\", nil\n\t})\n}\n\nfunc testSSHToPrivateHostByHostname(t *testing.T, terraformOptions *terraform.Options, keyPair *aws.Ec2Keypair) {\n\t// Run `terraform output` to get the value of an output variable\n\tpublicInstanceIP := terraform.Output(t, terraformOptions, \"public_instance_ip\")\n\n\t// Get hostname of private instance from AWS helper function instead of Terraform output\n\tprivateInstanceID := terraform.Output(t, terraformOptions, \"private_instance_id\")\n\tdeployedAWSRegion := terraformOptions.Vars[\"aws_region\"].(string)\n\tprivateInstanceHostname := aws.GetPrivateHostnameOfEc2Instance(t, privateInstanceID, deployedAWSRegion)\n\n\tsshToPrivateHost(t, publicInstanceIP, privateInstanceHostname, keyPair)\n}\n\nfunc testSSHToPrivateHost(t *testing.T, terraformOptions *terraform.Options, keyPair *aws.Ec2Keypair) {\n\t// Run `terraform output` to get the value of an output variable\n\tpublicInstanceIP := terraform.Output(t, terraformOptions, \"public_instance_ip\")\n\n\t// Get IP of private instance from AWS helper function instead of Terraform output\n\tprivateInstanceID := terraform.Output(t, terraformOptions, \"private_instance_id\")\n\tdeployedAWSRegion := terraformOptions.Vars[\"aws_region\"].(string)\n\tprivateInstanceIP := aws.GetPrivateIpOfEc2Instance(t, privateInstanceID, deployedAWSRegion)\n\n\tsshToPrivateHost(t, publicInstanceIP, privateInstanceIP, keyPair)\n}\n\nfunc sshToPrivateHost(t *testing.T, publicInstanceIP string, privateInstanceIP string, keyPair *aws.Ec2Keypair) {\n\t// We're going to try to SSH to the private instance using the public instance as a jump host. For both instances,\n\t// we are using the Key Pair we created earlier, and the user \"ubuntu\", as we know the Instances are running an\n\t// Ubuntu AMI that has such a user\n\tpublicHost := ssh.Host{\n\t\tHostname:    publicInstanceIP,\n\t\tSshKeyPair:  keyPair.KeyPair,\n\t\tSshUserName: \"ubuntu\",\n\t}\n\tprivateHost := ssh.Host{\n\t\tHostname:    privateInstanceIP,\n\t\tSshKeyPair:  keyPair.KeyPair,\n\t\tSshUserName: \"ubuntu\",\n\t}\n\n\t// It can take a minute or so for the Instance to boot up, so retry a few times\n\tmaxRetries := 30\n\ttimeBetweenRetries := 5 * time.Second\n\tdescription := fmt.Sprintf(\"SSH to private host %s via public host %s\", privateInstanceIP, publicInstanceIP)\n\n\t// Run a simple echo command on the server\n\texpectedText := \"Hello, World\"\n\tcommand := fmt.Sprintf(\"echo -n '%s'\", expectedText)\n\n\t// Verify that we can SSH to the Instance and run commands\n\tretry.DoWithRetry(t, description, maxRetries, timeBetweenRetries, func() (string, error) {\n\t\tactualText, err := ssh.CheckPrivateSshConnectionE(t, publicHost, privateHost, command)\n\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\tif strings.TrimSpace(actualText) != expectedText {\n\t\t\treturn \"\", fmt.Errorf(\"Expected SSH command to return '%s' but got '%s'\", expectedText, actualText)\n\t\t}\n\n\t\treturn \"\", nil\n\t})\n}\n\nfunc testSCPToPublicHost(t *testing.T, terraformOptions *terraform.Options, keyPair *aws.Ec2Keypair) {\n\t// Run `terraform output` to get the value of an output variable\n\tpublicInstanceIP := terraform.Output(t, terraformOptions, \"public_instance_ip\")\n\n\t// We're going to try to SSH to the instance IP, using the Key Pair we created earlier, and the user \"ubuntu\",\n\t// as we know the Instance is running an Ubuntu AMI that has such a user\n\tpublicHost := ssh.Host{\n\t\tHostname:    publicInstanceIP,\n\t\tSshKeyPair:  keyPair.KeyPair,\n\t\tSshUserName: \"ubuntu\",\n\t}\n\n\t// It can take a minute or so for the Instance to boot up, so retry a few times\n\tmaxRetries := 10\n\ttimeBetweenRetries := 1 * time.Second\n\tdescription := fmt.Sprintf(\"SCP file to public host %s\", publicInstanceIP)\n\n\t// Run a simple echo command on the server\n\texpectedText := \"Hello, World\"\n\n\t// Verify that we can SSH to the Instance and run commands\n\tretry.DoWithRetry(t, description, maxRetries, timeBetweenRetries, func() (string, error) {\n\t\terr := ssh.ScpFileToE(t, publicHost, os.FileMode(0644), \"/tmp/test.txt\", expectedText)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\tactualText, err := ssh.FetchContentsOfFileE(t, publicHost, false, \"/tmp/test.txt\")\n\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\tif strings.TrimSpace(actualText) != expectedText {\n\t\t\treturn \"\", fmt.Errorf(\"Expected SSH command to return '%s' but got '%s'\", expectedText, actualText)\n\t\t}\n\n\t\treturn \"\", nil\n\t})\n}\n\nfunc testSSHAgentToPublicHost(t *testing.T, terraformOptions *terraform.Options, keyPair *aws.Ec2Keypair) {\n\t// Run `terraform output` to get the value of an output variable\n\tpublicInstanceIP := terraform.Output(t, terraformOptions, \"public_instance_ip\")\n\n\t// start the ssh agent\n\tsshAgent := ssh.SshAgentWithKeyPair(t, keyPair.KeyPair)\n\tdefer sshAgent.Stop()\n\n\t// We're going to try to SSH to the instance IP, using the Key Pair we created earlier. Instead of\n\t// directly using the SSH key in the SSH connection, we're going to rely on an existing SSH agent that we\n\t// programatically emulate within this test. We're going to use the user \"ubuntu\" as we know the Instance\n\t// is running an Ubuntu AMI that has such a user\n\tpublicHost := ssh.Host{\n\t\tHostname:         publicInstanceIP,\n\t\tSshUserName:      \"ubuntu\",\n\t\tOverrideSshAgent: sshAgent,\n\t}\n\n\t// It can take a minute or so for the Instance to boot up, so retry a few times\n\tmaxRetries := 30\n\ttimeBetweenRetries := 5 * time.Second\n\tdescription := fmt.Sprintf(\"SSH with Agent to public host %s\", publicInstanceIP)\n\n\t// Run a simple echo command on the server\n\texpectedText := \"Hello, World\"\n\tcommand := fmt.Sprintf(\"echo -n '%s'\", expectedText)\n\n\t// Verify that we can SSH to the Instance and run commands\n\tretry.DoWithRetry(t, description, maxRetries, timeBetweenRetries, func() (string, error) {\n\n\t\tactualText, err := ssh.CheckSshCommandE(t, publicHost, command)\n\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\tif strings.TrimSpace(actualText) != expectedText {\n\t\t\treturn \"\", fmt.Errorf(\"Expected SSH command to return '%s' but got '%s'\", expectedText, actualText)\n\t\t}\n\n\t\treturn \"\", nil\n\t})\n}\n\nfunc testSSHAgentToPrivateHost(t *testing.T, terraformOptions *terraform.Options, keyPair *aws.Ec2Keypair) {\n\t// Run `terraform output` to get the value of an output variable\n\tpublicInstanceIP := terraform.Output(t, terraformOptions, \"public_instance_ip\")\n\tprivateInstanceIP := terraform.Output(t, terraformOptions, \"private_instance_ip\")\n\n\t// start the ssh agent\n\tsshAgent := ssh.SshAgentWithKeyPair(t, keyPair.KeyPair)\n\tdefer sshAgent.Stop()\n\n\t// We're going to try to SSH to the private instance using the public instance as a jump host. Instead of\n\t// directly using the SSH key in the SSH connection, we're going to rely on an existing SSH agent that we\n\t// programatically emulate within this test. For both instances, we are using the Key Pair we created earlier,\n\t// and the user \"ubuntu\", as we know the Instances are running an Ubuntu AMI that has such a user\n\tpublicHost := ssh.Host{\n\t\tHostname:         publicInstanceIP,\n\t\tSshUserName:      \"ubuntu\",\n\t\tOverrideSshAgent: sshAgent,\n\t}\n\tprivateHost := ssh.Host{\n\t\tHostname:         privateInstanceIP,\n\t\tSshUserName:      \"ubuntu\",\n\t\tOverrideSshAgent: sshAgent,\n\t}\n\n\t// It can take a minute or so for the Instance to boot up, so retry a few times\n\tmaxRetries := 30\n\ttimeBetweenRetries := 5 * time.Second\n\tdescription := fmt.Sprintf(\"SSH with Agent to private host %s via public host %s\", privateInstanceIP, publicInstanceIP)\n\n\t// Run a simple echo command on the server\n\texpectedText := \"Hello, World\"\n\tcommand := fmt.Sprintf(\"echo -n '%s'\", expectedText)\n\n\t// Verify that we can SSH to the Instance and run commands\n\tretry.DoWithRetry(t, description, maxRetries, timeBetweenRetries, func() (string, error) {\n\n\t\tactualText, err := ssh.CheckPrivateSshConnectionE(t, publicHost, privateHost, command)\n\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\tif strings.TrimSpace(actualText) != expectedText {\n\t\t\treturn \"\", fmt.Errorf(\"Expected SSH command to return '%s' but got '%s'\", expectedText, actualText)\n\t\t}\n\n\t\treturn \"\", nil\n\t})\n}\n"
  },
  {
    "path": "test/terraform_ssh_password_example_test.go",
    "content": "package test\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/gruntwork-io/terratest/modules/aws\"\n\t\"github.com/gruntwork-io/terratest/modules/random\"\n\t\"github.com/gruntwork-io/terratest/modules/retry\"\n\t\"github.com/gruntwork-io/terratest/modules/ssh\"\n\t\"github.com/gruntwork-io/terratest/modules/terraform\"\n\ttest_structure \"github.com/gruntwork-io/terratest/modules/test-structure\"\n)\n\n// An example of how to test the Terraform module in examples/terraform-ssh-password-example using Terratest. The test\n// also shows an example of how to break a test down into \"stages\" so you can skip stages by setting environment\n// variables (e.g., skip stage \"teardown\" by setting the environment variable \"SKIP_teardown=true\"), which speeds up\n// iteration when running this test over and over again locally.\nfunc TestTerraformSshPasswordExample(t *testing.T) {\n\tt.Parallel()\n\n\texampleFolder := test_structure.CopyTerraformFolderToTemp(t, \"../\", \"examples/terraform-ssh-password-example\")\n\n\t// At the end of the test, run `terraform destroy` to clean up any resources that were created.\n\tdefer test_structure.RunTestStage(t, \"teardown\", func() {\n\t\tterraformOptions := test_structure.LoadTerraformOptions(t, exampleFolder)\n\t\tterraform.Destroy(t, terraformOptions)\n\t})\n\n\t// Deploy the example.\n\ttest_structure.RunTestStage(t, \"setup\", func() {\n\t\tterraformOptions := configureTerraformSshPasswordOptions(t, exampleFolder)\n\n\t\t// Save the options so later test stages can use them.\n\t\ttest_structure.SaveTerraformOptions(t, exampleFolder, terraformOptions)\n\n\t\t// This will run `terraform init` and `terraform apply` and fail the test if there are any errors.\n\t\tterraform.InitAndApply(t, terraformOptions)\n\t})\n\n\t// Make sure we can SSH to the public instance directly from the public internet.\n\ttest_structure.RunTestStage(t, \"validate\", func() {\n\t\tterraformOptions := test_structure.LoadTerraformOptions(t, exampleFolder)\n\n\t\ttestSSHPasswordToPublicHost(t, terraformOptions)\n\t})\n}\n\nfunc configureTerraformSshPasswordOptions(t *testing.T, exampleFolder string) *terraform.Options {\n\t// A unique ID we can use to namespace resources so we don't clash with anything already in the AWS account or\n\t// tests running in parallel.\n\tuniqueID := random.UniqueId()\n\n\t// Give this EC2 instance and other resources in the Terraform code a name with a unique ID so it doesn't clash\n\t// with anything else in the AWS account.\n\tinstanceName := fmt.Sprintf(\"terratest-ssh-password-example-%s\", uniqueID)\n\n\t// Pick a random AWS region to test in. This helps ensure your code works in all regions.\n\tawsRegion := aws.GetRandomStableRegion(t, nil, nil)\n\n\t// Some AWS regions are missing certain instance types, so pick an available type based on the region we picked\n\tinstanceType := aws.GetRecommendedInstanceType(t, awsRegion, []string{\"t2.micro, t3.micro\", \"t2.small\", \"t3.small\"})\n\n\t// Create a random password that we can use for SSH access.\n\tpassword := random.UniqueId()\n\n\t// Construct the terraform options with default retryable errors to handle the most common retryable errors in\n\t// terraform testing.\n\tterraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{\n\t\t// The path to where our Terraform code is located.\n\t\tTerraformDir: exampleFolder,\n\n\t\t// Variables to pass to our Terraform code using -var options.\n\t\tVars: map[string]interface{}{\n\t\t\t\"aws_region\":         awsRegion,\n\t\t\t\"instance_name\":      instanceName,\n\t\t\t\"instance_type\":      instanceType,\n\t\t\t\"terratest_password\": password,\n\t\t},\n\t})\n\n\treturn terraformOptions\n}\n\nfunc testSSHPasswordToPublicHost(t *testing.T, terraformOptions *terraform.Options) {\n\t// Run `terraform output` to get the value of an output variable.\n\tpublicInstanceIP := terraform.Output(t, terraformOptions, \"public_instance_ip\")\n\n\t// We're going to try to SSH to the instance IP, using the username and password that will be set up (by\n\t// Terraform's user_data script) in the instance.\n\tpublicHost := ssh.Host{\n\t\tHostname:    publicInstanceIP,\n\t\tPassword:    terraformOptions.Vars[\"terratest_password\"].(string),\n\t\tSshUserName: \"terratest\",\n\t}\n\n\t// It can take a minute or so for the instance to boot up, so retry a few times.\n\tmaxRetries := 30\n\ttimeBetweenRetries := 10 * time.Second\n\tdescription := fmt.Sprintf(\"SSH to public host %s\", publicInstanceIP)\n\n\t// Run a simple echo command on the server.\n\texpectedText := \"Hello, World\"\n\tcommand := fmt.Sprintf(\"echo -n '%s'\", expectedText)\n\n\t// Verify that we can SSH to the instance and run commands.\n\tretry.DoWithRetry(t, description, maxRetries, timeBetweenRetries, func() (string, error) {\n\t\tactualText, err := ssh.CheckSshCommandE(t, publicHost, command)\n\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\tif strings.TrimSpace(actualText) != expectedText {\n\t\t\treturn \"\", fmt.Errorf(\"Expected SSH command to return '%s' but got '%s'\", expectedText, actualText)\n\t\t}\n\n\t\treturn \"\", nil\n\t})\n\n\t// Run a command on the server that results in an error.\n\texpectedText = \"Hello, World\"\n\tcommand = fmt.Sprintf(\"echo -n '%s' && exit 1\", expectedText)\n\tdescription = fmt.Sprintf(\"SSH to public host %s with error command\", publicInstanceIP)\n\n\t// Verify that we can SSH to the instance, run the command which forces an error, and see the output.\n\tretry.DoWithRetry(t, description, maxRetries, timeBetweenRetries, func() (string, error) {\n\t\tactualText, err := ssh.CheckSshCommandE(t, publicHost, command)\n\n\t\tif err == nil {\n\t\t\treturn \"\", fmt.Errorf(\"Expected SSH command to return an error but got none\")\n\t\t}\n\n\t\tif strings.TrimSpace(actualText) != expectedText {\n\t\t\treturn \"\", fmt.Errorf(\"Expected SSH command to return '%s' but got '%s'\", expectedText, actualText)\n\t\t}\n\n\t\treturn \"\", nil\n\t})\n}\n"
  },
  {
    "path": "test/terraform_unit_null_test.go",
    "content": "package test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terratest/modules/terraform\"\n\t\"github.com/magiconair/properties/assert\"\n)\n\nfunc TestUnitNullInput(t *testing.T) {\n\tt.Parallel()\n\n\tfoo := map[string]interface{}{\n\t\t\"nullable_string\":    nil,\n\t\t\"nonnullable_string\": \"foo\",\n\t}\n\toptions := &terraform.Options{\n\t\tTerraformDir: \"./fixtures/terraform-null\",\n\t\tVars:         map[string]interface{}{\"foo\": foo},\n\t}\n\tterraform.InitAndApply(t, options)\n\n\tfooOut := terraform.OutputMap(t, options, \"foo\")\n\tassert.Equal(t, fooOut, map[string]string{\"nonnullable_string\": \"foo\", \"nullable_string\": \"<nil>\"})\n\n\tbarOut := terraform.Output(t, options, \"bar\")\n\tassert.Equal(t, barOut, \"I AM NULL\")\n}\n"
  },
  {
    "path": "test-docker-images/README.md",
    "content": "# Gruntwork Terratest Docker Images\n\nAs part of writing [Unit Tests with Terratest](/README.md#unit-tests), we recommend using [Packer](https://packer.io) to\nbuild a Docker image using the same script [provisioners](https://www.packer.io/docs/templates/provisioners.html) that\nPacker uses to configure the Amazon Machine Image you would normally build for production usage. Docker images build 10x\nfaster than AMIs and launch 100x faster, reducing our cycle time while developing.\n\nBut Packer's Docker image builds can still be slower than desired because, unlike a native `docker build` command against\na `Dockerfile`, Packer does not use any [image caching](https://docs.docker.com/v17.09/engine/userguide/eng-image/dockerfile_best-practices/).\nAs a result, each `packer build` creates the Docker image from scratch. Unfortunately, much of the Docker image build\ntime is spent downloading libraries like `curl` and `sudo` which we assume are present on the AWS AMI associated with\nUbuntu, Amazon Linux, CentOS, or any other Linux distro we're supporting.\n\nWe solve this problem by creating canonical Gruntwork Terratest Docker Images which have most of the desired libraries\npre-installed. We upload these images to a public Docker Hub repo such as https://hub.docker.com/r/gruntwork/ubuntu-test/ so\nthat Packer templates that build Docker images can reference them directly as in the following example.\n\n### Sample Packer Builder\n\n```json\n{\n  \"builders\": [{\n    \"name\": \"ubuntu-ami\",\n    \"type\": \"amazon-ebs\"\n    // ... (other params omitted) ...\n  },{\n    \"name\": \"ubuntu-docker\",\n    \"type\": \"docker\",\n    \"image\": \"gruntwork/ubuntu-test:18.04\",\n    \"commit\": \"true\"\n  }],\n  \"provisioners\": [\n    // ...\n  ],\n  \"post-processors\": [{\n    \"type\": \"docker-tag\",\n    \"repository\": \"gruntwork/example\",\n    \"tag\": \"latest\",\n    \"only\": [\"ubuntu-docker\"]\n  }]\n}\n```\n"
  },
  {
    "path": "test-docker-images/gruntwork-amazon-linux-test/Dockerfile",
    "content": "# TODO: Is it worth referencing a specific tag instead of latest?\nFROM amazonlinux:2017.12\n\n# Reduce Docker image size per https://blog.replicated.com/refactoring-a-dockerfile-for-image-size/\n# - perl-Digest-SHA: installs shasum\nRUN yum update -y && \\\n    yum upgrade -y && \\\n    yum install -y \\\n        hostname \\\n        jq \\\n        perl-Digest-SHA \\\n        rsyslog \\\n        sudo \\\n        tar \\\n        vim \\\n        wget && \\\n        yum clean all && rm -rf /var/cache/yum\n\n# Installing pip with yum doesn't actually put it in the PATH, so we use easy_install instead. Pip will now be placed\n# in /usr/local/bin, but amazonlinux's sudo uses a sanitzed PATH that does not include /usr/local/bin, so we symlink pip.\n# The last line upgrades pip to the latest version.\nRUN curl https://bootstrap.pypa.io/ez_setup.py | sudo /usr/bin/python && \\\n    easy_install pip && \\\n    pip install --upgrade pip\n\n# Install the AWSCLI (which apparently does not come pre-bundled with Amazon Linux!)\nRUN pip install awscli --upgrade\n\n# Ideally, we'd install the latest version of Docker to avoid a conflict between the Docker client in this container\n# and the Docker API on your local host, but installing the latest version of Docker yields the error \"Requires:\n# container-selinux >= 2.9\", whch indicates that a newer Linux kernel version is required than what comes with Amazon Linux.\n# So we settle for the Amazon Linux supported version for now.\nRUN yum install -y docker"
  },
  {
    "path": "test-docker-images/gruntwork-amazon-linux-test/README.md",
    "content": "# Gruntwork Amazon-Linux-Test Docker Image\n\nThe purpose of this Docker image is to provide a pre-built Amazon Linux Docker image that has most of the libraries\nwe would expect to be installed on the Amazon Linux AMI that would run in AWS. For example, we'd expect `sudo` in AWS,\nbut it doesn't exist by default in Docker `amazonlinux:latest`.\n\n### Building and Pushing a New Docker Image to Docker Hub\n\nThis Docker image should publicly accessible via Docker Hub at https://hub.docker.com/r/gruntwork/amazonlinux-test/. To build and\nupload it:\n\n1. `docker build -t gruntwork/amazon-linux-test:2017.12 .`\n1. `docker push gruntwork/amazon-linux-test:2017.12`\n\n"
  },
  {
    "path": "test-docker-images/gruntwork-centos-test/Dockerfile",
    "content": "FROM centos/systemd:latest\n\n# Reduce Docker image size per https://blog.replicated.com/refactoring-a-dockerfile-for-image-size/\n# - perl-Digest-SHA: installs shasum\nRUN yum update -y && \\\n    yum upgrade -y && \\\n    yum install -y epel-release && \\\n    yum install -y \\\n        bind-utils \\\n        perl-Digest-SHA \\\n        python-pip \\\n        rsyslog \\\n        sudo \\\n        vim \\\n        wget && \\\n        yum clean all && rm -rf /var/cache/yum\n\n# Install jq. Oddly, there's no RPM for jq, so we install the binary directly. https://serverfault.com/a/768061/199943\nRUN wget -O jq https://github.com/stedolan/jq/releases/download/jq-1.5/jq-linux64 && \\\n    chmod +x ./jq && \\\n    cp jq /usr/bin\n\n# Install the AWS CLI per https://docs.aws.amazon.com/cli/latest/userguide/installing.html.\nRUN pip install --upgrade pip && \\\n    pip install --upgrade setuptools && \\\n    pip install awscli --upgrade\n\n# Install the latest version of Docker, Consumer Edition\nRUN yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo && \\\n    yum -y install docker-ce && \\\n    yum clean all\n\n# We run systemd as our container process. Systemd can spawn other forks as necessary to help us simulate a real-world\n# CentOS systemd environment.\nCMD [\"/usr/sbin/init\"]\n\n# NOTE! This Docker container should be run with the following runtime options to ensure that systemd works correctly:\n# Although this bind-mounted volume would appear at first glance not to work on MacOS or Windows, because those OSs are\n# running a VM to execute Docker and only a limited set of paths are mounted directly from the host, Docker is able to\n# use the Linux VM's privileges to execute systemd correctly.\n#\n# docker run -d --privileged -v /sys/fs/cgroup:/sys/fs/cgroup:ro gruntwork/centos-test\n"
  },
  {
    "path": "test-docker-images/gruntwork-centos-test/README.md",
    "content": "# Gruntwork CentOS-Test Docker Image\n\nThe purpose of this Docker image is to provide a pre-built CentOS 7 Docker image that has most of the libraries\nwe would expect to be installed on the CentOS 7 AMI that would run in AWS. For example, we'd expect `sudo` in AWS, but it\ndoesn't exist by default in Docker `centos:7`. It also aims to allow [systemd](https://www.freedesktop.org/wiki/Software/systemd/)\nto run, which, in turn, allows you to run one or more services as [systemd units](https://www.freedesktop.org/software/systemd/man/systemd.unit.html).\n\n### Building and Pushing a New Docker Image to Docker Hub\n\nThis Docker image should publicly accessible via Docker Hub at https://hub.docker.com/r/gruntwork/centos-test/. To build and\nupload it:\n\n1. `docker build -t gruntwork/centos-test:7 .`\n1. `docker push gruntwork/centos-test:7`\n\n### Running this Docker Image\n\nRunning systemd require elevated privileges for the Docker container, so you should run this Docker image with at least\nthe following options:\n\n```\ndocker run -d --privileged -v /sys/fs/cgroup:/sys/fs/cgroup:ro gruntwork/zookeeper-centos-test:latest\n```\n\nNote that:\n\n- We do not specify a run command like `/bin/bash` because we need to retain the Docker Image's default run command of\n  `/usr/sbin/init`. This makes systemd Process ID 1, which allows it to spawn an arbitrary number of other services\n- You can then connect to the Docker container with `docker exec -it <container-id> /bin/bash`.\n- The container must be `--privileged` because it needs to break out of the typical [cgroups](\n  https://docs.docker.com/engine/docker-overview/#the-underlying-technology) to run an init system like systemd.\n- You must \"hook in\" to a Linux host's cgroups to allow each service to run in its own cgroup. This works even on Docker\n  for Mac and Docker for Windows because those systems still use a Linux VM to run the Docker engine and do not expose\n  the entire host system (e.g. your Mac laptop) for docker volume mounting.\n\n\n"
  },
  {
    "path": "test-docker-images/gruntwork-ubuntu-test/Dockerfile",
    "content": "FROM ubuntu:22.04\n\n# Reduce Docker image size per https://blog.replicated.com/refactoring-a-dockerfile-for-image-size/\n# - dnsutils: Install handy DNS checking tools like dig\n# - libcrypt-hcesha-perl: Install shasum\n# - software-properties-common: Install add-apt-repository\nRUN DEBIAN_FRONTEND=noninteractive \\\n    apt-get update && \\\n    apt-get upgrade -y && \\\n    apt-get install --no-install-recommends -y \\\n        gpg-agent \\\n        apt-transport-https \\\n        ca-certificates \\\n        curl \\\n        dnsutils \\\n        jq \\\n        libcrypt-hcesha-perl \\\n        python3 \\\n        python3-pip \\\n        rsyslog \\\n        software-properties-common \\\n        sudo \\\n        vim \\\n        wget && \\\n        rm -rf /var/lib/apt/lists/*\n\n# Install the AWS CLI per https://docs.aws.amazon.com/cli/latest/userguide/installing.html. The last line upgrades pip\n# to the latest version.\nRUN pip3 install --upgrade setuptools && \\\n    pip3 install awscli --upgrade\n\n# Install the latest version of Docker, Consumer Edition\nRUN curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add - && \\\n    add-apt-repository \\\n       \"deb [arch=amd64] https://download.docker.com/linux/ubuntu \\\n       $(lsb_release -cs) \\\n       stable\" && \\\n    apt-get update && \\\n    apt-get -y install docker-ce && \\\n    rm -rf /var/lib/apt/lists/*\n"
  },
  {
    "path": "test-docker-images/gruntwork-ubuntu-test/README.md",
    "content": "# Gruntwork Ubuntu-Test Docker Image\n\nThe purpose of this Docker image is to provide a pre-built Ubuntu 18.04 Docker image that has most of the libraries\nwe would expect to be installed on the Ubuntu 18.04 AMI that would run in AWS. For example, we'd expect `curl` in AWS,\nbut it doesn't exist by default in Docker `ubuntu:18.04`.\n\n### Building and Pushing a New Docker Image to Docker Hub\n\nThis Docker image should publicly accessible via Docker Hub at https://hub.docker.com/r/gruntwork/ubuntu-test/. To build and\nupload it:\n\n1. `docker build -t gruntwork/ubuntu-test:18.04 .`\n1. `docker push gruntwork/ubuntu-test:18.04`\n\n"
  },
  {
    "path": "test-docker-images/moto/Dockerfile",
    "content": "FROM ubuntu:16.04\n\n# Reduce Docker image size per https://blog.replicated.com/refactoring-a-dockerfile-for-image-size/\nRUN DEBIAN_FRONTEND=noninteractive apt-get update && apt-get install --no-install-recommends -y \\\n    python-pip && \\\n    rm -rf /var/lib/apt/lists/* && \\\n    pip install --upgrade pip && \\\n    pip install --upgrade setuptools && \\\n    pip install --upgrade flask && \\\n    pip install --upgrade pyOpenSSL && \\\n    pip install --upgrade moto"
  },
  {
    "path": "test-docker-images/moto/README.md",
    "content": "# Moto\n\nThis docker image runs [moto](https://github.com/spulec/moto) as a service. We will use Moto as a local service that\naccepts AWS API calls and returns valid API responses. Moto can be used with any AWS SDK, including the [awscli](\nhttps://aws.amazon.com/cli/)[].\n\nThis Docker image is expected to run alongside a cluster of docker containers to represent a \"local AWS\".\n\n### About Moto\n\nMoto was originally written as a way to mock out the Python [boto](https://github.com/boto/boto3) library, the official\nAWS SDK for Python. It was motivated by the need to write automated tests for boto. But since the AWS API cannot run\n\"locally\", Moto was written to mock the responses of the AWS API.\n\nMoto works by receiving AWS API requests across a wide variety of AWS services, including most of EC2. It will then store\nthe requested AWS resource in memory and allow you to query that AWS resource using standard AWS API calls. There is\nno actual VM created, or other actual resource created.\n\n### Motivation\n\nAs part of writing [Unit Tests with Terratest](/README.md#unit-tests), we need a way to run our services in a Docker\ncontainer. But this presents a new challenge: Almost all our cluster-based setups query the AWS APIs to obtain metadata\nabout the EC2 Instance on which they're running. How can we simulate these API calls in a local environment? Moto seems\nto meet this use case perfectly.\n\n## Usage\n\n### Building and Pushing a New Docker Image to Docker Hub\n\nThis Docker image should publicly accessible via Docker Hub at https://hub.docker.com/r/gruntwork/moto/. To build and\nupload it:\n\n1. `docker build -t gruntwork/moto:v1 .`\n1. `docker push gruntwork/moto:v1`\n\n#### Run a Docker container\n\n```\ndocker run -p 5000:5000 gruntwork/moto moto_server ec2 --host 0.0.0.0\n```\n\nThis runs the `moto` service as a RESTful API, specially for the AWS EC2 API with support for acceping connections from\nany IP address (versus just from localhost). For additional information:\n- See the [moto stand-alone server usage docs](https://github.com/spulec/moto#stand-alone-server-mode)\n- See [which AWS services are supported](https://github.com/spulec/moto#in-a-nutshell)\n\n#### Make AWS API calls against Moto\n\nBecause Moto exposes an API that is intended to be identical to the official AWS API, you can use any any AWS SDK against\nit, including the AWS CLI, AWS SDK for Go, `curl` calls, or any other AWS API library. The only difference is that you\nmust explicitly set the \"endpoint uRL\" to point to the Moto server instead of the official AWS API. Changing this setting\nwill be different for each AWS SDK, but for the AWSCLI, you can simplify specify the `--endpoint-url` argument as follows:\n\n```\naws --region \"us-west-2\" --endpoint-url=\"http://localhost:5000\" ec2 run-instances --image-id ami-abc12345 --tag-specifications 'ResourceType=instance,Tags=[{Key=ServerGroupName,Value=josh}]'\n```\n\nNote that Moto supports all AWS regions, and will automatically create a VPC with default subnets for you!\n\n## Other Solutions Considered\n\n### LocalStack\n\n[LocalStack](https://localstack.cloud/) is a (coming soon) commercial service intending to offer \"local AWS as a service\".\nIt is based on the open source [localstack](https://github.com/localstack/localstack) project.\n\nLocal stack seems to offer a [small set of advantages](https://github.com/localstack/localstack#why-localstack) over\nMoto, including throwing periodic errors to simulate a real-world cloud environment. But Localstack doesn't implement\n100% of the APIs implemented by Moto (including EC2, the most important one for us!), its docker image is ~500 MB, it\ndoesn't appear to be an active commercial entity, and Moto supports a RESTful API already.\n\nFor these reasons, Moto is the better fit for our needs."
  }
]