├── .devcontainer └── devcontainer.json ├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ ├── codeql-analysis.yml │ └── test.yml ├── .gitignore ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── CONTRIBUTING.md ├── GitVersion.yml ├── LICENSE ├── README.md ├── _test-agent ├── temp │ └── .gitignore └── tools │ └── .gitignore ├── overview.md ├── package-lock.json ├── package.json ├── pipelines ├── main.yml ├── publish │ └── poll_task_install.sh ├── templates │ ├── build_npm.yml │ ├── build_tasks.yml │ ├── download_artifacts.yml │ ├── integration_tests.yml │ ├── test │ │ ├── aws_self_configured.yml │ │ ├── aws_service_connection.yml │ │ ├── azurerm_mgmt_group_service_connection.yml │ │ ├── gcp_credential_file.yml │ │ ├── init_with_self_configured_backend.yml │ │ ├── init_with_version_11.yml │ │ ├── init_without_ensure_backend.yml │ │ ├── local_exec_az_cli.yml │ │ ├── plan_with_command_options_var_file.yml │ │ ├── publish_plan_results.yml │ │ ├── smoke_test.yml │ │ ├── state_commands.yml │ │ └── switch_workspaces.yml │ └── test_npm.yml └── test.yml ├── screenshots ├── overview-tfcli-azure-sub.png ├── overview-tfcli-backend-azurerm.png ├── overview-tfcli-ensure-backend.png ├── overview-tfcli-secure-vars.jpg ├── overview-tfinstall-task-fields.png ├── overview-tfplan-view-no-plans.jpg └── overview-tfplan-view.jpg ├── scripts ├── deploy.sh ├── max_tags.sh ├── publish.sh └── set-version.sh ├── tasks ├── terraform-cli │ ├── .nycrc.yml │ ├── README.md │ ├── cucumber.js │ ├── icon.png │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── authentication │ │ │ └── azurerm.ts │ │ ├── backends │ │ │ ├── aws.ts │ │ │ ├── azurerm.ts │ │ │ ├── gcs.ts │ │ │ └── index.ts │ │ ├── commands │ │ │ ├── az-account-set.ts │ │ │ ├── az-group-create.ts │ │ │ ├── az-login.ts │ │ │ ├── az-storage-account-create.ts │ │ │ ├── az-storage-container-create.ts │ │ │ ├── index.ts │ │ │ ├── tf-apply.ts │ │ │ ├── tf-destroy.ts │ │ │ ├── tf-fmt.ts │ │ │ ├── tf-force-unlock.ts │ │ │ ├── tf-import.ts │ │ │ ├── tf-init.ts │ │ │ ├── tf-output.ts │ │ │ ├── tf-plan.ts │ │ │ ├── tf-refresh.ts │ │ │ ├── tf-show.ts │ │ │ ├── tf-state-list.ts │ │ │ ├── tf-state-move.ts │ │ │ ├── tf-state-remove.ts │ │ │ ├── tf-state.ts │ │ │ ├── tf-validate.ts │ │ │ ├── tf-version.ts │ │ │ ├── tf-workspace-new.ts │ │ │ ├── tf-workspace-select.ts │ │ │ └── tf-workspace.ts │ │ ├── context │ │ │ ├── azdo-task-context.ts │ │ │ ├── index.ts │ │ │ └── mock-task-context.ts │ │ ├── index.ts │ │ ├── logger │ │ │ ├── ai-logger.ts │ │ │ ├── index.ts │ │ │ └── task-logger.ts │ │ ├── providers │ │ │ ├── aws.ts │ │ │ ├── azurerm.ts │ │ │ ├── google.ts │ │ │ └── index.ts │ │ ├── runners │ │ │ ├── azdo-runner.ts │ │ │ ├── azdo-tool-factory.ts │ │ │ ├── builders │ │ │ │ ├── index.ts │ │ │ │ ├── run-with-auto-approve.ts │ │ │ │ ├── run-with-azcli.ts │ │ │ │ ├── run-with-backend.ts │ │ │ │ ├── run-with-command-options.ts │ │ │ │ ├── run-with-force.ts │ │ │ │ ├── run-with-forced-detailed-exit-code.ts │ │ │ │ ├── run-with-json-output.ts │ │ │ │ ├── run-with-lock-id.ts │ │ │ │ ├── run-with-options.ts │ │ │ │ ├── run-with-plan-or-state-file.ts │ │ │ │ ├── run-with-raw-outputs.ts │ │ │ │ ├── run-with-resource-addresses.ts │ │ │ │ ├── run-with-resource-target.ts │ │ │ │ ├── run-with-secure-var-file.ts │ │ │ │ ├── run-with-show-options.ts │ │ │ │ ├── run-with-source-destination.ts │ │ │ │ ├── run-with-success-codes.ts │ │ │ │ ├── run-with-terraform.ts │ │ │ │ └── run-with-workspace.ts │ │ │ ├── index.ts │ │ │ ├── mock-tool-factory.ts │ │ │ └── terraform-error.ts │ │ ├── task-agent │ │ │ ├── azdo-task-agent.ts │ │ │ ├── index.ts │ │ │ └── mock-task-agent.ts │ │ ├── task.ts │ │ └── tests │ │ │ ├── default.env │ │ │ ├── default.vars │ │ │ ├── features │ │ │ ├── publish-plan-results │ │ │ │ ├── plan-output-no-changes.txt │ │ │ │ ├── plan-output-with-1-unchanged-1-to-add.txt │ │ │ │ ├── plan-output-with-adds-destroys-and-updates.txt │ │ │ │ ├── plan-output-with-more-than-nine-changes.txt │ │ │ │ ├── plan-summary-no-changes.txt │ │ │ │ ├── plan-summary-with-1-unchanged-1-to-add.txt │ │ │ │ ├── plan-summary-with-adds-destroys-and-updates.txt │ │ │ │ ├── plan-summary-with-more-than-nine-changes.txt │ │ │ │ └── publish-plan-results.feature │ │ │ ├── state │ │ │ │ ├── stdout-state-list-addressed.txt │ │ │ │ ├── stdout-state-list-simple.txt │ │ │ │ ├── stdout-state-move-ok.txt │ │ │ │ ├── stdout-state-remove.txt │ │ │ │ ├── terraform-state.feature │ │ │ │ └── terraform.tfstate │ │ │ ├── terraform-apply-aws.feature │ │ │ ├── terraform-apply-sub-override.feature │ │ │ ├── terraform-apply.feature │ │ │ ├── terraform-destroy-aws.feature │ │ │ ├── terraform-destroy-sub-override.feature │ │ │ ├── terraform-destroy.feature │ │ │ ├── terraform-fmt.feature │ │ │ ├── terraform-force-unlock-aws.feature │ │ │ ├── terraform-force-unlock-sub-override.feature │ │ │ ├── terraform-force-unlock.feature │ │ │ ├── terraform-import-aws.feature │ │ │ ├── terraform-import-with-sub-override.feature │ │ │ ├── terraform-import.feature │ │ │ ├── terraform-init-aws.feature │ │ │ ├── terraform-init-azurerm-sub-override.feature │ │ │ ├── terraform-init-azurerm.feature │ │ │ ├── terraform-init-gcp.feature │ │ │ ├── terraform-output │ │ │ │ ├── console_tf_output_all_types.txt │ │ │ │ ├── console_tf_output_string.txt │ │ │ │ ├── stdout_tf_output_all_types.json │ │ │ │ ├── stdout_tf_output_string.json │ │ │ │ └── terraform-output.feature │ │ │ ├── terraform-plan-aws.feature │ │ │ ├── terraform-plan-with-sub-override.feature │ │ │ ├── terraform-plan.feature │ │ │ ├── terraform-refresh-aws.feature │ │ │ ├── terraform-refresh-with-sub-override.feature │ │ │ ├── terraform-refresh.feature │ │ │ ├── terraform-show.feature │ │ │ ├── terraform-validate.feature │ │ │ ├── terraform.feature │ │ │ ├── version │ │ │ │ ├── stdout_version_0_11_14.txt │ │ │ │ ├── stdout_version_0_14_10.txt │ │ │ │ ├── stdout_version_0_15_3.txt │ │ │ │ └── terraform-version.feature │ │ │ └── workspace │ │ │ │ ├── stdout-new-workspace-bar-skipExisting.txt │ │ │ │ ├── stdout-new-workspace-bar.txt │ │ │ │ ├── terraform-workspace-new.feature │ │ │ │ └── terraform-workspace-select.feature │ │ │ ├── gcp-fake-key.json │ │ │ ├── stdout_tf_show_tfplan_no_destroy.json │ │ │ ├── stdout_tf_show_tfplan_output_only.json │ │ │ ├── stdout_tf_show_tfplan_with_destroy.json │ │ │ ├── stdout_tf_show_tfplan_with_destroy_apimanagement.json │ │ │ ├── stdout_tf_show_tfplan_with_destroy_eol.json │ │ │ ├── stdout_tf_show_tfstate_version_only.json │ │ │ └── steps │ │ │ ├── mock-answer-spy.ts │ │ │ ├── task-answers.steps.ts │ │ │ ├── task-context.steps.ts │ │ │ ├── task-runner.ts │ │ │ └── task.steps.ts │ ├── task.json │ └── tsconfig.json └── terraform-installer │ ├── .env-sample │ ├── README.md │ ├── icon.png │ ├── package-lock.json │ ├── package.json │ ├── src │ ├── index.ts │ ├── installer.ts │ └── sanitizer.ts │ ├── task.json │ └── tsconfig.json ├── templates ├── .terraform │ └── .gitignore ├── aws │ ├── .terraform │ │ └── .gitignore │ └── main.tf ├── default.env ├── default.vars ├── gcp │ ├── .terraform │ │ └── .gitignore │ └── main.tf ├── local-exec-az-cli │ ├── local-exec-az-cli.vars │ └── main.tf ├── main.tf ├── sample.tf ├── variables.tf ├── version11 │ ├── .terraform │ │ └── .gitignore │ ├── main.tf │ └── version11.vars └── versions.tf ├── views └── terraform-plan │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── src │ ├── __mocks__ │ │ └── styleMock.js │ ├── index.html │ ├── index.tsx │ ├── plan-summary-tab │ │ ├── plan-summary-tab.scss │ │ ├── plan-summary-tab.test.tsx │ │ ├── plan-summary-tab.tsx │ │ ├── table-data.tsx │ │ └── test-data.tsx │ └── services │ │ └── attachments │ │ ├── azdo-attachment-service.ts │ │ ├── index.ts │ │ └── mock-attachment-service.ts │ ├── tsconfig.json │ ├── tsconfig.test.json │ └── webpack.config.js ├── vss-extension-ga.json ├── vss-extension-icon.png ├── vss-extension-rc.json └── vss-extension.json /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Node.js & TypeScript", 3 | "image": "mcr.microsoft.com/devcontainers/typescript-node:1-22-bookworm", 4 | "features": { 5 | "ghcr.io/devcontainers/features/azure-cli:1": {}, 6 | "ghcr.io/devcontainers/features/docker-in-docker:2": {}, 7 | "ghcr.io/devcontainers/features/terraform:1": {}, 8 | "ghcr.io/stuartleeks/dev-container-features/azure-cli-persistence:0": {}, 9 | "ghcr.io/stuartleeks/dev-container-features/shell-history:0": {} 10 | }, 11 | "customizations": { 12 | "vscode": { 13 | "extensions": [ 14 | "eamodio.gitlens", 15 | "redhat.vscode-yaml", 16 | "vscode-icons-team.vscode-icons", 17 | "editorconfig.editorconfig", 18 | "dbaeumer.vscode-eslint", 19 | "ecmel.vscode-html-css", 20 | "streetsidesoftware.code-spell-checker", 21 | "alexkrechik.cucumberautocomplete", 22 | "ms-azure-devops.azure-pipelines", 23 | "github.copilot", 24 | "hashicorp.terraform", 25 | "github.vscode-github-actions" 26 | ] 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Setup pipeline as (include yaml configuration or screenshots of classic ui editor) 16 | 2. Execute pipeline 17 | 3. See error 18 | 19 | **Expected behavior** 20 | A clear and concise description of what you expected to happen. 21 | 22 | **Screenshots** 23 | If applicable, add screenshots to help explain your problem. 24 | 25 | **Pipeline Logs** 26 | Include logs that help demonstrate the problem. Please make sure to redact any sensitive info such as secrets. 27 | 28 | **Agent Configuration** 29 | - OS: [e.g. ubuntu debian] 30 | - Hosted/Self Hosted 31 | - Terraform version used (Default for hosted agent is acceptable) 32 | - AzureCLI version used (Default for hosted agent is acceptable or N/A) 33 | 34 | **Additional context** 35 | Add any other context about the problem here. 36 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: CodeQL Analysis 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: '0 8 * * *' 8 | workflow_dispatch: 9 | 10 | permissions: 11 | # required for all workflows 12 | security-events: write 13 | 14 | # only required for workflows in private repositories 15 | actions: read 16 | contents: read 17 | 18 | jobs: 19 | analyze: 20 | name: Analyze 21 | runs-on: ubuntu-latest 22 | steps: 23 | - name: Checkout repository 24 | uses: actions/checkout@v2 25 | 26 | - name: Initialize CodeQL 27 | uses: github/codeql-action/init@v1 28 | with: 29 | queries: security-and-quality 30 | languages: javascript 31 | 32 | - name: Perform CodeQL Analysis 33 | uses: github/codeql-action/analyze@v1 34 | 35 | # Built with ❤ by [Pipeline Foundation](https://pipeline.foundation) 36 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: 4 | push: 5 | branches-ignore: 6 | - main 7 | pull_request: 8 | branches: [ main ] 9 | 10 | jobs: 11 | setup: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | with: 17 | fetch-depth: 0 18 | - name: Install GitVersion 19 | uses: gittools/actions/gitversion/setup@v3.0.2 20 | with: 21 | versionSpec: '5.x' 22 | - name: Determine Version 23 | id: version_step # step id used as reference for output values 24 | uses: gittools/actions/gitversion/execute@v3.0.2 25 | 26 | test-cli: 27 | runs-on: ubuntu-latest 28 | defaults: 29 | run: 30 | working-directory: tasks/terraform-cli 31 | 32 | strategy: 33 | matrix: 34 | node-version: ['18.x', '20.x'] 35 | 36 | steps: 37 | - uses: actions/checkout@v4 38 | - name: Use Node.js ${{ matrix.node-version }} 39 | uses: actions/setup-node@v4 40 | with: 41 | node-version: ${{ matrix.node-version }} 42 | - run: npm ci 43 | - run: npm test -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | .env* 75 | !.env-sample* 76 | 77 | # parcel-bundler cache (https://parceljs.org/) 78 | .cache 79 | 80 | # Next.js build output 81 | .next 82 | 83 | # Nuxt.js build / generate output 84 | .nuxt 85 | dist 86 | 87 | # Gatsby files 88 | .cache/ 89 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 90 | # https://nextjs.org/blog/next-9-1#public-directory-support 91 | # public 92 | 93 | # vuepress build output 94 | .vuepress/dist 95 | 96 | # Serverless directories 97 | .serverless/ 98 | 99 | # FuseBox cache 100 | .fusebox/ 101 | 102 | # DynamoDB Local files 103 | .dynamodb/ 104 | 105 | # TernJS port file 106 | .tern-port 107 | 108 | # built modules 109 | .bin/ 110 | 111 | # packaged marketplace extension 112 | *.vsix 113 | 114 | # unit test report for azure devops 115 | .tests/ 116 | 117 | #packaged output 118 | .dist/ 119 | 120 | # tfx runtime files 121 | .taskkey 122 | 123 | .terraform.lock.hcl 124 | dist/ 125 | 126 | azure-pipelines-tasks-terraform.code-workspace 127 | 128 | tfplan 129 | *.tfplan 130 | 131 | .idea 132 | self.json 133 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "ACCESSTOKEN", 4 | "Azdo", 5 | "SYSTEMVSSCONNECTION", 6 | "TEAMPROJECT", 7 | "Terraform", 8 | "azurerm", 9 | "backend", 10 | "devops", 11 | "injectable", 12 | "inversify", 13 | "terraform", 14 | "toolrunner", 15 | "vsts" 16 | ], 17 | "cucumberautocomplete.steps": [ 18 | "tasks/terraform-cli/src/tests/steps/*.ts" 19 | ], 20 | "cucumberautocomplete.syncfeatures": "tasks/terraform-cli/src/tests/features/*.feature", 21 | "cucumberautocomplete.strictGherkinCompletion": false, 22 | "yaml.schemas": { 23 | "https://raw.githubusercontent.com/microsoft/azure-pipelines-vscode/master/service-schema.json": "file:///workspaces/azure-pipelines-tasks-terraform/pipelines/test/test.yml" 24 | } 25 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Run Unit Tests for Terraform CLI Task 4 | 5 | 1. Navigate to `cd tasks\terraform-cli`. 6 | 1. Run `npm run test`. 7 | 8 | ## Build in VS Code with docker (including with Github Codespaces) 9 | 10 | We have a devcontainer setup so you can run your development environment in Docker or in Github Codespaces. If locally, you will be asked to restart in remote container. To do this you'll need a local docker. Otherwise you can just run the codespace directly from Github. 11 | 12 | In either case, you should have everything required to build and test the project. 13 | 14 | ## Build Locally 15 | 16 | 1. Downgrade to node V6. 17 | 1. Ensure you have Python installed and in the path (e.g. `winget install python`). 18 | 1. Ensure you have C++ tools installed. See here https://github.com/nodejs/node-gyp#on-windows. 19 | 1. Navigate to the root folder. 20 | 1. If you haven't already, setup a https://marketplace.visualstudio.com/manage account and publisher following [these](https://learn.microsoft.com/en-us/azure/devops/extend/publish/overview?toc=%2Fazure%2Fdevops%2Fmarketplace-extensibility%2Ftoc.json&view=azure-devops#create-a-publisher) steps. 21 | 1. Create a file called `self.json` inside the root folder. The file contents should look like the following, but replace the `publisher` field with the publisher you setup earlier. 22 | ```json 23 | { 24 | "name": "Terraform CLI (Dev - Individual)", 25 | "public": false, 26 | "publisher": "" 27 | } 28 | ``` 29 | 7. Run `npm run package:self`. 30 | 1. This will generate a `.vsix` file prefixed with your published name. 31 | 1. Navigate to your publisher portal: https://marketplace.visualstudio.com/manage/publishers 32 | 1. Choose your publisher and select `New extension` and choose `Azure DevOps`. 33 | 1. You'll be prompted to drag and drop your `.vsix` file, do that and wait for it to be verified. Ensure you choose that your extension will be Private. 34 | 1. Click on the three dots `...` next to the extension name and choose `Share/Unshare`. 35 | 1. Click `+ Organization` and enter the name of your Azure DevOps org. 36 | 1. Now navigate to your Azure DevOps org and install the extension as you would any other. 37 | 1. You are now ready to use the extension and test it. -------------------------------------------------------------------------------- /GitVersion.yml: -------------------------------------------------------------------------------- 1 | next-version: 0.6.22 2 | mode: ContinuousDelivery 3 | branches: 4 | pull-request: 5 | tag: pr 6 | ignore: 7 | sha: [] 8 | merge-message-formats: {} 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Charles Zipp 4 | Copyright (c) 2023 Jason Johnson 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## NOTICE: PROJECT TRANSITIONED TO NEW OWNER 2 | 3 | [@jason-johnson](https://github.com/jason-johnson) has taken over ownership of this extension. The previous publisher will no longer be creating updates for this extension so, going forward, please use the new publisher as described in this updated documentation. 4 | 5 | # Azure Pipelines Extension for Terraform 6 | 7 | ![Build Status](https://dev.azure.com/azure-pipelines-terraform-rc/azure-pipelines-terraform-rc/_apis/build/status/jason-johnson.azure-pipelines-tasks-terraform?branchName=main) 8 | ![Visual Studio Marketplace Installs - Azure DevOps Extension](https://img.shields.io/visual-studio-marketplace/azure-devops/installs/total/JasonBJohnson.azure-pipelines-tasks-terraform?label=marketplace%20installs) 9 | 10 | This contains tasks for installing and executing Terraform commands from Azure Pipelines. These extensions are intended to work on any build agent. They are also intended to provide a guided abstraction to deploying infrastructure with Terraform from Azure Pipelines. 11 | 12 | The tasks contained within this extension are: 13 | 14 | - [Terraform Installer](/tasks/terraform-installer/README.md) 15 | - [Terraform CLI](/tasks/terraform-cli/README.md) 16 | 17 | This extension also contains views for the pipeline summary to help inspect actions performed by terraform. 18 | 19 | The views contained within this extension are: 20 | 21 | - [Terraform Plan](/views/terraform-plan/README.md) 22 | 23 | ## Telemetry Collection 24 | 25 | The software may collect information about you and your use of the software and send to the repository owner. The repository owner may use this information to provide services and improve our products and services. You may turn off the telemetry as described below. 26 | 27 | ### Disabling Telemetry Collection 28 | 29 | Telemetry collection can be disabled by setting the `allowTelemetryCollection` property to `false`. 30 | 31 | From classic pipeline editor, uncheck the `Allow Telemetry Collection` checkbox to disable 32 | telemetry collection. 33 | 34 | ### Preferred Languages 35 | 36 | We prefer all communications to be in English. 37 | -------------------------------------------------------------------------------- /_test-agent/temp/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore all files within this directory 2 | * 3 | !.gitignore -------------------------------------------------------------------------------- /_test-agent/tools/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore all files within this directory 2 | * 3 | !.gitignore -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "azure-pipelines-tasks-terraform", 3 | "version": "1.0.0", 4 | "description": "This contains the Azure Pipelines tasks for installing and executing terraform commands.", 5 | "scripts": { 6 | "pack-cli": "cd ./tasks/terraform-cli && npm install --include:dev && npm run build && npm run pack && sh ./../../scripts/set-version.sh", 7 | "pack-inst": "cd ./tasks/terraform-installer && npm install --include:dev && npm run build && npm run pack && sh ./../../scripts/set-version.sh", 8 | "pack-views": "cd ./views/terraform-plan && npm install --include:dev && npm run build && npm run pack", 9 | "publish:self": "tfx extension create --manifest-globs vss-extension.json vss-extension-rc.json --overrides-file ./self.json", 10 | "package:self": "npm install --include:dev && npm run pack-views && npm run pack-cli && npm run pack-inst && npm run publish:self" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/jason-johnson/azure-pipelines-tasks-terraform.git" 15 | }, 16 | "author": "Charles Zipp", 17 | "license": "MIT", 18 | "bugs": { 19 | "url": "https://github.com/jason-johnson/azure-pipelines-tasks-terraform/issues" 20 | }, 21 | "homepage": "https://github.com/jason-johnson/azure-pipelines-tasks-terraform#readme", 22 | "devDependencies": { 23 | "sass": "^1.80.7", 24 | "tfx-cli": "^0.17.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /pipelines/publish/poll_task_install.sh: -------------------------------------------------------------------------------- 1 | #/bin/bash 2 | set -e 3 | 4 | # polls until the provided task id is available in the target org/service-url. 5 | 6 | task_id= 7 | token= 8 | service_url= 9 | attempts=0 10 | max=25 11 | 12 | while getopts t:s:c: flag; do 13 | case "${flag}" in 14 | t) token="${OPTARG}";; 15 | s) service_url="${OPTARG}";; 16 | c) task_id="${OPTARG}";; 17 | esac 18 | done 19 | until $(tfx build tasks list --service-url $service_url -t $token --no-color --json | jq -r --arg t "$task_id" '.[] | select(.id == $t) | .id' | grep -q "$task_id"); 20 | do 21 | if [ $attempts -gt $max ] 22 | then 23 | echo "wait limit reached! exiting..." 24 | exit 0; 25 | else 26 | echo "waiting for task to become available..." 27 | sleep $(( attempts++ )); 28 | fi 29 | done 30 | 31 | echo "task is available!" -------------------------------------------------------------------------------- /pipelines/templates/build_npm.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | - name: workingDir 3 | type: string 4 | - name: runTests 5 | type: boolean 6 | default: "false" 7 | - name: artifact 8 | type: string 9 | 10 | steps: 11 | - task: Npm@1 12 | displayName: version 13 | inputs: 14 | workingDir: ${{ parameters.workingDir }} 15 | command: custom 16 | customCommand: version $(GitVersion.SemVer) --no-git-tag-version --allow-same-version 17 | - task: Npm@1 18 | displayName: install 19 | inputs: 20 | workingDir: ${{ parameters.workingDir }} 21 | command: ci 22 | - task: Npm@1 23 | displayName: build 24 | inputs: 25 | workingDir: ${{ parameters.workingDir }} 26 | command: custom 27 | customCommand: run build 28 | - ${{ if eq(parameters.runTests, true) }}: 29 | - template: test_npm.yml 30 | parameters: 31 | workingDir: ${{ parameters.workingDir }} 32 | - task: qetza.replacetokens.replacetokens-task.replacetokens@6 33 | displayName: version tasks 34 | inputs: 35 | targetFiles: ${{ parameters.workingDir }}/task.json 36 | escape: off 37 | - task: Npm@1 38 | displayName: pack 39 | inputs: 40 | workingDir: ${{ parameters.workingDir }} 41 | command: custom 42 | customCommand: run pack 43 | - task: CopyFiles@2 44 | displayName: stage artifacts 45 | inputs: 46 | sourceFolder: ${{ parameters.workingDir }}/.dist/ 47 | contents: | 48 | **/* 49 | targetFolder: $(Build.ArtifactStagingDirectory) 50 | - publish: '$(Build.ArtifactStagingDirectory)' 51 | artifact: ${{ parameters.artifact }} 52 | displayName: publish artifacts -------------------------------------------------------------------------------- /pipelines/templates/build_tasks.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | - name: instance 3 | type: string 4 | - name: majorNumber 5 | type: string 6 | - name: checkout 7 | default: self 8 | 9 | jobs: 10 | - job: terraform_cli_${{ parameters.instance }} 11 | variables: 12 | majorNumber: ${{ parameters.majorNumber }} 13 | steps: 14 | - checkout: ${{ parameters.checkout }} 15 | - template: build_npm.yml 16 | parameters: 17 | workingDir: $(System.DefaultWorkingDirectory)/tasks/terraform-cli 18 | artifact: terraform_cli_${{ parameters.instance }} 19 | runTests: true 20 | 21 | - job: terraform_installer_${{ parameters.instance }} 22 | variables: 23 | majorNumber: ${{ parameters.majorNumber }} 24 | steps: 25 | - checkout: ${{ parameters.checkout }} 26 | - template: build_npm.yml 27 | parameters: 28 | workingDir: $(System.DefaultWorkingDirectory)/tasks/terraform-installer 29 | artifact: terraform_installer_${{ parameters.instance }} 30 | 31 | 32 | -------------------------------------------------------------------------------- /pipelines/templates/download_artifacts.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | - name: tags 3 | type: string 4 | - name: experimentalVersion 5 | type: string 6 | 7 | 8 | steps: 9 | - task: DownloadPipelineArtifact@2 10 | displayName: download terraform extension 11 | inputs: 12 | artifact: terraform_extension 13 | path: $(System.DefaultWorkingDirectory) 14 | - task: DownloadPipelineArtifact@2 15 | displayName: download terraform_installer@2 16 | inputs: 17 | artifact: terraform_installer_main 18 | path: $(System.DefaultWorkingDirectory)/tasks/terraform-installer/v${{ parameters.experimentalVersion }} 19 | - task: DownloadPipelineArtifact@2 20 | displayName: download terraform_cli@2 21 | inputs: 22 | artifact: terraform_cli_main 23 | path: $(System.DefaultWorkingDirectory)/tasks/terraform-cli/v${{ parameters.experimentalVersion }} 24 | - task: DownloadPipelineArtifact@2 25 | displayName: download views terraform plan 26 | inputs: 27 | artifact: terraform_plan 28 | path: $(System.DefaultWorkingDirectory)/views/terraform-plan 29 | - ${{ each tag in split(parameters.tags, ',')}}: 30 | - task: DownloadPipelineArtifact@2 31 | displayName: download terraform_installer@${{ split(tag, '.')[0] }} 32 | inputs: 33 | artifact: terraform_installer_${{ replace(tag, '.', '_') }} 34 | path: $(System.DefaultWorkingDirectory)/tasks/terraform-installer/v${{ split(tag, '.')[0] }} 35 | - task: DownloadPipelineArtifact@2 36 | displayName: download terraform_cli@${{ split(tag, '.')[0] }} 37 | inputs: 38 | artifact: terraform_cli_${{ replace(tag, '.', '_') }} 39 | path: $(System.DefaultWorkingDirectory)/tasks/terraform-cli/v${{ split(tag, '.')[0] }} -------------------------------------------------------------------------------- /pipelines/templates/integration_tests.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | - name: scenarios 3 | type: object 4 | default: [] 5 | - name: taskVersions 6 | type: object 7 | default: [1,2] 8 | 9 | stages: 10 | - ${{ each taskVersion in parameters.taskVersions }}: 11 | - stage: test_${{ taskVersion }} 12 | variables: 13 | - group: env_rc 14 | - name: backendAzureRmResourceGroupName 15 | value: rg-trfrm-rc-chn-jbj 16 | - name: backendAzureRmResourceGroupLocation 17 | value: switzerlandnorth 18 | - name: backendAzureRmStorageAccountName 19 | value: sttrfrmrcchnjbj 20 | - name: backendAzureRmStorageAccountSku 21 | value: Standard_RAGRS 22 | - name: backendAzureRmContainerName 23 | value: azure-pipelines-terraform 24 | jobs: 25 | - ${{ each scenario in parameters.scenarios }}: 26 | - template: ${{ scenario }} 27 | parameters: 28 | taskVersion: ${{ taskVersion }} -------------------------------------------------------------------------------- /pipelines/templates/test/aws_self_configured.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | - name: taskVersion 3 | default: 2 4 | 5 | jobs: 6 | - job: aws_self_configured_${{ parameters.taskVersion }} 7 | variables: 8 | test_templates_dir: $(terraform_templates_dir)/aws 9 | steps: 10 | - task: JasonBJohnson.azure-pipelines-tasks-terraform-rc.azure-pipelines-tasks-terraform-installer.TerraformInstaller@${{ parameters.taskVersion }} 11 | displayName: install terraform 12 | inputs: 13 | terraformVersion: $(terraformVersion) 14 | - task: TerraformCLI@${{ parameters.taskVersion }} 15 | displayName: 'terraform init' 16 | inputs: 17 | command: init 18 | workingDirectory: $(test_templates_dir) 19 | backendType: self-configured 20 | commandOptions: -backend-config=bucket=s3-trfrm-rc-eus-czp -backend-config=key=azure-pipelines-terraform/aws-self-config 21 | # The secure env file contains the following vars 22 | # AWS_DEFAULT_REGION, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY 23 | # see also https://www.terraform.io/docs/language/settings/backends/s3.html#configuration 24 | secureVarsFile: self-configured-aws.env 25 | - task: TerraformCLI@${{ parameters.taskVersion }} 26 | displayName: 'terraform validate' 27 | inputs: 28 | workingDirectory: $(test_templates_dir) 29 | - task: TerraformCLI@${{ parameters.taskVersion }} 30 | displayName: 'terraform plan' 31 | inputs: 32 | command: plan 33 | workingDirectory: $(test_templates_dir) 34 | secureVarsFile: self-configured-aws.env 35 | commandOptions: '-out=$(System.DefaultWorkingDirectory)/terraform.tfplan' 36 | - task: TerraformCLI@${{ parameters.taskVersion }} 37 | displayName: 'terraform apply' 38 | inputs: 39 | command: apply 40 | workingDirectory: $(test_templates_dir) 41 | secureVarsFile: self-configured-aws.env 42 | commandOptions: '$(System.DefaultWorkingDirectory)/terraform.tfplan' 43 | - task: TerraformCLI@${{ parameters.taskVersion }} 44 | displayName: 'terraform destroy' 45 | inputs: 46 | command: destroy 47 | workingDirectory: $(test_templates_dir) 48 | secureVarsFile: self-configured-aws.env -------------------------------------------------------------------------------- /pipelines/templates/test/aws_service_connection.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | - name: taskVersion 3 | default: 2 4 | 5 | jobs: 6 | - job: aws_service_connection_${{ parameters.taskVersion }} 7 | variables: 8 | test_templates_dir: $(terraform_templates_dir)/aws 9 | steps: 10 | - task: JasonBJohnson.azure-pipelines-tasks-terraform-rc.azure-pipelines-tasks-terraform-installer.TerraformInstaller@${{ parameters.taskVersion }} 11 | displayName: install terraform 12 | inputs: 13 | terraformVersion: $(terraformVersion) 14 | - task: TerraformCLI@${{ parameters.taskVersion }} 15 | displayName: 'terraform init' 16 | inputs: 17 | command: init 18 | workingDirectory: $(test_templates_dir) 19 | backendType: aws 20 | backendServiceAws: env_test_aws 21 | backendAwsRegion: us-east-1 22 | backendAwsBucket: s3-trfrm-rc-eus-czp 23 | backendAwsKey: 'azure-pipelines-terraform/aws-service-connection' 24 | - task: TerraformCLI@${{ parameters.taskVersion }} 25 | displayName: 'terraform validate' 26 | inputs: 27 | workingDirectory: $(test_templates_dir) 28 | - task: TerraformCLI@${{ parameters.taskVersion }} 29 | displayName: 'terraform plan' 30 | inputs: 31 | command: plan 32 | workingDirectory: $(test_templates_dir) 33 | providerServiceAws: env_test_aws 34 | providerAwsRegion: us-east-1 35 | commandOptions: '-out=$(System.DefaultWorkingDirectory)/terraform.tfplan' 36 | - task: TerraformCLI@${{ parameters.taskVersion }} 37 | displayName: 'terraform apply' 38 | inputs: 39 | command: apply 40 | workingDirectory: $(test_templates_dir) 41 | providerServiceAws: env_test_aws 42 | providerAwsRegion: us-east-1 43 | commandOptions: '$(System.DefaultWorkingDirectory)/terraform.tfplan' 44 | - task: TerraformCLI@${{ parameters.taskVersion }} 45 | displayName: 'terraform destroy' 46 | inputs: 47 | command: destroy 48 | workingDirectory: $(test_templates_dir) 49 | providerServiceAws: env_test_aws 50 | providerAwsRegion: us-east-1 -------------------------------------------------------------------------------- /pipelines/templates/test/azurerm_mgmt_group_service_connection.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | - name: taskVersion 3 | default: 2 4 | 5 | jobs: 6 | - job: azurerm_mgmt_group_service_connection_${{ parameters.taskVersion }} 7 | steps: 8 | - task: JasonBJohnson.azure-pipelines-tasks-terraform-rc.azure-pipelines-tasks-terraform-installer.TerraformInstaller@${{ parameters.taskVersion }} 9 | displayName: install terraform 10 | inputs: 11 | terraformVersion: $(terraformVersion) 12 | - task: TerraformCLI@${{ parameters.taskVersion }} 13 | displayName: 'check version' 14 | inputs: 15 | command: version 16 | workingDirectory: $(terraform_templates_dir) 17 | - task: TerraformCLI@${{ parameters.taskVersion }} 18 | displayName: 'terraform init' 19 | inputs: 20 | command: init 21 | workingDirectory: $(terraform_templates_dir) 22 | backendType: azurerm 23 | backendServiceArm: 'env_test_mgmt' 24 | backendAzureRmSubscriptionId: $(azure_subscription_id) 25 | backendAzureRmResourceGroupName: $(backendAzureRmResourceGroupName) 26 | backendAzureRmResourceGroupLocation: $(backendAzureRmResourceGroupLocation) 27 | backendAzureRmStorageAccountName: $(backendAzureRmStorageAccountName) 28 | backendAzureRmStorageAccountSku: $(backendAzureRmStorageAccountSku) 29 | backendAzureRmContainerName: $(backendAzureRmContainerName) 30 | backendAzureRmKey: azurerm_mgmt_group_service_connection.tfstate 31 | - task: TerraformCLI@${{ parameters.taskVersion }} 32 | displayName: 'terraform plan' 33 | inputs: 34 | command: plan 35 | workingDirectory: $(terraform_templates_dir) 36 | environmentServiceName: 'env_test_mgmt' 37 | providerAzureRmSubscriptionId: $(azure_subscription_id) 38 | secureVarsFile: azurerm-mgmt-group-scope.vars 39 | commandOptions: '-out=$(System.DefaultWorkingDirectory)/terraform.tfplan -detailed-exitcode' 40 | - task: TerraformCLI@${{ parameters.taskVersion }} 41 | displayName: 'terraform apply' 42 | inputs: 43 | command: apply 44 | workingDirectory: $(terraform_templates_dir) 45 | environmentServiceName: 'env_test_mgmt' 46 | providerAzureRmSubscriptionId: $(azure_subscription_id) 47 | commandOptions: '$(System.DefaultWorkingDirectory)/terraform.tfplan' 48 | - task: TerraformCLI@${{ parameters.taskVersion }} 49 | displayName: 'terraform destroy' 50 | inputs: 51 | command: destroy 52 | workingDirectory: $(terraform_templates_dir) 53 | environmentServiceName: 'env_test_mgmt' 54 | providerAzureRmSubscriptionId: $(azure_subscription_id) 55 | secureVarsFile: azurerm-mgmt-group-scope.vars -------------------------------------------------------------------------------- /pipelines/templates/test/gcp_credential_file.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | - name: taskVersion 3 | default: 2 4 | 5 | jobs: 6 | - job: gcp_credential_file_${{ parameters.taskVersion }} 7 | variables: 8 | test_templates_dir: $(terraform_templates_dir)/gcp 9 | steps: 10 | - task: JasonBJohnson.azure-pipelines-tasks-terraform-rc.azure-pipelines-tasks-terraform-installer.TerraformInstaller@${{ parameters.taskVersion }} 11 | displayName: install terraform 12 | inputs: 13 | terraformVersion: $(terraformVersion) 14 | - task: TerraformCLI@${{ parameters.taskVersion }} 15 | displayName: 'terraform init' 16 | inputs: 17 | command: init 18 | workingDirectory: $(test_templates_dir) 19 | backendType: gcs 20 | backendGcsCredentials: gcp-service-account-key.json 21 | backendGcsBucket: gcs-trfrm-rc-eus-czp 22 | backendGcsPrefix: 'azure-pipelines-terraform/gcp_credential_file' 23 | - task: TerraformCLI@${{ parameters.taskVersion }} 24 | displayName: 'terraform validate' 25 | inputs: 26 | workingDirectory: $(test_templates_dir) 27 | - task: TerraformCLI@${{ parameters.taskVersion }} 28 | displayName: 'terraform plan' 29 | inputs: 30 | command: plan 31 | workingDirectory: $(test_templates_dir) 32 | providerGoogleCredentials: gcp-service-account-key.json 33 | providerGoogleProject: gcs-trfrm-rc-eus-czp 34 | providerGoogleRegion: 'us-east-1' 35 | commandOptions: '-out=$(System.DefaultWorkingDirectory)/terraform.tfplan' 36 | - task: TerraformCLI@${{ parameters.taskVersion }} 37 | displayName: 'terraform apply' 38 | inputs: 39 | command: apply 40 | workingDirectory: $(test_templates_dir) 41 | providerGoogleCredentials: gcp-service-account-key.json 42 | providerGoogleProject: gcs-trfrm-rc-eus-czp 43 | providerGoogleRegion: 'us-east-1' 44 | commandOptions: '$(System.DefaultWorkingDirectory)/terraform.tfplan' 45 | - task: TerraformCLI@${{ parameters.taskVersion }} 46 | displayName: 'terraform destroy' 47 | inputs: 48 | command: destroy 49 | workingDirectory: $(test_templates_dir) 50 | providerGoogleCredentials: gcp-service-account-key.json 51 | providerGoogleProject: gcs-trfrm-rc-eus-czp 52 | providerGoogleRegion: 'us-east-1' -------------------------------------------------------------------------------- /pipelines/templates/test/init_with_self_configured_backend.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | - name: taskVersion 3 | default: 2 4 | 5 | jobs: 6 | - job: init_with_self_configured_backend_${{ parameters.taskVersion }} 7 | steps: 8 | - task: JasonBJohnson.azure-pipelines-tasks-terraform-rc.azure-pipelines-tasks-terraform-installer.TerraformInstaller@${{ parameters.taskVersion }} 9 | displayName: install terraform 10 | inputs: 11 | terraformVersion: $(terraformVersion) 12 | - task: DownloadSecureFile@1 13 | name: backend_config_file 14 | displayName: 'download backend config file' 15 | inputs: 16 | secureFile: 'backend_rc.vars' 17 | - task: TerraformCLI@${{ parameters.taskVersion }} 18 | displayName: 'terraform init' 19 | inputs: 20 | command: init 21 | workingDirectory: $(terraform_templates_dir) 22 | backendType: self-configured 23 | commandOptions: '-backend-config="$(backend_config_file.secureFilePath)"' -------------------------------------------------------------------------------- /pipelines/templates/test/init_with_version_11.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | - name: taskVersion 3 | default: 2 4 | 5 | jobs: 6 | - job: init_with_version_11_${{ parameters.taskVersion }} 7 | variables: 8 | test_templates_dir: $(terraform_templates_dir)/version11 9 | steps: 10 | - task: JasonBJohnson.azure-pipelines-tasks-terraform-rc.azure-pipelines-tasks-terraform-installer.TerraformInstaller@${{ parameters.taskVersion }} 11 | displayName: install terraform 12 | inputs: 13 | terraformVersion: 0.11.14 14 | - task: TerraformCLI@${{ parameters.taskVersion }} 15 | displayName: 'terraform init' 16 | inputs: 17 | command: init 18 | workingDirectory: $(test_templates_dir) 19 | backendType: azurerm 20 | backendServiceArm: 'env_test' 21 | backendAzureRmResourceGroupName: $(backendAzureRmResourceGroupName) 22 | backendAzureRmResourceGroupLocation: $(backendAzureRmResourceGroupLocation) 23 | backendAzureRmStorageAccountName: $(backendAzureRmStorageAccountName) 24 | backendAzureRmStorageAccountSku: $(backendAzureRmStorageAccountSku) 25 | backendAzureRmContainerName: $(backendAzureRmContainerName) 26 | backendAzureRmKey: init_with_version_11.tfstate -------------------------------------------------------------------------------- /pipelines/templates/test/init_without_ensure_backend.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | - name: taskVersion 3 | default: 2 4 | 5 | jobs: 6 | - job: init_without_ensure_backend_${{ parameters.taskVersion }} 7 | steps: 8 | - task: JasonBJohnson.azure-pipelines-tasks-terraform-rc.azure-pipelines-tasks-terraform-installer.TerraformInstaller@${{ parameters.taskVersion }} 9 | displayName: install terraform 10 | inputs: 11 | terraformVersion: $(terraformVersion) 12 | - task: TerraformCLI@${{ parameters.taskVersion }} 13 | displayName: 'terraform init' 14 | inputs: 15 | command: init 16 | workingDirectory: $(terraform_templates_dir) 17 | backendType: azurerm 18 | backendServiceArm: 'env_test' 19 | backendAzureRmResourceGroupName: $(backendAzureRmResourceGroupName) 20 | backendAzureRmStorageAccountName: $(backendAzureRmStorageAccountName) 21 | backendAzureRmContainerName: $(backendAzureRmContainerName) 22 | backendAzureRmKey: init_without_ensure_backend.tfstate -------------------------------------------------------------------------------- /pipelines/templates/test/local_exec_az_cli.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | - name: taskVersion 3 | default: 2 4 | 5 | jobs: 6 | - job: local_exec_az_cli_${{ parameters.taskVersion }} 7 | variables: 8 | test_templates_dir: $(terraform_templates_dir)/local-exec-az-cli 9 | steps: 10 | - task: TerraformCLI@${{ parameters.taskVersion }} 11 | displayName: 'terraform init' 12 | inputs: 13 | command: init 14 | workingDirectory: $(test_templates_dir) 15 | backendType: azurerm 16 | backendServiceArm: 'env_test' 17 | backendAzureRmResourceGroupName: $(backendAzureRmResourceGroupName) 18 | backendAzureRmResourceGroupLocation: $(backendAzureRmResourceGroupLocation) 19 | backendAzureRmStorageAccountName: $(backendAzureRmStorageAccountName) 20 | backendAzureRmStorageAccountSku: $(backendAzureRmStorageAccountSku) 21 | backendAzureRmContainerName: $(backendAzureRmContainerName) 22 | backendAzureRmKey: local_exec_az_cli.tfstate 23 | - task: TerraformCLI@${{ parameters.taskVersion }} 24 | displayName: 'terraform validate' 25 | inputs: 26 | workingDirectory: $(test_templates_dir) 27 | - task: TerraformCLI@${{ parameters.taskVersion }} 28 | displayName: 'terraform plan' 29 | inputs: 30 | command: plan 31 | workingDirectory: $(test_templates_dir) 32 | environmentServiceName: 'env_test' 33 | secureVarsFile: $(tf_variables_secure_file) 34 | commandOptions: '-out=$(System.DefaultWorkingDirectory)/terraform.tfplan' 35 | runAzLogin: true 36 | - task: TerraformCLI@${{ parameters.taskVersion }} 37 | displayName: 'terraform apply' 38 | inputs: 39 | command: apply 40 | workingDirectory: $(test_templates_dir) 41 | environmentServiceName: 'env_test' 42 | commandOptions: '$(System.DefaultWorkingDirectory)/terraform.tfplan' 43 | runAzLogin: true 44 | - task: TerraformCLI@${{ parameters.taskVersion }} 45 | displayName: 'terraform destroy' 46 | condition: always() 47 | inputs: 48 | command: destroy 49 | workingDirectory: $(test_templates_dir) 50 | environmentServiceName: 'env_test' 51 | secureVarsFile: $(tf_variables_secure_file) 52 | runAzLogin: true -------------------------------------------------------------------------------- /pipelines/templates/test/plan_with_command_options_var_file.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | - name: taskVersion 3 | default: 2 4 | 5 | jobs: 6 | - job: plan_with_comand_options_var_file_${{ parameters.taskVersion }} 7 | steps: 8 | - task: JasonBJohnson.azure-pipelines-tasks-terraform-rc.azure-pipelines-tasks-terraform-installer.TerraformInstaller@${{ parameters.taskVersion }} 9 | displayName: install terraform 10 | inputs: 11 | terraformVersion: $(terraformVersion) 12 | - task: TerraformCLI@${{ parameters.taskVersion }} 13 | displayName: 'terraform init' 14 | inputs: 15 | command: init 16 | workingDirectory: $(terraform_templates_dir) 17 | backendType: azurerm 18 | backendServiceArm: 'env_test' 19 | backendAzureRmResourceGroupName: $(backendAzureRmResourceGroupName) 20 | backendAzureRmResourceGroupLocation: $(backendAzureRmResourceGroupLocation) 21 | backendAzureRmStorageAccountName: $(backendAzureRmStorageAccountName) 22 | backendAzureRmStorageAccountSku: $(backendAzureRmStorageAccountSku) 23 | backendAzureRmContainerName: $(backendAzureRmContainerName) 24 | backendAzureRmKey: plan_with_comand_options_var_file.tfstate 25 | - task: TerraformCLI@${{ parameters.taskVersion }} 26 | displayName: 'terraform plan' 27 | inputs: 28 | command: plan 29 | workingDirectory: $(terraform_templates_dir) 30 | environmentServiceName: 'env_test' 31 | commandOptions: '-var-file="./default.vars" -out=$(System.DefaultWorkingDirectory)/terraform.tfplan -detailed-exitcode' -------------------------------------------------------------------------------- /pipelines/templates/test/publish_plan_results.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | - name: taskVersion 3 | default: 2 4 | 5 | jobs: 6 | - job: publish_plan_results_${{ parameters.taskVersion }} 7 | steps: 8 | - task: JasonBJohnson.azure-pipelines-tasks-terraform-rc.azure-pipelines-tasks-terraform-installer.TerraformInstaller@${{ parameters.taskVersion }} 9 | displayName: install terraform 10 | inputs: 11 | terraformVersion: $(terraformVersion) 12 | - task: TerraformCLI@${{ parameters.taskVersion }} 13 | displayName: 'terraform init' 14 | inputs: 15 | command: init 16 | workingDirectory: $(terraform_templates_dir) 17 | backendType: azurerm 18 | backendServiceArm: 'env_test' 19 | backendAzureRmResourceGroupName: $(backendAzureRmResourceGroupName) 20 | backendAzureRmResourceGroupLocation: $(backendAzureRmResourceGroupLocation) 21 | backendAzureRmStorageAccountName: $(backendAzureRmStorageAccountName) 22 | backendAzureRmStorageAccountSku: $(backendAzureRmStorageAccountSku) 23 | backendAzureRmContainerName: $(backendAzureRmContainerName) 24 | backendAzureRmKey: publish_plan_results.tfstate 25 | - task: TerraformCLI@${{ parameters.taskVersion }} 26 | displayName: 'terraform plan' 27 | inputs: 28 | command: plan 29 | workingDirectory: $(terraform_templates_dir) 30 | environmentServiceName: 'env_test' 31 | secureVarsFile: $(tf_variables_secure_file) 32 | publishPlanResults: publish_plan_results_rc 33 | commandOptions: '-out=$(System.DefaultWorkingDirectory)/terraform.tfplan -detailed-exitcode' -------------------------------------------------------------------------------- /pipelines/templates/test/switch_workspaces.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | - name: taskVersion 3 | default: 2 4 | 5 | jobs: 6 | - job: switch_workspaces_${{ parameters.taskVersion }} 7 | steps: 8 | - task: JasonBJohnson.azure-pipelines-tasks-terraform-rc.azure-pipelines-tasks-terraform-installer.TerraformInstaller@${{ parameters.taskVersion }} 9 | displayName: install terraform 10 | inputs: 11 | terraformVersion: $(terraformVersion) 12 | - task: TerraformCLI@${{ parameters.taskVersion }} 13 | displayName: 'terraform init' 14 | inputs: 15 | command: init 16 | workingDirectory: $(terraform_templates_dir) 17 | backendType: azurerm 18 | backendServiceArm: 'env_test' 19 | ensureBackend: true 20 | backendAzureRmResourceGroupName: $(backendAzureRmResourceGroupName) 21 | backendAzureRmResourceGroupLocation: $(backendAzureRmResourceGroupLocation) 22 | backendAzureRmStorageAccountName: $(backendAzureRmStorageAccountName) 23 | backendAzureRmStorageAccountSku: $(backendAzureRmStorageAccountSku) 24 | backendAzureRmContainerName: $(backendAzureRmContainerName) 25 | backendAzureRmKey: workspace_switch.tfstate 26 | - task: TerraformCLI@${{ parameters.taskVersion }} 27 | displayName: new workspace foo 28 | inputs: 29 | workingDirectory: $(terraform_templates_dir) 30 | command: workspace 31 | workspaceSubCommand: new 32 | workspaceName: foo 33 | - task: TerraformCLI@${{ parameters.taskVersion }} 34 | displayName: new workspace foo skip existing 35 | inputs: 36 | workingDirectory: $(terraform_templates_dir) 37 | command: workspace 38 | workspaceSubCommand: new 39 | workspaceName: foo 40 | skipExistingWorkspace: true 41 | - task: TerraformCLI@${{ parameters.taskVersion }} 42 | displayName: select workspace default 43 | inputs: 44 | workingDirectory: $(terraform_templates_dir) 45 | command: workspace 46 | workspaceSubCommand: select 47 | workspaceName: default 48 | - bash: | 49 | terraform workspace show 50 | displayName: show workspace 51 | workingDirectory: $(terraform_templates_dir) 52 | - bash: | 53 | terraform workspace delete foo 54 | displayName: delete new workspace 55 | workingDirectory: $(terraform_templates_dir) -------------------------------------------------------------------------------- /pipelines/templates/test_npm.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | - name: workingDir 3 | type: string 4 | 5 | steps: 6 | - task: Npm@1 7 | displayName: test - report coverage 8 | inputs: 9 | workingDir: ${{ parameters.workingDir }} 10 | command: custom 11 | customCommand: run test:coverage 12 | - task: PublishCodeCoverageResults@2 13 | displayName: test - publish coverage 14 | inputs: 15 | codeCoverageTool: cobertura 16 | summaryFileLocation: ${{ parameters.workingDir }}/.tests/coverage/cobertura-coverage.xml 17 | reportDirectory: ${{ parameters.workingDir }}/.tests/coverage 18 | - task: Npm@1 19 | displayName: test - report results 20 | inputs: 21 | workingDir: ${{ parameters.workingDir }} 22 | command: custom 23 | customCommand: run test:report 24 | - task: PublishTestResults@2 25 | displayName: test - publish results 26 | inputs: 27 | testResultsFormat: JUnit 28 | testResultsFiles: 'results.xml' 29 | searchFolder: ${{ parameters.workingDir }}/.tests -------------------------------------------------------------------------------- /screenshots/overview-tfcli-azure-sub.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jason-johnson/azure-pipelines-tasks-terraform/756f2ba62a644865933be62c2f5f94cdd36a9e75/screenshots/overview-tfcli-azure-sub.png -------------------------------------------------------------------------------- /screenshots/overview-tfcli-backend-azurerm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jason-johnson/azure-pipelines-tasks-terraform/756f2ba62a644865933be62c2f5f94cdd36a9e75/screenshots/overview-tfcli-backend-azurerm.png -------------------------------------------------------------------------------- /screenshots/overview-tfcli-ensure-backend.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jason-johnson/azure-pipelines-tasks-terraform/756f2ba62a644865933be62c2f5f94cdd36a9e75/screenshots/overview-tfcli-ensure-backend.png -------------------------------------------------------------------------------- /screenshots/overview-tfcli-secure-vars.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jason-johnson/azure-pipelines-tasks-terraform/756f2ba62a644865933be62c2f5f94cdd36a9e75/screenshots/overview-tfcli-secure-vars.jpg -------------------------------------------------------------------------------- /screenshots/overview-tfinstall-task-fields.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jason-johnson/azure-pipelines-tasks-terraform/756f2ba62a644865933be62c2f5f94cdd36a9e75/screenshots/overview-tfinstall-task-fields.png -------------------------------------------------------------------------------- /screenshots/overview-tfplan-view-no-plans.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jason-johnson/azure-pipelines-tasks-terraform/756f2ba62a644865933be62c2f5f94cdd36a9e75/screenshots/overview-tfplan-view-no-plans.jpg -------------------------------------------------------------------------------- /screenshots/overview-tfplan-view.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jason-johnson/azure-pipelines-tasks-terraform/756f2ba62a644865933be62c2f5f94cdd36a9e75/screenshots/overview-tfplan-view.jpg -------------------------------------------------------------------------------- /scripts/deploy.sh: -------------------------------------------------------------------------------- 1 | #/bin/bash 2 | set -xe 3 | 4 | env=alpha 5 | location=eastus 6 | location_suffix=eus 7 | org=czp 8 | 9 | while getopts e:l:o: flag; do 10 | case "${flag}" in 11 | e) env="${OPTARG}";; 12 | l) location="${OPTARG}";; 13 | o) org="${OPTARG}";; 14 | esac 15 | done 16 | 17 | case "${location}" in 18 | eastus) location_suffix=eus;; 19 | eastus2) location_suffix=eus2;; 20 | westus) location_suffix=wus;; 21 | westus2) location_suffix=wus2;; 22 | esac 23 | 24 | suffix=-trfrm-${env}-${location_suffix}-${org} 25 | suffix_no_hyphen=trfrm${env}${location_suffix}${org} 26 | suffix_no_location=-trfrm-${env}-${org} 27 | 28 | subscription=$(az account show --query id -o tsv) 29 | 30 | resource_group=rg${suffix} 31 | az group create -l $location -n $resource_group 32 | 33 | workspace=logs${suffix_no_location} 34 | workspace_id=/subscriptions/${subscription}/resourcegroups/${resource_group}/providers/microsoft.operationalinsights/workspaces/${workspace} 35 | az monitor log-analytics workspace create --resource-group $resource_group --workspace-name $workspace 36 | 37 | app_insights=ai${suffix} 38 | az monitor app-insights component create --app $app_insights --location $location --kind web -g $resource_group --workspace $workspace_id 39 | -------------------------------------------------------------------------------- /scripts/max_tags.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | newestTag=$(git tag --list --sort='-version:refname' | head -n1) 5 | newestMajor=${newestTag%%.*} 6 | 7 | tagsarray=( $newestTag ) 8 | 9 | for i in $(seq 0 $(($newestMajor-1))); do 10 | tag=$(git tag --list "${i}.*" --sort='-version:refname' | head -n1) 11 | tagsarray+=($tag) 12 | done 13 | 14 | tags_string="${tagsarray[*]}" 15 | echo "##vso[task.setvariable variable=tags;isoutput=true]${tags_string//${IFS:0:1}/,}" -------------------------------------------------------------------------------- /scripts/set-version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo "Updating Version to 1.0.3" 3 | SED="sed" 4 | 5 | if [[ "$OSTYPE" == "darwin"* ]]; then 6 | SED="gsed" 7 | fi 8 | ${SED} -i 's/#{GitVersion.Major}#/1/g' ./.dist/task.json 9 | ${SED} -i 's/#{GitVersion.Minor}#/0/g' ./.dist/task.json 10 | ${SED} -i 's/#{GitVersion.Patch}#/3/g' ./.dist/task.json 11 | 12 | -------------------------------------------------------------------------------- /tasks/terraform-cli/.nycrc.yml: -------------------------------------------------------------------------------- 1 | report-dir: ./.tests/coverage 2 | temp-dir: ./.tests/.nyc_output -------------------------------------------------------------------------------- /tasks/terraform-cli/cucumber.js: -------------------------------------------------------------------------------- 1 | let common = [ 2 | 'src/tests/features/**/*.feature', // Specify our feature files 3 | '--require-module ts-node/register', // Load TypeScript module 4 | '--require src/tests/steps/**/*.ts', // Load step definitions 5 | '--format progress-bar', // Load custom formatter 6 | '--format @cucumber/pretty-formatter', // Load custom formatter 7 | ].join(' '); 8 | 9 | let report = [ 10 | 'src/tests/features/**/*.feature', // Specify our feature files 11 | '--require-module ts-node/register', // Load TypeScript module 12 | '--require src/tests/steps/**/*.ts', // Load step definitions 13 | '--format=json' 14 | ].join(' '); 15 | 16 | module.exports = { 17 | default: common, 18 | report: report 19 | }; -------------------------------------------------------------------------------- /tasks/terraform-cli/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jason-johnson/azure-pipelines-tasks-terraform/756f2ba62a644865933be62c2f5f94cdd36a9e75/tasks/terraform-cli/icon.png -------------------------------------------------------------------------------- /tasks/terraform-cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "azure-pipelines-tasks-terraform-cli", 3 | "version": "1.0.0", 4 | "description": "Azure Pipelines task to execute terraform cli commands", 5 | "main": ".bin/index.js", 6 | "scripts": { 7 | "build": "tsc --build", 8 | "test": "cucumber-js -p default", 9 | "test:coverage": "nyc -r cobertura -r html \"cucumber-js\" \"-p\" \"default\"", 10 | "test:report": "mkdir -p .tests && cucumber-js -p report | grep -Ev '^#{2}vso|^\\[command]|^[a-zA-Z0-9]' | cucumber-junit > ./.tests/results.xml", 11 | "start": "ts-node -r dotenv/config ./src/index.ts", 12 | "pack": "copyfiles package.json task.json icon.png \".bin/*.js\" \".bin/**/*.js\" -e \".bin/tests/*\" .dist && cd .dist && npm install --only=prod" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/jason-johnson/azure-pipelines-tasks-terraform.git" 17 | }, 18 | "keywords": [ 19 | "terraform", 20 | "azure-devops", 21 | "azure-pipelines" 22 | ], 23 | "author": "Charles Zipp", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/jason-johnson/azure-pipelines-tasks-terraform/issues" 27 | }, 28 | "homepage": "https://github.com/jason-johnson/azure-pipelines-tasks-terraform#readme", 29 | "dependencies": { 30 | "applicationinsights": "^2.9.6", 31 | "azure-devops-node-api": "^14.1.0", 32 | "azure-pipelines-task-lib": "^4.17.3", 33 | "azure-pipelines-tasks-artifacts-common": "^2.247.0", 34 | "azure-pipelines-tasks-azure-arm-rest": "^3.248.1", 35 | "dotenv": "^16.4.5", 36 | "intercept-stdout": "^0.1.2", 37 | "mock-require": "^3.0.3", 38 | "q": "^2.0.3", 39 | "reflect-metadata": "^0.2.2", 40 | "uuid": "^11.0.3" 41 | }, 42 | "devDependencies": { 43 | "@cucumber/cucumber": "^10.9.0", 44 | "@cucumber/pretty-formatter": "^1.0.1", 45 | "@types/chai": "^5.0.1", 46 | "@types/chai-arrays": "^2.0.3", 47 | "@types/intercept-stdout": "^0.1.3", 48 | "@types/mock-require": "^3.0.0", 49 | "@types/node": "^22.10.0", 50 | "@types/q": "^1.5.8", 51 | "chai": "^4.5.0", 52 | "chai-arrays": "^2.2.0", 53 | "copyfiles": "^2.4.1", 54 | "cucumber-junit": "^1.7.1", 55 | "cucumber-tsflow": "^4.4.4", 56 | "nyc": "^17.1.0", 57 | "tfx-cli": "^0.17.0", 58 | "ts-node": "^10.9.2", 59 | "typescript": "^5.6.3" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /tasks/terraform-cli/src/backends/aws.ts: -------------------------------------------------------------------------------- 1 | import { ITerraformBackend, TerraformBackendInitResult } from "."; 2 | import { ITaskContext } from "../context"; 3 | 4 | export default class AwsBackend implements ITerraformBackend { 5 | async init(ctx: ITaskContext): Promise { 6 | let backendConfig: any = { 7 | bucket: ctx.backendAwsBucket, 8 | key: ctx.backendAwsKey, 9 | region: ctx.backendAwsRegion, 10 | access_key: ctx.backendServiceAwsAccessKey, 11 | secret_key: ctx.backendServiceAwsSecretKey 12 | } 13 | 14 | const result = { 15 | args: [] 16 | } 17 | 18 | for(var config in backendConfig){ 19 | if(backendConfig[config]){ 20 | result.args.push(`-backend-config=${config}=${backendConfig[config]}`); 21 | } 22 | } 23 | 24 | return Promise.resolve(result); 25 | } 26 | } -------------------------------------------------------------------------------- /tasks/terraform-cli/src/backends/gcs.ts: -------------------------------------------------------------------------------- 1 | import { ITerraformBackend, TerraformBackendInitResult } from "."; 2 | import { ITaskContext } from "../context"; 3 | import { ITaskAgent } from "../task-agent"; 4 | 5 | export default class GcsBackend implements ITerraformBackend { 6 | constructor(private readonly agent: ITaskAgent) { 7 | } 8 | 9 | async init(ctx: ITaskContext): Promise { 10 | const credentials = await this.agent.downloadSecureFile(ctx.backendGcsCredentials); 11 | 12 | let backendConfig: any = { 13 | bucket: ctx.backendGcsBucket, 14 | prefix: ctx.backendGcsPrefix, 15 | credentials 16 | }; 17 | 18 | const result = { 19 | args: [] 20 | } 21 | 22 | for(var config in backendConfig){ 23 | if(backendConfig[config]){ 24 | result.args.push(`-backend-config=${config}=${backendConfig[config]}`); 25 | } 26 | } 27 | 28 | return Promise.resolve(result); 29 | } 30 | } -------------------------------------------------------------------------------- /tasks/terraform-cli/src/backends/index.ts: -------------------------------------------------------------------------------- 1 | import { ITaskContext } from '../context'; 2 | 3 | export interface TerraformBackendInitResult{ 4 | args: string[]; 5 | } 6 | export interface ITerraformBackend{ 7 | init(ctx: ITaskContext): Promise; 8 | } 9 | export enum BackendTypes{ 10 | local = "local", 11 | azurerm = "azurerm", 12 | selfConfigured = "self-configured", 13 | aws = "aws", 14 | gcs = "gcs" 15 | } 16 | 17 | export { default as AzureRMBackend } from './azurerm'; -------------------------------------------------------------------------------- /tasks/terraform-cli/src/commands/az-account-set.ts: -------------------------------------------------------------------------------- 1 | import { IRunner } from "../runners"; 2 | import { ICommand, CommandResponse, CommandPipeline } from "."; 3 | import { RunWithAzCli } from "../runners/builders"; 4 | import { ITaskContext } from "../context"; 5 | 6 | export class AzAccountSet implements ICommand { 7 | constructor( 8 | private readonly runner: IRunner){ 9 | } 10 | 11 | async exec(ctx: ITaskContext): Promise{ 12 | const options = await new RunWithAzCli("set", ["account"]).build(); 13 | options.addArgs( 14 | "-s", (ctx.backendAzureRmSubscriptionId || ctx.backendServiceArmSubscriptionId) 15 | ); 16 | const result = await this.runner.exec(options); 17 | return result.toCommandResponse(); 18 | } 19 | } 20 | 21 | declare module "." { 22 | interface CommandPipeline { 23 | azAccountSet(this: CommandPipeline): CommandPipeline; 24 | } 25 | } 26 | 27 | CommandPipeline.prototype.azAccountSet = function(this: CommandPipeline): CommandPipeline { 28 | return this.pipe((runner: IRunner) => new AzAccountSet(runner)); 29 | } -------------------------------------------------------------------------------- /tasks/terraform-cli/src/commands/az-group-create.ts: -------------------------------------------------------------------------------- 1 | import { IRunner } from "../runners"; 2 | import { ICommand, CommandResponse, CommandPipeline } from "."; 3 | import { RunWithAzCli } from "../runners/builders"; 4 | import { ITaskContext } from "../context"; 5 | 6 | export class AzGroupCreate implements ICommand { 7 | constructor( 8 | private readonly runner: IRunner){ 9 | } 10 | 11 | async exec(ctx: ITaskContext): Promise{ 12 | const options = await new RunWithAzCli("create", ["group"]).build(); 13 | options.addArgs( 14 | "--name", ctx.backendAzureRmResourceGroupName, 15 | "--location", ctx.backendAzureRmResourceGroupLocation 16 | ); 17 | const result = await this.runner.exec(options); 18 | return result.toCommandResponse(); 19 | } 20 | } 21 | 22 | declare module "." { 23 | interface CommandPipeline { 24 | azGroupCreate(this: CommandPipeline): CommandPipeline; 25 | } 26 | } 27 | 28 | CommandPipeline.prototype.azGroupCreate = function(this: CommandPipeline): CommandPipeline { 29 | return this.pipe((runner: IRunner) => new AzGroupCreate(runner)); 30 | } -------------------------------------------------------------------------------- /tasks/terraform-cli/src/commands/az-storage-account-create.ts: -------------------------------------------------------------------------------- 1 | import { IRunner } from "../runners"; 2 | import { ICommand, CommandResponse, CommandPipeline, CommandStatus } from "."; 3 | import { RunWithAzCli } from "../runners/builders"; 4 | import { ITaskContext } from "../context"; 5 | 6 | export class AzStorageAccountCreate implements ICommand { 7 | constructor( 8 | private readonly runner: IRunner){ 9 | } 10 | 11 | async exec(ctx: ITaskContext): Promise{ 12 | const showOptions = await new RunWithAzCli("show", ["storage", "account"]).build(); 13 | showOptions.addArgs( 14 | "--name", ctx.backendAzureRmStorageAccountName, 15 | "--resource-group", ctx.backendAzureRmResourceGroupName 16 | ); 17 | const showResult = await this.runner.exec(showOptions); 18 | if(showResult.exitCode !== 0 && showResult.stderr){ 19 | const expectedError: string = `The Resource 'Microsoft.Storage/storageAccounts/${ctx.backendAzureRmStorageAccountName}' under resource group '${ctx.backendAzureRmResourceGroupName}' was not found`; 20 | if(showResult.stderr.includes(expectedError)){ 21 | console.log("az storage account create: storage account not found, creating..."); 22 | } 23 | else{ 24 | throw new Error(showResult.stderr); 25 | } 26 | } 27 | else{ 28 | console.log("az storage account create: storage account already exists"); 29 | return new CommandResponse(CommandStatus.Success, showResult.stdout, showResult.exitCode); 30 | } 31 | 32 | const createOptions = await new RunWithAzCli("create", ["storage", "account"]).build(); 33 | createOptions.addArgs( 34 | "--name", ctx.backendAzureRmStorageAccountName, 35 | "--resource-group", ctx.backendAzureRmResourceGroupName, 36 | "--sku", ctx.backendAzureRmStorageAccountSku, 37 | "--kind", "StorageV2", 38 | "--encryption-services", "blob", 39 | "--access-tier", "hot", 40 | "--allow-blob-public-access", "false", 41 | "--https-only", "true", 42 | "--min-tls-version", "TLS1_2", 43 | ); 44 | const result = await this.runner.exec(createOptions); 45 | return result.toCommandResponse(); 46 | } 47 | } 48 | 49 | declare module "." { 50 | interface CommandPipeline { 51 | azStorageAccountCreate(this: CommandPipeline): CommandPipeline; 52 | } 53 | } 54 | 55 | CommandPipeline.prototype.azStorageAccountCreate = function(this: CommandPipeline): CommandPipeline { 56 | return this.pipe((runner: IRunner) => new AzStorageAccountCreate(runner)); 57 | } 58 | -------------------------------------------------------------------------------- /tasks/terraform-cli/src/commands/az-storage-container-create.ts: -------------------------------------------------------------------------------- 1 | import { IRunner } from "../runners"; 2 | import { ICommand, CommandResponse, CommandPipeline } from "."; 3 | import { RunWithAzCli } from "../runners/builders"; 4 | import { ITaskContext } from "../context"; 5 | 6 | export class AzStorageContainerCreate implements ICommand { 7 | constructor( 8 | private readonly runner: IRunner){ 9 | } 10 | 11 | async exec(ctx: ITaskContext): Promise{ 12 | const options = await new RunWithAzCli("create", ["storage", "container"]).build(); 13 | options.addArgs( 14 | "--auth-mode", "login", 15 | "--name", ctx.backendAzureRmContainerName, 16 | "--account-name", ctx.backendAzureRmStorageAccountName 17 | ); 18 | const result = await this.runner.exec(options); 19 | return result.toCommandResponse(); 20 | } 21 | } 22 | 23 | declare module "." { 24 | interface CommandPipeline { 25 | azStorageContainerCreate(this: CommandPipeline): CommandPipeline; 26 | } 27 | } 28 | 29 | CommandPipeline.prototype.azStorageContainerCreate = function(this: CommandPipeline): CommandPipeline { 30 | return this.pipe((runner: IRunner) => new AzStorageContainerCreate(runner)); 31 | } -------------------------------------------------------------------------------- /tasks/terraform-cli/src/commands/tf-apply.ts: -------------------------------------------------------------------------------- 1 | import { CommandResponse, ICommand } from "."; 2 | import { ITaskContext } from "../context"; 3 | import { TerraformProviderContext } from "../providers"; 4 | import { IRunner } from "../runners"; 5 | import { RunWithTerraform } from "../runners/builders"; 6 | import { ITaskAgent } from "../task-agent"; 7 | 8 | export class TerraformApply implements ICommand { 9 | constructor( 10 | private readonly taskAgent: ITaskAgent, 11 | private readonly runner: IRunner, 12 | private readonly providers: TerraformProviderContext 13 | ) { 14 | } 15 | 16 | async exec(ctx: ITaskContext): Promise { 17 | await this.providers.init(); 18 | const options = await new RunWithTerraform(ctx) 19 | .withSecureVarFile(this.taskAgent, ctx.secureVarsFileId, ctx.secureVarsFileName) 20 | .withAutoApprove() 21 | .withCommandOptions(ctx.commandOptions) 22 | .build(); 23 | 24 | const result = await this.runner.exec(options); 25 | 26 | return result.toCommandResponse(); 27 | } 28 | } -------------------------------------------------------------------------------- /tasks/terraform-cli/src/commands/tf-destroy.ts: -------------------------------------------------------------------------------- 1 | import { CommandResponse, ICommand } from "."; 2 | import { ITaskContext } from "../context"; 3 | import { TerraformProviderContext } from "../providers"; 4 | import { IRunner } from "../runners"; 5 | import { RunWithTerraform } from "../runners/builders"; 6 | import { ITaskAgent } from "../task-agent"; 7 | 8 | export class TerraformDestroy implements ICommand { 9 | constructor( 10 | private readonly taskAgent: ITaskAgent, 11 | private readonly runner: IRunner, 12 | private readonly providers: TerraformProviderContext 13 | ) { 14 | } 15 | 16 | async exec(ctx: ITaskContext): Promise { 17 | await this.providers.init(); 18 | const options = await new RunWithTerraform(ctx) 19 | .withSecureVarFile(this.taskAgent, ctx.secureVarsFileId, ctx.secureVarsFileName) 20 | .withAutoApprove() 21 | .withCommandOptions(ctx.commandOptions) 22 | .build(); 23 | const result = await this.runner.exec(options); 24 | 25 | return result.toCommandResponse(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tasks/terraform-cli/src/commands/tf-fmt.ts: -------------------------------------------------------------------------------- 1 | import { CommandResponse, ICommand } from "."; 2 | import { ITaskContext } from "../context"; 3 | import { IRunner } from "../runners"; 4 | import { RunWithTerraform } from "../runners/builders"; 5 | 6 | export class TerraformFormat implements ICommand { 7 | constructor( 8 | private readonly runner: IRunner 9 | ) { 10 | } 11 | 12 | async exec(ctx: ITaskContext): Promise { 13 | const options = await new RunWithTerraform(ctx) 14 | .withOptions(ctx.commandOptions, [ 15 | "--check", 16 | "--diff", 17 | "--recursive" 18 | ]) 19 | .withCommandOptions(ctx.commandOptions) 20 | .build(); 21 | const result = await this.runner.exec(options); 22 | 23 | return result.toCommandResponse(); 24 | } 25 | } -------------------------------------------------------------------------------- /tasks/terraform-cli/src/commands/tf-force-unlock.ts: -------------------------------------------------------------------------------- 1 | import { CommandResponse, ICommand } from "."; 2 | import { ITaskContext } from "../context"; 3 | import { TerraformProviderContext } from "../providers"; 4 | import { IRunner } from "../runners"; 5 | import { RunWithTerraform } from "../runners/builders"; 6 | import { ITaskAgent } from "../task-agent"; 7 | 8 | export class TerraformForceUnlock implements ICommand { 9 | constructor( 10 | private readonly taskAgent: ITaskAgent, 11 | private readonly runner: IRunner, 12 | private readonly providers: TerraformProviderContext 13 | ) { 14 | } 15 | 16 | async exec(ctx: ITaskContext): Promise { 17 | await this.providers.init(); 18 | const options = await new RunWithTerraform(ctx, undefined, "force-unlock") 19 | .withSecureVarFile(this.taskAgent, ctx.secureVarsFileId, ctx.secureVarsFileName) 20 | .withForce() 21 | .withLockId(ctx.lockId) 22 | .build(); 23 | const result = await this.runner.exec(options); 24 | 25 | return result.toCommandResponse(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tasks/terraform-cli/src/commands/tf-import.ts: -------------------------------------------------------------------------------- 1 | import { CommandResponse, ICommand } from "."; 2 | import { ITaskContext } from "../context"; 3 | import { TerraformProviderContext } from "../providers"; 4 | import { IRunner } from "../runners"; 5 | import { RunWithTerraform } from "../runners/builders"; 6 | import { ITaskAgent } from "../task-agent"; 7 | 8 | export class TerraformImport implements ICommand { 9 | constructor( 10 | private readonly taskAgent: ITaskAgent, 11 | private readonly runner: IRunner, 12 | private readonly providers: TerraformProviderContext 13 | ) { 14 | } 15 | 16 | async exec(ctx: ITaskContext): Promise { 17 | await this.providers.init(); 18 | const options = await new RunWithTerraform(ctx) 19 | .withSecureVarFile(this.taskAgent, ctx.secureVarsFileId, ctx.secureVarsFileName) 20 | .withCommandOptions(ctx.commandOptions) 21 | .withResourceTarget(ctx.resourceAddress, ctx.resourceId) 22 | .build(); 23 | const result = await this.runner.exec(options); 24 | 25 | return result.toCommandResponse(); 26 | } 27 | } -------------------------------------------------------------------------------- /tasks/terraform-cli/src/commands/tf-init.ts: -------------------------------------------------------------------------------- 1 | import { ICommand, CommandResponse } from "."; 2 | import { IRunner } from "../runners"; 3 | import { AzureRMBackend, BackendTypes, ITerraformBackend } from "../backends"; 4 | import { RunWithTerraform } from "../runners/builders"; 5 | import { ITaskContext } from "../context"; 6 | import { ITaskAgent } from "../task-agent"; 7 | import AwsBackend from "../backends/aws"; 8 | import { ILogger } from "../logger"; 9 | import GcsBackend from "../backends/gcs"; 10 | 11 | export class TerraformInit implements ICommand { 12 | private readonly runner: IRunner; 13 | private readonly taskAgent: ITaskAgent; 14 | private readonly logger: ILogger; 15 | 16 | constructor(taskAgent: ITaskAgent, runner: IRunner, logger: ILogger){ 17 | this.taskAgent = taskAgent; 18 | this.runner = runner; 19 | this.logger = logger; 20 | } 21 | 22 | private getBackend(ctx: ITaskContext): ITerraformBackend | undefined{ 23 | let backend: ITerraformBackend | undefined; 24 | switch(ctx.backendType){ 25 | case BackendTypes.azurerm: 26 | backend = new AzureRMBackend(this.runner, this.logger); 27 | break; 28 | case BackendTypes.aws: 29 | backend = new AwsBackend(); 30 | break; 31 | case BackendTypes.gcs: 32 | backend = new GcsBackend(this.taskAgent); 33 | break; 34 | } 35 | return backend 36 | } 37 | 38 | async exec(ctx: ITaskContext): Promise { 39 | const backend = this.getBackend(ctx); 40 | const options = await new RunWithTerraform(ctx) 41 | .withBackend(ctx, backend) 42 | .withSecureVarFile(this.taskAgent, ctx.secureVarsFileId, ctx.secureVarsFileName) 43 | .withCommandOptions(ctx.commandOptions) 44 | .build(); 45 | 46 | const result = await this.runner.exec(options); 47 | return result.toCommandResponse(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /tasks/terraform-cli/src/commands/tf-output.ts: -------------------------------------------------------------------------------- 1 | import { CommandResponse, ICommand } from "."; 2 | import { ITaskContext } from "../context"; 3 | import { ILogger } from "../logger"; 4 | import { IRunner } from "../runners"; 5 | import { RunWithTerraform } from "../runners/builders"; 6 | 7 | export class TerraformOutput implements ICommand { 8 | constructor( 9 | private readonly runner: IRunner, 10 | private readonly logger: ILogger 11 | ) { 12 | } 13 | 14 | async exec(ctx: ITaskContext): Promise { 15 | const options = await new RunWithTerraform(ctx, true) 16 | .withJsonOutput(ctx.commandOptions) 17 | .withCommandOptions(ctx.commandOptions) 18 | .build(); 19 | const result = await this.runner.exec(options); 20 | 21 | if(result.stdout){ 22 | const outputVariables = JSON.parse(result.stdout); 23 | 24 | for(const key in outputVariables){ 25 | const outputVariable = outputVariables[key]; 26 | if(["string", "number", "bool"].includes(outputVariable.type)){ 27 | // set pipeline variable so its available to subsequent steps 28 | ctx.setVariable(`TF_OUT_${key.toUpperCase()}`, outputVariable.value, outputVariable.sensitive); 29 | } 30 | } 31 | } 32 | else{ 33 | this.logger.warning("Terraform output command was run but, returned no results. No output variables found.") 34 | } 35 | 36 | return result.toCommandResponse(); 37 | } 38 | } -------------------------------------------------------------------------------- /tasks/terraform-cli/src/commands/tf-refresh.ts: -------------------------------------------------------------------------------- 1 | import { CommandResponse, ICommand } from "."; 2 | import { ITaskContext } from "../context"; 3 | import { TerraformProviderContext } from "../providers"; 4 | import { IRunner } from "../runners"; 5 | import { RunWithTerraform } from "../runners/builders"; 6 | import { ITaskAgent } from "../task-agent"; 7 | 8 | export class TerraformRefresh implements ICommand { 9 | constructor( 10 | private readonly taskAgent: ITaskAgent, 11 | private readonly runner: IRunner, 12 | private readonly providers: TerraformProviderContext 13 | ) { 14 | } 15 | 16 | async exec(ctx: ITaskContext): Promise { 17 | await this.providers.init(); 18 | const options = await new RunWithTerraform(ctx) 19 | .withSecureVarFile(this.taskAgent, ctx.secureVarsFileId, ctx.secureVarsFileName) 20 | .withCommandOptions(ctx.commandOptions) 21 | .build(); 22 | const result = await this.runner.exec(options); 23 | 24 | return result.toCommandResponse(); 25 | } 26 | } -------------------------------------------------------------------------------- /tasks/terraform-cli/src/commands/tf-show.ts: -------------------------------------------------------------------------------- 1 | import { CommandResponse, ICommand } from "."; 2 | import { ITaskContext } from "../context"; 3 | import { ILogger } from "../logger"; 4 | import { IRunner } from "../runners"; 5 | import { RunWithTerraform } from "../runners/builders"; 6 | import { ITaskAgent } from "../task-agent"; 7 | 8 | export class TerraformShow implements ICommand { 9 | constructor( 10 | private readonly taskAgent: ITaskAgent, 11 | private readonly runner: IRunner, 12 | private readonly logger: ILogger 13 | ) { 14 | } 15 | 16 | async exec(ctx: ITaskContext): Promise { 17 | const options = await new RunWithTerraform(ctx, true) 18 | .withSecureVarFile(this.taskAgent, ctx.secureVarsFileId, ctx.secureVarsFileName) 19 | .withJsonOutput(ctx.commandOptions) 20 | .withCommandOptions(ctx.commandOptions) 21 | .withPlanOrStateFile(ctx.planOrStateFilePath) 22 | .build(); 23 | const result = await this.runner.exec(options); 24 | 25 | //check for destroy 26 | if (ctx.planOrStateFilePath) 27 | { 28 | if(ctx.planOrStateFilePath.includes(".tfstate")) 29 | { 30 | this.logger.warning("Cannot check for destroy in .tfstate file"); 31 | } 32 | else 33 | { 34 | this.detectDestroyChanges(ctx, result.stdout); 35 | } 36 | } 37 | 38 | return result.toCommandResponse(); 39 | } 40 | 41 | private detectDestroyChanges(ctx: ITaskContext, result: string): void 42 | { 43 | const resultNoEol = result.replace(/(\r\n|\r|\n|\\n|\t|\\")/gm, ""); 44 | let jsonResult = JSON.parse(resultNoEol); 45 | const deleteValue = "delete"; 46 | 47 | if(jsonResult.resource_changes){ 48 | for (let resourceChange of jsonResult.resource_changes) { 49 | if (resourceChange.change.actions.includes(deleteValue)) 50 | { 51 | this.setDestroyDetectedFlag(ctx, true); 52 | this.logger.warning("Destroy detected!") 53 | return; 54 | } 55 | } 56 | } 57 | 58 | this.logger.debug("No destroy detected") 59 | this.setDestroyDetectedFlag(ctx, false); 60 | } 61 | 62 | private setDestroyDetectedFlag(ctx: ITaskContext, value : boolean):void 63 | { 64 | ctx.setVariable("TERRAFORM_PLAN_HAS_DESTROY_CHANGES", value.toString(), false); 65 | this.logger.debug(`set vso[task.setvariable variable=TERRAFORM_PLAN_HAS_DESTROY_CHANGES] to ${value}`) 66 | } 67 | } -------------------------------------------------------------------------------- /tasks/terraform-cli/src/commands/tf-state-list.ts: -------------------------------------------------------------------------------- 1 | import {CommandResponse, ICommand} from "."; 2 | import { ITaskContext } from "../context"; 3 | import { IRunner } from "../runners"; 4 | import { RunWithTerraform } from "../runners/builders"; 5 | 6 | export class TerraformStateListCommand implements ICommand { 7 | constructor( 8 | private readonly runner: IRunner, 9 | ) { 10 | } 11 | 12 | async exec(ctx: ITaskContext): Promise { 13 | const options = await new RunWithTerraform(ctx, false, ctx.stateSubCommand, [ctx.name]) 14 | .withCommandOptions(ctx.commandOptions) 15 | .withResourceAddresses(ctx.stateAddresses) 16 | .build(); 17 | 18 | const result = await this.runner.exec(options); 19 | 20 | return result.toCommandResponse(); 21 | } 22 | } -------------------------------------------------------------------------------- /tasks/terraform-cli/src/commands/tf-state-move.ts: -------------------------------------------------------------------------------- 1 | import {CommandResponse, ICommand} from "."; 2 | import { ITaskContext } from "../context"; 3 | import { IRunner } from "../runners"; 4 | import { RunWithTerraform } from "../runners/builders"; 5 | 6 | export class TerraformStateMoveCommand implements ICommand { 7 | constructor( 8 | private readonly runner: IRunner, 9 | ) { 10 | } 11 | 12 | async exec(ctx: ITaskContext): Promise { 13 | const options = await new RunWithTerraform(ctx, false, ctx.stateSubCommand, [ctx.name]) 14 | .withCommandOptions(ctx.commandOptions) 15 | .withSourceDestination(ctx.stateMoveSource, ctx.stateMoveDestination) 16 | .build(); 17 | 18 | const result = await this.runner.exec(options); 19 | 20 | return result.toCommandResponse(); 21 | } 22 | } -------------------------------------------------------------------------------- /tasks/terraform-cli/src/commands/tf-state-remove.ts: -------------------------------------------------------------------------------- 1 | import {CommandResponse, ICommand} from "."; 2 | import { ITaskContext } from "../context"; 3 | import { IRunner } from "../runners"; 4 | import { RunWithTerraform } from "../runners/builders"; 5 | 6 | export class TerraformStateRemoveCommand implements ICommand { 7 | constructor( 8 | private readonly runner: IRunner, 9 | ) { 10 | } 11 | 12 | async exec(ctx: ITaskContext): Promise { 13 | const options = await new RunWithTerraform(ctx, false, ctx.stateSubCommand, [ctx.name]) 14 | .withCommandOptions(ctx.commandOptions) 15 | .withResourceAddresses(ctx.stateAddresses) 16 | .build(); 17 | 18 | const result = await this.runner.exec(options); 19 | 20 | return result.toCommandResponse(); 21 | } 22 | } -------------------------------------------------------------------------------- /tasks/terraform-cli/src/commands/tf-state.ts: -------------------------------------------------------------------------------- 1 | import {CommandResponse, ICommand} from "."; 2 | import {ITaskContext} from "../context"; 3 | 4 | export class TerraformStateCommand implements ICommand { 5 | constructor( 6 | private readonly subCommands: { [subCommand: string]: ICommand } 7 | ) { 8 | } 9 | 10 | exec(ctx: ITaskContext): Promise { 11 | const subCommand = this.subCommands[ctx.stateSubCommand]; 12 | if (subCommand) { 13 | return subCommand.exec(ctx); 14 | } else { 15 | throw new Error(`State sub-command "${ctx.stateSubCommand}" is not supported`); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tasks/terraform-cli/src/commands/tf-validate.ts: -------------------------------------------------------------------------------- 1 | import { ICommand, CommandResponse } from "."; 2 | import { IRunner } from "../runners"; 3 | import { ITaskAgent } from "../task-agent"; 4 | import { RunnerOptionsBuilder, RunWithTerraform } from "../runners/builders"; 5 | import { ITaskContext } from "../context"; 6 | 7 | export class TerraformValidate implements ICommand { 8 | private readonly taskAgent: ITaskAgent; 9 | private readonly runner: IRunner; 10 | 11 | constructor(taskAgent: ITaskAgent, runner: IRunner){ 12 | this.runner = runner; 13 | this.taskAgent = taskAgent; 14 | } 15 | 16 | async exec(ctx: ITaskContext): Promise { 17 | let builder: RunnerOptionsBuilder = new RunWithTerraform(ctx); 18 | 19 | if(!ctx.terraformVersionMinor || ctx.terraformVersionMinor < 15){ 20 | builder = builder.withSecureVarFile(this.taskAgent, ctx.secureVarsFileId, ctx.secureVarsFileName) 21 | } 22 | builder = builder.withCommandOptions(ctx.commandOptions); 23 | const options = await builder.build(); 24 | const result = await this.runner.exec(options); 25 | return result.toCommandResponse(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tasks/terraform-cli/src/commands/tf-version.ts: -------------------------------------------------------------------------------- 1 | import { ICommand, CommandResponse } from "."; 2 | import { IRunner, RunnerOptions } from "../runners"; 3 | import { ITaskContext } from "../context"; 4 | import { ILogger } from "../logger"; 5 | 6 | const versionRe = /([0-9]+)\.([0-9]+)\.([0-9]+)?/; 7 | const versionOutOfDate = /Your version of Terraform is out of date! The latest version\r?\nis ([0-9]+\.[0-9]+\.[0-9]+)\./; 8 | 9 | export class TerraformVersion implements ICommand { 10 | private readonly runner: IRunner; 11 | private readonly logger: ILogger 12 | 13 | constructor(runner: IRunner, logger: ILogger){ 14 | this.runner = runner, 15 | this.logger = logger; 16 | } 17 | 18 | async exec(ctx: ITaskContext): Promise { 19 | const options = new RunnerOptions("terraform", "version", ctx.cwd); 20 | const result = await this.runner.exec(options); 21 | const version = result.stdout.match(versionRe); 22 | if(version){ 23 | ctx.setTerraformVersion(version[0], Number.parseInt(version[1]), Number.parseInt(version[2]), Number.parseInt(version[3])) 24 | } 25 | if(ctx.name === "version"){ 26 | const outOfDate = result.stdout.match(versionOutOfDate); 27 | if (outOfDate !== null){ 28 | this.logger.warning(`Your version of Terraform is out of date! The latest version is ${String(outOfDate[1])}.`); 29 | } 30 | } 31 | 32 | return result.toCommandResponse(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tasks/terraform-cli/src/commands/tf-workspace-new.ts: -------------------------------------------------------------------------------- 1 | import {CommandResponse, CommandStatus, ICommand} from "."; 2 | import { ITaskContext } from "../context"; 3 | import { IRunner } from "../runners"; 4 | import { RunWithTerraform } from "../runners/builders"; 5 | 6 | export class TerraformWorkspaceNew implements ICommand { 7 | constructor( 8 | private readonly runner: IRunner 9 | ) { 10 | } 11 | 12 | async exec(ctx: ITaskContext): Promise { 13 | const options = await new RunWithTerraform(ctx, false, ctx.workspaceSubCommand, [ctx.name]) 14 | .withCommandOptions(ctx.commandOptions) 15 | .withWorkspace(ctx.workspaceName) 16 | .build(); 17 | 18 | const result = await this.runner.exec(options); 19 | 20 | 21 | const command_result = result.toCommandResponse(); 22 | const skipExistingWorkspace = ctx.skipExistingWorkspace && command_result.message && command_result.message.indexOf("already exists") >= 0; 23 | return new CommandResponse( 24 | skipExistingWorkspace ? CommandStatus.Success : command_result.status, 25 | command_result.message, 26 | skipExistingWorkspace ? 0 : command_result.lastExitCode, 27 | ); 28 | } 29 | } -------------------------------------------------------------------------------- /tasks/terraform-cli/src/commands/tf-workspace-select.ts: -------------------------------------------------------------------------------- 1 | import { CommandResponse, ICommand } from "."; 2 | import { ITaskContext } from "../context"; 3 | import { IRunner } from "../runners"; 4 | import { RunWithTerraform } from "../runners/builders"; 5 | 6 | export class TerraformWorkspaceSelect implements ICommand { 7 | constructor( 8 | private readonly runner: IRunner 9 | ) { 10 | } 11 | 12 | async exec(ctx: ITaskContext): Promise { 13 | const options = await new RunWithTerraform(ctx, false, ctx.workspaceSubCommand, [ctx.name]) 14 | .withCommandOptions(ctx.commandOptions) 15 | .withWorkspace(ctx.workspaceName) 16 | .build(); 17 | 18 | const result = await this.runner.exec(options); 19 | 20 | return result.toCommandResponse(); 21 | } 22 | } -------------------------------------------------------------------------------- /tasks/terraform-cli/src/commands/tf-workspace.ts: -------------------------------------------------------------------------------- 1 | import { CommandResponse, ICommand } from "."; 2 | import { ITaskContext } from "../context"; 3 | 4 | export class TerraformWorkspace implements ICommand{ 5 | constructor( 6 | private readonly subCommands: {[subCommand: string]: ICommand} 7 | ) { 8 | } 9 | 10 | exec(ctx: ITaskContext): Promise{ 11 | const subCommand = this.subCommands[ctx.workspaceSubCommand]; 12 | if(subCommand){ 13 | return subCommand.exec(ctx); 14 | } 15 | else{ 16 | throw new Error(`Workspace sub-command "${ctx.workspaceSubCommand}" is not supported`); 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /tasks/terraform-cli/src/index.ts: -------------------------------------------------------------------------------- 1 | import { AzdoTaskContext } from './context' 2 | import { AzdoRunner, AzdoToolFactory } from "./runners"; 3 | import { AzdoTaskAgent } from './task-agent'; 4 | import { Task } from "./task"; 5 | import * as tasks from 'azure-pipelines-task-lib'; 6 | import ai = require('applicationinsights'); 7 | import ApplicationInsightsLogger from './logger/ai-logger'; 8 | import TaskLogger from './logger/task-logger'; 9 | import { CommandResponse, CommandStatus } from './commands'; 10 | 11 | const allowTelemetryCollection = tasks.getBoolInput("allowTelemetryCollection") 12 | if(allowTelemetryCollection) { 13 | ai.setup(tasks.getInput("aiInstrumentationKey")) 14 | .setAutoCollectConsole(true, true) 15 | .setAutoCollectExceptions(true) 16 | .setAutoCollectDependencies(true) 17 | .setAutoDependencyCorrelation(true) 18 | .setInternalLogging(false) 19 | .start(); 20 | 21 | ai.defaultClient.commonProperties = { 22 | 'system.teamfoundationcollectionuri': tasks.getVariable("System.TeamFoundationCollectionUri"), 23 | 'system.teamproject': tasks.getVariable("System.TeamProject"), 24 | 'system.hosttype': tasks.getVariable("System.HostType"), 25 | 'agent.os': tasks.getVariable("Agent.OS"), 26 | 'agent.osarchitecture': tasks.getVariable("Agent.OSArchitecture"), 27 | 'agent.jobstatus': tasks.getVariable("Agent.JobStatus") 28 | } 29 | } 30 | 31 | const taskContext = new AzdoTaskContext(); 32 | const toolFactory = new AzdoToolFactory(); 33 | const taskLogger = new TaskLogger(taskContext, tasks); 34 | const aiLogger = new ApplicationInsightsLogger(taskContext, taskLogger, ai.defaultClient); 35 | const runner = new AzdoRunner(toolFactory, aiLogger); 36 | const taskAgent = new AzdoTaskAgent(); 37 | const task = new Task(taskContext, runner, taskAgent, aiLogger); 38 | 39 | task.exec() 40 | .then((response: CommandResponse) => { 41 | switch(response.status){ 42 | case CommandStatus.Failed: 43 | tasks.setResult(tasks.TaskResult.Failed, response.message || ""); 44 | break; 45 | case CommandStatus.SuccessWithIssues: 46 | tasks.setResult(tasks.TaskResult.SucceededWithIssues, response.message || ""); 47 | break; 48 | case CommandStatus.SuccessWithIssues: 49 | tasks.setResult(tasks.TaskResult.Succeeded, response.message || ""); 50 | break; 51 | } 52 | if(allowTelemetryCollection){ 53 | ai.defaultClient.flush(); 54 | } 55 | }) 56 | .catch((error) => { 57 | tasks.setResult(tasks.TaskResult.Failed, error); 58 | if(allowTelemetryCollection){ 59 | ai.defaultClient.flush(); 60 | } 61 | }); -------------------------------------------------------------------------------- /tasks/terraform-cli/src/logger/ai-logger.ts: -------------------------------------------------------------------------------- 1 | import { TelemetryClient } from "applicationinsights"; 2 | import { RequestTelemetry, ExceptionTelemetry } from "applicationinsights/out/Declarations/Contracts"; 3 | import { ILogger } from "."; 4 | import { ITaskContext, getTrackedProperties } from "../context"; 5 | 6 | export default class ApplicationInsightsLogger implements ILogger{ 7 | constructor( 8 | private readonly ctx: ITaskContext, 9 | private readonly logger: ILogger, 10 | private readonly telemetry: TelemetryClient){ 11 | } 12 | 13 | command(success: boolean, duration: number): void { 14 | if(this.ctx.allowTelemetryCollection){ 15 | this.telemetry.trackRequest({ 16 | name: this.ctx.name, 17 | success: success, 18 | resultCode: success ? "200" : "500", 19 | duration: duration, 20 | properties: getTrackedProperties(this.ctx) 21 | }); 22 | } 23 | this.logger.command(success, duration); 24 | } 25 | error(error: string | Error, properties: any): void { 26 | if(this.ctx.allowTelemetryCollection){ 27 | this.telemetry.trackException({ 28 | exception: error instanceof Error ? error : new Error(error.toString()), 29 | properties, 30 | }); 31 | } 32 | this.logger.error(error, properties); 33 | } 34 | 35 | warning(message: string): void { 36 | this.logger.warning(message); 37 | } 38 | 39 | debug(message: string): void { 40 | this.logger.debug(message); 41 | } 42 | } -------------------------------------------------------------------------------- /tasks/terraform-cli/src/logger/index.ts: -------------------------------------------------------------------------------- 1 | export interface ILogger { 2 | command(success: boolean, duration: number, customProperties?: { [key:string]: string }): void; 3 | error(error: string | Error, properties?: any): void; 4 | warning(message: string): void; 5 | debug(message: string): void; 6 | } -------------------------------------------------------------------------------- /tasks/terraform-cli/src/logger/task-logger.ts: -------------------------------------------------------------------------------- 1 | import { ILogger } from "."; 2 | import { ITaskContext, getTrackedProperties } from "../context"; 3 | 4 | export default class TaskLogger implements ILogger { 5 | constructor( 6 | private readonly ctx: ITaskContext, 7 | private readonly tasks: any){ 8 | } 9 | 10 | command(success: boolean, duration: number): void { 11 | this.tasks.debug(`executed command '${this.ctx.name}'`, { 12 | name: this.ctx.name, 13 | success: success, 14 | resultCode: success ? 200 : 500, 15 | duration: duration, 16 | properties: getTrackedProperties(this.ctx) 17 | }) 18 | } 19 | 20 | error(message: string): void { 21 | this.tasks.error(message); 22 | } 23 | 24 | warning(message: string): void { 25 | this.tasks.warning(message); 26 | } 27 | 28 | debug(message: string): void { 29 | this.tasks.debug(message); 30 | } 31 | } -------------------------------------------------------------------------------- /tasks/terraform-cli/src/providers/aws.ts: -------------------------------------------------------------------------------- 1 | import { ITerraformProvider } from "."; 2 | 3 | interface AwsProviderConfiguration{ 4 | providerServiceAws?: string, 5 | providerServiceAwsAccessKey: string, 6 | providerServiceAwsSecretKey: string, 7 | providerAwsRegion?: string 8 | } 9 | 10 | export default class AwsProvider implements ITerraformProvider { 11 | constructor(private readonly config: AwsProviderConfiguration) { 12 | } 13 | 14 | isDefined(): boolean{ 15 | if(this.config.providerServiceAws){ 16 | return true; 17 | } 18 | else{ 19 | return false; 20 | } 21 | } 22 | 23 | async init(): Promise { 24 | process.env['AWS_ACCESS_KEY_ID'] = this.config.providerServiceAwsAccessKey; 25 | process.env['AWS_SECRET_ACCESS_KEY'] = this.config.providerServiceAwsSecretKey; 26 | process.env['AWS_REGION'] = this.config.providerAwsRegion; 27 | } 28 | } -------------------------------------------------------------------------------- /tasks/terraform-cli/src/providers/azurerm.ts: -------------------------------------------------------------------------------- 1 | import { ITerraformProvider } from "."; 2 | import { CommandPipeline } from "../commands"; 3 | import { ITaskContext } from "../context"; 4 | import { IRunner } from "../runners"; 5 | import { AzureRMAuthentication, AuthorizationScheme, ServicePrincipalCredentials, WorkloadIdentityFederationCredentials } from "../authentication/azurerm"; 6 | 7 | export default class AzureRMProvider implements ITerraformProvider { 8 | constructor( 9 | private readonly runner: IRunner, 10 | private readonly ctx: ITaskContext){ 11 | } 12 | 13 | isDefined(): boolean{ 14 | if(this.ctx.environmentServiceName){ 15 | return true; 16 | } 17 | else{ 18 | return false; 19 | } 20 | } 21 | 22 | async init(): Promise { 23 | const authorizationScheme = AzureRMAuthentication.getAuthorizationScheme(this.ctx.environmentServiceArmAuthorizationScheme); 24 | 25 | const subscriptionId = this.ctx.providerAzureRmSubscriptionId || this.ctx.environmentServiceArmSubscriptionId; 26 | 27 | if(subscriptionId){ 28 | process.env['ARM_SUBSCRIPTION_ID'] = subscriptionId; 29 | } 30 | 31 | process.env['ARM_TENANT_ID'] = this.ctx.environmentServiceArmTenantId; 32 | 33 | switch(authorizationScheme) { 34 | case AuthorizationScheme.ServicePrincipal: 35 | var servicePrincipalCredentials : ServicePrincipalCredentials = AzureRMAuthentication.getServicePrincipalCredentials(this.ctx); 36 | process.env['ARM_CLIENT_ID'] = servicePrincipalCredentials.servicePrincipalId; 37 | process.env['ARM_CLIENT_SECRET'] = servicePrincipalCredentials.servicePrincipalKey; 38 | break; 39 | 40 | case AuthorizationScheme.ManagedServiceIdentity: 41 | process.env['ARM_USE_MSI'] = 'true'; 42 | break; 43 | 44 | case AuthorizationScheme.WorkloadIdentityFederation: 45 | var workloadIdentityFederationCredentials : WorkloadIdentityFederationCredentials = AzureRMAuthentication.getWorkloadIdentityFederationCredentials(this.ctx); 46 | process.env['ARM_CLIENT_ID'] = workloadIdentityFederationCredentials.servicePrincipalId; 47 | process.env['ARM_OIDC_TOKEN'] = workloadIdentityFederationCredentials.idToken; 48 | process.env['ARM_USE_OIDC'] = 'true'; 49 | break; 50 | } 51 | 52 | if(this.ctx.runAzLogin){ 53 | //run az login so provisioners needing az cli can be run. 54 | await new CommandPipeline(this.runner) 55 | .azLogin() 56 | .exec(this.ctx); 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /tasks/terraform-cli/src/providers/google.ts: -------------------------------------------------------------------------------- 1 | import { ITerraformProvider } from "."; 2 | import { ITaskAgent } from "../task-agent"; 3 | 4 | interface GoogleProviderConfiguration{ 5 | providerGoogleCredentials?: string, 6 | providerGoogleProject?: string, 7 | providerGoogleRegion?: string, 8 | } 9 | 10 | export default class GoogleProvider implements ITerraformProvider { 11 | constructor( 12 | private readonly agent: ITaskAgent, 13 | private readonly config: GoogleProviderConfiguration) { 14 | } 15 | 16 | isDefined(): boolean{ 17 | if(this.config.providerGoogleCredentials 18 | || this.config.providerGoogleProject 19 | || this.config.providerGoogleRegion) 20 | { 21 | return true; 22 | } 23 | else{ 24 | return false; 25 | } 26 | } 27 | 28 | async init(): Promise { 29 | if(this.config.providerGoogleCredentials){ 30 | const credentials = await this.agent.downloadSecureFile(this.config.providerGoogleCredentials); 31 | process.env['GOOGLE_CREDENTIALS'] = credentials; 32 | } 33 | if(this.config.providerGoogleProject){ 34 | process.env['GOOGLE_PROJECT'] = this.config.providerGoogleProject; 35 | } 36 | if(this.config.providerGoogleRegion){ 37 | process.env['GOOGLE_REGION'] = this.config.providerGoogleProject; 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /tasks/terraform-cli/src/providers/index.ts: -------------------------------------------------------------------------------- 1 | import { ILogger } from '../logger'; 2 | 3 | export interface ITerraformProvider{ 4 | isDefined(): boolean; 5 | init(): Promise; 6 | } 7 | 8 | export { default as AzureRmProvider } from './azurerm'; 9 | export { default as AwsProvider } from './aws'; 10 | 11 | export class TerraformProviderContext { 12 | private readonly providers: ITerraformProvider[]; 13 | private readonly logger: ILogger; 14 | 15 | constructor(logger: ILogger, ...providers: ITerraformProvider[]){ 16 | this.providers = providers; 17 | this.logger = logger; 18 | } 19 | 20 | async init(): Promise{ 21 | process.env['TF_IN_AUTOMATION'] = 'True'; 22 | 23 | for(let i = 0; i < this.providers.length; i++){ 24 | if(this.providers[i].isDefined()){ 25 | await this.providers[i].init(); 26 | } 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /tasks/terraform-cli/src/runners/azdo-runner.ts: -------------------------------------------------------------------------------- 1 | import { IExecOptions } from "azure-pipelines-task-lib/toolrunner"; 2 | import os from 'os'; 3 | import { IRunner, IToolFactory, RunnerOptions, RunnerResult } from "."; 4 | import { ILogger } from "../logger"; 5 | import { TerraformAggregateError } from "./terraform-error"; 6 | 7 | export default class AzdoRunner implements IRunner { 8 | constructor(private readonly toolFactory: IToolFactory, private readonly logger: ILogger){ } 9 | 10 | private _processBuffers(buffers: Buffer[], delimiter: string = os.EOL): string { 11 | return buffers 12 | .map(data => { 13 | return data.toString(); 14 | }) 15 | .join(delimiter) 16 | .toString(); 17 | } 18 | 19 | async exec(options: RunnerOptions): Promise { 20 | const tool = this.toolFactory.create(options.tool); 21 | 22 | const stdOutBuffers: Buffer[] = []; 23 | const stdErrBuffers: Buffer[] = []; 24 | 25 | //buffer stdout writes so it can be set in result 26 | tool.on("stdout", (data: Buffer) => { 27 | stdOutBuffers.push(data); 28 | }); 29 | 30 | //buffer stderr writes so it can be set in result 31 | tool.on("stderr", (data: Buffer) => { 32 | stdErrBuffers.push(data); 33 | }); 34 | 35 | // add the groups & subgroups 36 | options.path.forEach(segment => { 37 | tool.arg(segment); 38 | }); 39 | 40 | // add the command itself 41 | tool.arg(options.command); 42 | 43 | // add the args for the command 44 | options.args.forEach(arg => { 45 | tool.arg(arg); 46 | }); 47 | 48 | const exitCode = await tool.exec({ 49 | cwd: options.cwd, 50 | ignoreReturnCode: true, 51 | silent: options.silent, 52 | }); 53 | 54 | const delimiter = options.rawOutput ? "" : undefined 55 | const stdout = this._processBuffers(stdOutBuffers, delimiter); 56 | const stderr = this._processBuffers(stdErrBuffers, delimiter); 57 | 58 | if(!options.successfulExitCodes.includes(exitCode)){ 59 | if(options.tool === 'terraform'){ 60 | const terraformError = new TerraformAggregateError(options.command, stderr, exitCode); 61 | this.logger.error(terraformError); 62 | } 63 | else{ 64 | this.logger.error(new Error(stderr)); 65 | } 66 | } 67 | 68 | return new RunnerResult(exitCode, stdout, stderr, options.successfulExitCodes); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /tasks/terraform-cli/src/runners/azdo-tool-factory.ts: -------------------------------------------------------------------------------- 1 | import tasks = require("azure-pipelines-task-lib/task"); 2 | import { IToolFactory, IToolRunner } from './index'; 3 | 4 | export default class AzdoToolFactory implements IToolFactory { 5 | create(tool: string): IToolRunner { 6 | const terraformPath = tasks.which(tool, true); 7 | return tasks.tool(terraformPath); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tasks/terraform-cli/src/runners/builders/index.ts: -------------------------------------------------------------------------------- 1 | import { RunnerOptions } from ".."; 2 | 3 | export abstract class RunnerOptionsBuilder { 4 | abstract build(): Promise; 5 | } 6 | 7 | export abstract class RunnerOptionsDecorator extends RunnerOptionsBuilder{ 8 | constructor(protected readonly builder: RunnerOptionsBuilder){ 9 | super(); 10 | } 11 | } 12 | 13 | export { default as RunWithAutoApprove } from './run-with-auto-approve'; 14 | export { default as RunWithAzCli } from "./run-with-azcli"; 15 | export { default as RunWithBackend } from './run-with-backend'; 16 | export { default as RunWithCommandOptions } from './run-with-command-options'; 17 | export { default as RunWithForce } from './run-with-force'; 18 | export { default as RunWithDetailedExitCode } from "./run-with-forced-detailed-exit-code"; 19 | export { default as RunWithJsonOutput } from './run-with-json-output'; 20 | export { default as RunWithLockId } from './run-with-lock-id'; 21 | export { default as RunWithOptions } from './run-with-options'; 22 | export { default as RunWithPlanOrStateFile } from './run-with-plan-or-state-file'; 23 | export { default as RunWithRawOutputs } from "./run-with-raw-outputs"; 24 | export { default as RunWithResourceTarget } from './run-with-resource-target'; 25 | export { default as RunWithResourceAddresses } from './run-with-resource-addresses'; 26 | export { default as RunWithSecureVarFile } from './run-with-secure-var-file'; 27 | export { default as RunWithSourceDestination } from './run-with-source-destination'; 28 | export { default as RunWithSuccessCodes } from './run-with-success-codes'; 29 | export { default as RunWithTerraform } from "./run-with-terraform"; 30 | export { default as RunWithWorkspace } from "./run-with-workspace"; 31 | 32 | -------------------------------------------------------------------------------- /tasks/terraform-cli/src/runners/builders/run-with-auto-approve.ts: -------------------------------------------------------------------------------- 1 | import { RunnerOptionsBuilder, RunnerOptionsDecorator } from "."; 2 | import { RunnerOptions } from ".."; 3 | 4 | export default class RunWithAutoApprove extends RunnerOptionsDecorator{ 5 | constructor(builder: RunnerOptionsBuilder) { 6 | super(builder); 7 | } 8 | async build(): Promise { 9 | const options = await this.builder.build(); 10 | if(options.command !== 'apply' && options.command !== 'destroy') 11 | throw "'-auto-approve option only valid for commands apply and destroy"; 12 | const autoApproveOption = "-auto-approve"; 13 | if(!options.args || (options.args && options.args.indexOf(autoApproveOption) === -1)){ 14 | options.args.push(autoApproveOption); 15 | } 16 | return options; 17 | } 18 | } 19 | 20 | declare module "." { 21 | interface RunnerOptionsBuilder { 22 | withAutoApprove(this: RunnerOptionsBuilder): RunnerOptionsBuilder; 23 | } 24 | } 25 | 26 | RunnerOptionsBuilder.prototype.withAutoApprove = function(this: RunnerOptionsBuilder): RunnerOptionsBuilder { 27 | return new RunWithAutoApprove(this); 28 | } -------------------------------------------------------------------------------- /tasks/terraform-cli/src/runners/builders/run-with-azcli.ts: -------------------------------------------------------------------------------- 1 | import { RunnerOptionsBuilder } from "."; 2 | import { RunnerOptions } from ".."; 3 | 4 | export default class RunWithAzCli extends RunnerOptionsBuilder { 5 | constructor( 6 | private readonly command: string, 7 | private readonly path: string[] = [], 8 | private readonly cwd?: string, 9 | private readonly silent?: boolean 10 | ) { 11 | super(); 12 | } 13 | build(): Promise { 14 | return Promise.resolve( 15 | new RunnerOptions("az", this.command, this.cwd, this.silent, this.path) 16 | ) 17 | } 18 | } -------------------------------------------------------------------------------- /tasks/terraform-cli/src/runners/builders/run-with-backend.ts: -------------------------------------------------------------------------------- 1 | import { RunnerOptions } from ".."; 2 | import { RunnerOptionsDecorator, RunnerOptionsBuilder } from "."; 3 | import { ITerraformBackend } from "../../backends"; 4 | import { ITaskContext } from "../../context"; 5 | 6 | export default class RunWithBackend extends RunnerOptionsDecorator{ 7 | constructor( 8 | builder: RunnerOptionsBuilder, 9 | private readonly ctx: ITaskContext, 10 | private readonly backend?: ITerraformBackend) { 11 | super(builder); 12 | } 13 | async build(): Promise { 14 | const options = await this.builder.build(); 15 | if(this.backend){ 16 | const result = await this.backend.init(this.ctx); 17 | options.addArgs(...result.args); 18 | } 19 | return options; 20 | } 21 | } 22 | 23 | declare module "."{ 24 | interface RunnerOptionsBuilder{ 25 | withBackend(this: RunnerOptionsBuilder, ctx: ITaskContext, backend?: ITerraformBackend): RunnerOptionsBuilder; 26 | } 27 | } 28 | 29 | RunnerOptionsBuilder.prototype.withBackend = function(this: RunnerOptionsBuilder, ctx: ITaskContext, backend?: ITerraformBackend): RunnerOptionsBuilder { 30 | return new RunWithBackend(this, ctx, backend); 31 | } 32 | -------------------------------------------------------------------------------- /tasks/terraform-cli/src/runners/builders/run-with-force.ts: -------------------------------------------------------------------------------- 1 | import { RunnerOptionsBuilder, RunnerOptionsDecorator } from "."; 2 | import { RunnerOptions } from ".."; 3 | 4 | export default class RunWithForce extends RunnerOptionsDecorator{ 5 | constructor(builder: RunnerOptionsBuilder) { 6 | super(builder); 7 | } 8 | async build(): Promise { 9 | const options = await this.builder.build(); 10 | const forceOption = "-force"; 11 | if(!options.args || (options.args && options.args.indexOf(forceOption) === -1)){ 12 | options.args.push(forceOption); 13 | } 14 | return options; 15 | } 16 | } 17 | 18 | declare module "." { 19 | interface RunnerOptionsBuilder { 20 | withForce(this: RunnerOptionsBuilder): RunnerOptionsBuilder; 21 | } 22 | } 23 | 24 | RunnerOptionsBuilder.prototype.withForce = function(this: RunnerOptionsBuilder): RunnerOptionsBuilder { 25 | return new RunWithForce(this); 26 | } -------------------------------------------------------------------------------- /tasks/terraform-cli/src/runners/builders/run-with-forced-detailed-exit-code.ts: -------------------------------------------------------------------------------- 1 | import { RunnerOptionsBuilder, RunnerOptionsDecorator } from "."; 2 | import { RunnerOptions } from ".."; 3 | 4 | export default class RunWithForcedDetailedExitCode extends RunnerOptionsDecorator { 5 | constructor(builder: RunnerOptionsBuilder, private readonly commandOptions: string | undefined, private readonly forceAdd: boolean = true) { 6 | super(builder); 7 | } 8 | async build(): Promise { 9 | const options = await this.builder.build(); 10 | 11 | if (this.forceAdd) { 12 | const detailedExitCodeOption = "-detailed-exitcode"; 13 | if ((!options.args || (options.args && options.args.indexOf(detailedExitCodeOption) === -1)) && (!this.commandOptions || !this.commandOptions.includes(detailedExitCodeOption))) { 14 | options.args.push(detailedExitCodeOption); 15 | } 16 | } 17 | return options; 18 | } 19 | } 20 | 21 | declare module "." { 22 | interface RunnerOptionsBuilder { 23 | withForcedDetailedExitCode(this: RunnerOptionsBuilder, commandOptions: string | undefined, forceAdd: boolean): RunnerOptionsBuilder; 24 | } 25 | } 26 | 27 | RunnerOptionsBuilder.prototype.withForcedDetailedExitCode = function (this: RunnerOptionsBuilder, commandOptions: string | undefined, forceAdd: boolean): RunnerOptionsBuilder { 28 | return new RunWithForcedDetailedExitCode(this, commandOptions, forceAdd); 29 | } -------------------------------------------------------------------------------- /tasks/terraform-cli/src/runners/builders/run-with-json-output.ts: -------------------------------------------------------------------------------- 1 | import { RunnerOptionsBuilder, RunnerOptionsDecorator } from "."; 2 | import { RunnerOptions } from ".."; 3 | 4 | export default class RunWithJsonOutput extends RunnerOptionsDecorator{ 5 | constructor(builder: RunnerOptionsBuilder, private readonly commandOptions: string | undefined) { 6 | super(builder); 7 | } 8 | async build(): Promise { 9 | const options = await this.builder.build(); 10 | const jsonOutputOption = "-json"; 11 | // is not defined already as an arg and not provided in command options input 12 | if((!options.args || (options.args && options.args.indexOf(jsonOutputOption) === -1)) && (!this.commandOptions || !this.commandOptions.includes(jsonOutputOption))){ 13 | options.args.push(jsonOutputOption); 14 | } 15 | return options; 16 | } 17 | } 18 | 19 | declare module "." { 20 | interface RunnerOptionsBuilder { 21 | withJsonOutput(this: RunnerOptionsBuilder, commandOptions: string | undefined): RunnerOptionsBuilder; 22 | } 23 | } 24 | 25 | RunnerOptionsBuilder.prototype.withJsonOutput = function(this: RunnerOptionsBuilder, commandOptions: string | undefined): RunnerOptionsBuilder { 26 | return new RunWithJsonOutput(this, commandOptions); 27 | } -------------------------------------------------------------------------------- /tasks/terraform-cli/src/runners/builders/run-with-lock-id.ts: -------------------------------------------------------------------------------- 1 | import { RunnerOptionsBuilder, RunnerOptionsDecorator } from "."; 2 | import { RunnerOptions } from ".."; 3 | 4 | export default class RunWithLockId extends RunnerOptionsDecorator{ 5 | constructor(builder: RunnerOptionsBuilder, 6 | private readonly lockId: string) { 7 | super(builder); 8 | } 9 | async build(): Promise { 10 | const options = await this.builder.build(); 11 | options.args.push(this.lockId); 12 | return options; 13 | } 14 | } 15 | 16 | declare module "." { 17 | interface RunnerOptionsBuilder { 18 | withLockId(this: RunnerOptionsBuilder, lockId: string): RunnerOptionsBuilder; 19 | } 20 | } 21 | 22 | RunnerOptionsBuilder.prototype.withLockId = function(this: RunnerOptionsBuilder, lockId: string): RunnerOptionsBuilder { 23 | return new RunWithLockId(this, lockId); 24 | } -------------------------------------------------------------------------------- /tasks/terraform-cli/src/runners/builders/run-with-options.ts: -------------------------------------------------------------------------------- 1 | import { RunnerOptionsBuilder, RunnerOptionsDecorator } from "."; 2 | import { RunnerOptions } from ".."; 3 | 4 | export default class RunWithOptions extends RunnerOptionsDecorator{ 5 | 6 | constructor( 7 | builder: RunnerOptionsBuilder, 8 | private readonly commandOptions: string | undefined, 9 | private readonly optionsToAdd: string[]) { 10 | super(builder); 11 | } 12 | async build(): Promise { 13 | const options = await this.builder.build(); 14 | 15 | this.optionsToAdd.forEach((opt) => { 16 | if( 17 | (!options.args || (options.args && options.args.indexOf(opt) === -1)) && 18 | (!this.commandOptions || !this.commandOptions.includes(opt)) 19 | ){ 20 | options.args.push(opt); 21 | } 22 | }) 23 | return options; 24 | } 25 | } 26 | 27 | declare module "." { 28 | interface RunnerOptionsBuilder { 29 | withOptions(this: RunnerOptionsBuilder, commandOptions: string | undefined, optionsToAdd: string[]): RunnerOptionsBuilder; 30 | } 31 | } 32 | 33 | RunnerOptionsBuilder.prototype.withOptions = function(this: RunnerOptionsBuilder, commandOptions: string | undefined, optionsToAdd: string[]): RunnerOptionsBuilder { 34 | return new RunWithOptions(this, commandOptions, optionsToAdd); 35 | } -------------------------------------------------------------------------------- /tasks/terraform-cli/src/runners/builders/run-with-plan-or-state-file.ts: -------------------------------------------------------------------------------- 1 | import { RunnerOptionsBuilder, RunnerOptionsDecorator } from "."; 2 | import { RunnerOptions } from ".."; 3 | 4 | export default class RunWithPlanOrStateFile extends RunnerOptionsDecorator{ 5 | constructor(builder: RunnerOptionsBuilder, private readonly planOrStateFile: string) { 6 | super(builder); 7 | } 8 | async build(): Promise { 9 | const options = await this.builder.build(); 10 | options.args.push(this.planOrStateFile); 11 | return options; 12 | } 13 | } 14 | 15 | declare module "." { 16 | interface RunnerOptionsBuilder { 17 | withPlanOrStateFile(this: RunnerOptionsBuilder, planOrStateFile: string): RunnerOptionsBuilder; 18 | } 19 | } 20 | 21 | RunnerOptionsBuilder.prototype.withPlanOrStateFile = function(this: RunnerOptionsBuilder, planOrStateFile: string): RunnerOptionsBuilder { 22 | return new RunWithPlanOrStateFile(this, planOrStateFile); 23 | } -------------------------------------------------------------------------------- /tasks/terraform-cli/src/runners/builders/run-with-raw-outputs.ts: -------------------------------------------------------------------------------- 1 | // This will prevent processing of the std(out|err) in the runner 2 | // to be returned as is. 3 | 4 | import { RunnerOptionsBuilder, RunnerOptionsDecorator } from "."; 5 | import { RunnerOptions } from ".."; 6 | 7 | export default class RunWithRawOutputs extends RunnerOptionsDecorator{ 8 | constructor(builder: RunnerOptionsBuilder) { 9 | super(builder); 10 | } 11 | async build(): Promise { 12 | const options = await this.builder.build(); 13 | options.rawOutput = true 14 | return options; 15 | } 16 | } 17 | 18 | declare module "." { 19 | interface RunnerOptionsBuilder { 20 | withRawOutputs(this: RunnerOptionsBuilder): RunnerOptionsBuilder; 21 | } 22 | } 23 | 24 | RunnerOptionsBuilder.prototype.withRawOutputs = function(this: RunnerOptionsBuilder): RunnerOptionsBuilder { 25 | return new RunWithRawOutputs(this); 26 | } -------------------------------------------------------------------------------- /tasks/terraform-cli/src/runners/builders/run-with-resource-addresses.ts: -------------------------------------------------------------------------------- 1 | import { RunnerOptionsBuilder, RunnerOptionsDecorator } from "."; 2 | import { RunnerOptions } from ".."; 3 | 4 | export default class RunWithResourceAddresses extends RunnerOptionsDecorator{ 5 | constructor(builder: RunnerOptionsBuilder, 6 | private readonly resourceAddresses: string[],) { 7 | super(builder); 8 | } 9 | async build(): Promise { 10 | const options = await this.builder.build(); 11 | this.resourceAddresses.forEach(address => options.args.push(address)); 12 | return options; 13 | } 14 | } 15 | 16 | declare module "." { 17 | interface RunnerOptionsBuilder { 18 | withResourceAddresses(this: RunnerOptionsBuilder, resourceAddresses: string[]): RunnerOptionsBuilder; 19 | } 20 | } 21 | 22 | RunnerOptionsBuilder.prototype.withResourceAddresses = function(this: RunnerOptionsBuilder, resourceAddresses: string[]): RunnerOptionsBuilder { 23 | return new RunWithResourceAddresses(this, resourceAddresses); 24 | } -------------------------------------------------------------------------------- /tasks/terraform-cli/src/runners/builders/run-with-resource-target.ts: -------------------------------------------------------------------------------- 1 | import { RunnerOptionsBuilder, RunnerOptionsDecorator } from "."; 2 | import { RunnerOptions } from ".."; 3 | 4 | export default class RunWithResourceTarget extends RunnerOptionsDecorator{ 5 | constructor(builder: RunnerOptionsBuilder, 6 | private readonly resourceAddress: string, 7 | private readonly resourceId: string) { 8 | super(builder); 9 | } 10 | async build(): Promise { 11 | const options = await this.builder.build(); 12 | options.args.push(this.resourceAddress, this.resourceId); 13 | return options; 14 | } 15 | } 16 | 17 | declare module "." { 18 | interface RunnerOptionsBuilder { 19 | withResourceTarget(this: RunnerOptionsBuilder, resourceAddress: string, resourceId: string): RunnerOptionsBuilder; 20 | } 21 | } 22 | 23 | RunnerOptionsBuilder.prototype.withResourceTarget = function(this: RunnerOptionsBuilder, resourceAddress: string, resourceId: string): RunnerOptionsBuilder { 24 | return new RunWithResourceTarget(this, resourceAddress, resourceId); 25 | } -------------------------------------------------------------------------------- /tasks/terraform-cli/src/runners/builders/run-with-secure-var-file.ts: -------------------------------------------------------------------------------- 1 | import { RunnerOptionsDecorator, RunnerOptionsBuilder } from "."; 2 | import { ITaskAgent } from "../../task-agent"; 3 | import { RunnerOptions } from ".."; 4 | import * as dotenv from "dotenv" 5 | import path from 'path'; 6 | 7 | export default class RunWithSecureVarFile extends RunnerOptionsDecorator{ 8 | private readonly taskAgent: ITaskAgent; 9 | private readonly secureVarFileId: string | undefined; 10 | private readonly secureVarFileName: string | undefined; 11 | constructor(builder: RunnerOptionsBuilder, taskAgent: ITaskAgent, secureVarFileId?: string | undefined, secureVarFileName?: string | undefined) { 12 | super(builder); 13 | this.taskAgent = taskAgent; 14 | this.secureVarFileId = secureVarFileId; 15 | this.secureVarFileName = secureVarFileName; 16 | } 17 | async build(): Promise { 18 | const options = await this.builder.build(); 19 | if(this.secureVarFileId && this.secureVarFileName){ 20 | let secureFilePath = await this.taskAgent.downloadSecureFile(this.secureVarFileId); 21 | if(this.isEnvFile(this.secureVarFileName)) { 22 | let config = dotenv.config({ path: secureFilePath }).parsed; 23 | if ((!config) || (Object.keys(config).length === 0 && config.constructor === Object)) { 24 | throw "The .env file doesn't have valid entries."; 25 | } 26 | } else { 27 | if(options.command === 'init' || options.command === 'show') { 28 | throw `terraform ${options.command} command supports only env files, no tfvars are allowed during this stage.`; 29 | } 30 | secureFilePath = secureFilePath.replace(/ /g, '\\ '); 31 | options.addArgs(`-var-file=${secureFilePath}`); 32 | } 33 | } 34 | return options; 35 | } 36 | isEnvFile(fileName: string) { 37 | if (fileName === undefined || fileName === null) return false; 38 | if (fileName === '.env') return true; 39 | return ('.env' === path.extname(fileName)) 40 | } 41 | } 42 | 43 | declare module "."{ 44 | interface RunnerOptionsBuilder{ 45 | withSecureVarFile(this: RunnerOptionsBuilder, taskAgent: ITaskAgent, secureVarFileId?: string | undefined, secureVarFileName?: string | undefined): RunnerOptionsBuilder; 46 | } 47 | } 48 | 49 | RunnerOptionsBuilder.prototype.withSecureVarFile = function(this: RunnerOptionsBuilder, taskAgent: ITaskAgent, secureVarFileId?: string | undefined, secureVarFileName?: string | undefined): RunnerOptionsBuilder { 50 | return new RunWithSecureVarFile(this, taskAgent, secureVarFileId, secureVarFileName); 51 | } -------------------------------------------------------------------------------- /tasks/terraform-cli/src/runners/builders/run-with-show-options.ts: -------------------------------------------------------------------------------- 1 | import { RunnerOptionsBuilder, RunnerOptionsDecorator } from "."; 2 | import { RunnerOptions } from ".."; 3 | 4 | export default class RunWithAutoApprove extends RunnerOptionsDecorator{ 5 | constructor(builder: RunnerOptionsBuilder) { 6 | super(builder); 7 | } 8 | async build(): Promise { 9 | const options = await this.builder.build(); 10 | if(options.command !== 'apply' && options.command !== 'destroy') 11 | throw "'-auto-approve option only valid for commands apply and destroy"; 12 | const autoApproveOption = "-auto-approve"; 13 | if(!options.args || (options.args && options.args.indexOf(autoApproveOption) === -1)){ 14 | options.args.push(autoApproveOption); 15 | } 16 | return options; 17 | } 18 | } 19 | 20 | declare module "." { 21 | interface RunnerOptionsBuilder { 22 | withAutoApprove(this: RunnerOptionsBuilder): RunnerOptionsBuilder; 23 | } 24 | } 25 | 26 | RunnerOptionsBuilder.prototype.withAutoApprove = function(this: RunnerOptionsBuilder): RunnerOptionsBuilder { 27 | return new RunWithAutoApprove(this); 28 | } -------------------------------------------------------------------------------- /tasks/terraform-cli/src/runners/builders/run-with-source-destination.ts: -------------------------------------------------------------------------------- 1 | import { RunnerOptionsBuilder, RunnerOptionsDecorator } from "."; 2 | import { RunnerOptions } from ".."; 3 | 4 | export default class RunWithSourceDestination extends RunnerOptionsDecorator{ 5 | constructor(builder: RunnerOptionsBuilder, 6 | private readonly source: string, 7 | private readonly destination: string) { 8 | super(builder); 9 | } 10 | async build(): Promise { 11 | const options = await this.builder.build(); 12 | options.args.push(this.source, this.destination); 13 | return options; 14 | } 15 | } 16 | 17 | declare module "." { 18 | interface RunnerOptionsBuilder { 19 | withSourceDestination(this: RunnerOptionsBuilder, source: string, destination: string): RunnerOptionsBuilder; 20 | } 21 | } 22 | 23 | RunnerOptionsBuilder.prototype.withSourceDestination = function(this: RunnerOptionsBuilder, source: string, destination: string): RunnerOptionsBuilder { 24 | return new RunWithSourceDestination(this, source, destination); 25 | } -------------------------------------------------------------------------------- /tasks/terraform-cli/src/runners/builders/run-with-success-codes.ts: -------------------------------------------------------------------------------- 1 | import { RunnerOptionsBuilder, RunnerOptionsDecorator } from "."; 2 | import { RunnerOptions } from ".."; 3 | 4 | export default class RunWithSuccessCodes extends RunnerOptionsDecorator{ 5 | constructor(builder: RunnerOptionsBuilder, private readonly successCodes: number[]) { 6 | super(builder); 7 | } 8 | async build(): Promise { 9 | const options = await this.builder.build(); 10 | options.successfulExitCodes = this.successCodes; 11 | return options; 12 | } 13 | } 14 | 15 | declare module "." { 16 | interface RunnerOptionsBuilder { 17 | withSuccessCodes(this: RunnerOptionsBuilder, successCodes: number[]): RunnerOptionsBuilder; 18 | } 19 | } 20 | 21 | RunnerOptionsBuilder.prototype.withSuccessCodes = function(this: RunnerOptionsBuilder, successCodes: number[]): RunnerOptionsBuilder { 22 | return new RunWithSuccessCodes(this, successCodes); 23 | } -------------------------------------------------------------------------------- /tasks/terraform-cli/src/runners/builders/run-with-terraform.ts: -------------------------------------------------------------------------------- 1 | import { RunnerOptionsBuilder } from "."; 2 | import { RunnerOptions } from ".."; 3 | import { ITaskContext } from "../../context"; 4 | 5 | export default class RunWithTerraform extends RunnerOptionsBuilder { 6 | constructor( 7 | protected readonly ctx: ITaskContext, 8 | protected readonly silent?: boolean, 9 | protected readonly command?: string, 10 | protected readonly path?: string[] 11 | ) { 12 | super(); 13 | } 14 | build(): Promise { 15 | const command = this.command || this.ctx.name; 16 | return Promise.resolve( 17 | new RunnerOptions("terraform", command, this.ctx.cwd, this.silent, this.path) 18 | ) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tasks/terraform-cli/src/runners/builders/run-with-workspace.ts: -------------------------------------------------------------------------------- 1 | import { RunnerOptionsBuilder, RunnerOptionsDecorator } from "."; 2 | import { RunnerOptions } from ".."; 3 | 4 | export default class RunWithWorkspace extends RunnerOptionsDecorator{ 5 | constructor(builder: RunnerOptionsBuilder, private readonly workspaceName: string) { 6 | super(builder); 7 | } 8 | async build(): Promise { 9 | const options = await this.builder.build(); 10 | options.addArgs(this.workspaceName) 11 | return options; 12 | } 13 | } 14 | 15 | declare module "." { 16 | interface RunnerOptionsBuilder { 17 | withWorkspace(this: RunnerOptionsBuilder, workspaceName: string): RunnerOptionsBuilder; 18 | } 19 | } 20 | 21 | RunnerOptionsBuilder.prototype.withWorkspace = function(this: RunnerOptionsBuilder, workspaceName: string): RunnerOptionsBuilder { 22 | return new RunWithWorkspace(this, workspaceName); 23 | } -------------------------------------------------------------------------------- /tasks/terraform-cli/src/runners/index.ts: -------------------------------------------------------------------------------- 1 | import { IExecOptions } from 'azure-pipelines-task-lib/toolrunner'; 2 | 3 | export class RunnerOptions{ 4 | private _args: string[]; 5 | constructor( 6 | public readonly tool: string, 7 | public readonly command: string, 8 | public readonly cwd?: string, 9 | public readonly silent?: boolean | undefined, 10 | public readonly path: string[] = [], 11 | args: string[] = [], 12 | public successfulExitCodes: number[] = [0], 13 | public rawOutput?: boolean | false, 14 | ){ 15 | this._args = args; 16 | } 17 | 18 | get args() { 19 | return this._args; 20 | } 21 | addArgs(...args: string[]){ 22 | this._args = this._args.concat(args); 23 | } 24 | concatArgs(args: string[]){ 25 | this._args = this._args.concat(args); 26 | } 27 | } 28 | 29 | export class RunnerResult { 30 | constructor( 31 | public readonly exitCode: number, 32 | public readonly stdout: string, 33 | public readonly stderr: string, 34 | public readonly successfulExitCodes: number[] 35 | ){} 36 | } 37 | 38 | export interface IRunner{ 39 | exec(options: RunnerOptions): Promise; 40 | } 41 | 42 | export interface IToolRunner { 43 | arg(val: string | string[]): this; 44 | line(val: string): this; 45 | exec(options: IExecOptions): Q.Promise; 46 | on(event: string | symbol, listener: (...args: any[]) => void): this; 47 | } 48 | 49 | export interface IToolFactory { 50 | create(tool: string): IToolRunner 51 | } 52 | 53 | export { default as AzdoRunner } from './azdo-runner'; 54 | export { default as AzdoToolFactory } from './azdo-tool-factory'; 55 | export { TerraformAggregateError, TerraformError } from './terraform-error'; 56 | 57 | -------------------------------------------------------------------------------- /tasks/terraform-cli/src/runners/mock-tool-factory.ts: -------------------------------------------------------------------------------- 1 | import * as tasks from 'azure-pipelines-task-lib/mock-task'; 2 | import { IToolFactory, IToolRunner } from './index'; 3 | 4 | export default class MockToolFactory implements IToolFactory { 5 | create(tool: string): IToolRunner { 6 | const terraformPath = tasks.which(tool, true); 7 | return tasks.tool(terraformPath); 8 | } 9 | } 10 | 11 | export { setAnswers } from 'azure-pipelines-task-lib/mock-task'; -------------------------------------------------------------------------------- /tasks/terraform-cli/src/runners/terraform-error.ts: -------------------------------------------------------------------------------- 1 | export class TerraformError extends Error { 2 | constructor(name: string, message: string, stack?: string | undefined) { 3 | super(message); 4 | this.name = name; 5 | this.stack = stack; 6 | } 7 | } 8 | 9 | export class TerraformAggregateError extends Error { 10 | public readonly errors: TerraformError[]; 11 | public readonly stderr: string; 12 | public readonly exitCode: number; 13 | constructor(command: string, stderr: string, exitCode: number) { 14 | let stderrEscaped: string = stderr 15 | .replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, ''); 16 | let lines: string[] = stderrEscaped 17 | .split('\n\n') 18 | .map(line => line.replace('\nError:', 'Error:')) 19 | .map(line => line.replace('\n', ' ')) 20 | .filter(line => line && line !== ""); 21 | let message: string = lines 22 | .filter(line => line.startsWith('Error:')) 23 | .map(line => line.replace('Error:', '')) 24 | .join(" | "); 25 | super(message); 26 | this.stderr = stderrEscaped; 27 | this.name = `Terraform command '${command}' failed with exit code '${exitCode}'.`; 28 | this.errors = []; 29 | this.exitCode = exitCode; 30 | lines.forEach((line, i) => { 31 | if (line.startsWith('Error:')) { 32 | let name: string = line.replace('Error: ', ''); 33 | let message: string = lines[(i + 1)]; 34 | this.errors.push(new TerraformError(name, message, this.stack)); 35 | } 36 | }); 37 | } 38 | } -------------------------------------------------------------------------------- /tasks/terraform-cli/src/task-agent/index.ts: -------------------------------------------------------------------------------- 1 | export interface ITaskAgent { 2 | downloadSecureFile(secureFileId: string): Promise; 3 | attachNewPlanFile(workingDirectory: string, type: string, name: string, content: string): void; 4 | attachNewFile(workingDirectory: string, type: string, name: string, content: string): void; 5 | writeFile(workingDirectory: string, fileName: string, content: string): string; 6 | } 7 | 8 | export { default as AzdoTaskAgent } from './azdo-task-agent'; 9 | export { default as MockTaskAgent } from './mock-task-agent'; 10 | 11 | -------------------------------------------------------------------------------- /tasks/terraform-cli/src/task-agent/mock-task-agent.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import { ITaskAgent } from "."; 3 | import { v4 as uuidv4 } from 'uuid'; 4 | 5 | export default class TaskAgentMock implements ITaskAgent { 6 | attachNewPlanFile(workingDirectory: string, type: string, name: string, content: string): void { 7 | const fileName = `default_stage_default_job_${uuidv4()}`; 8 | const filePath = this.writeFile(workingDirectory, fileName, content); 9 | this.attachedFiles[name] = { type: type, path: filePath }; 10 | this.attachedTypes[type] = { [name]: filePath}; 11 | } 12 | public readonly attachedFiles: { [name: string]: { type: string, path: string } } = {}; 13 | public readonly attachedTypes: { [type: string]: { [ name: string ]: string } } = {} 14 | public readonly writtenFiles: { [path: string]: string } = {}; 15 | 16 | attachNewFile(workingDirectory: string, type: string, name: string, content: string): void { 17 | const filePath = this.writeFile(workingDirectory, name, content); 18 | this.attachedFiles[name] = { type: type, path: filePath }; 19 | this.attachedTypes[type] = { [name]: filePath}; 20 | } 21 | 22 | writeFile(workingDirectory: string, fileName: string, content: string): string { 23 | const filePath = path.join(workingDirectory, fileName); 24 | this.writtenFiles[filePath] = content; 25 | return filePath; 26 | } 27 | 28 | async downloadSecureFile(secureFileId: string): Promise { 29 | const secureFileEnv = `SECUREFILE_NAME_${secureFileId}`; 30 | const filePath = process.env[secureFileEnv]; 31 | 32 | if(!filePath){ 33 | throw `Secure file ${secureFileId} not found. Did you add 'inputSecureFile' into your scenario pipeline?` 34 | } 35 | 36 | return Promise.resolve(filePath); 37 | } 38 | } 39 | 40 | -------------------------------------------------------------------------------- /tasks/terraform-cli/src/tests/default.env: -------------------------------------------------------------------------------- 1 | TF_VAR_region="eastus" 2 | TF_VAR_app-short-name="tffoo" 3 | TF_VAR_env-short-name="dev" -------------------------------------------------------------------------------- /tasks/terraform-cli/src/tests/default.vars: -------------------------------------------------------------------------------- 1 | region="eastus" 2 | app-short-name="tffoo" 3 | env-short-name="dev" -------------------------------------------------------------------------------- /tasks/terraform-cli/src/tests/features/publish-plan-results/plan-output-no-changes.txt: -------------------------------------------------------------------------------- 1 | azurerm_resource_group.test_01: Refreshing state... [id=/subscriptions/00000000-aaaa-0000-0000-aaaaaaaaaaaa/resourceGroups/test-01-rg] 2 | azurerm_resource_group.test_02: Refreshing state... [id=/subscriptions/00000000-aaaa-0000-0000-aaaaaaaaaaaa/resourceGroups/test-02-rg] 3 | 4 | No changes. Infrastructure is up-to-date. 5 | 6 | This means that Terraform did not detect any differences between your 7 | configuration and real physical resources that exist. As a result, no 8 | actions need to be performed. 9 | -------------------------------------------------------------------------------- /tasks/terraform-cli/src/tests/features/publish-plan-results/plan-output-with-1-unchanged-1-to-add.txt: -------------------------------------------------------------------------------- 1 | azurerm_resource_group.test_01: Refreshing state... [id=/subscriptions/00000000-aaaa-0000-0000-aaaaaaaaaaaa/resourceGroups/test-01-rg] 2 | 3 | Terraform will perform the following actions: 4 | 5 |  # azurerm_resource_group.test_02 will be created 6 |  + resource "azurerm_resource_group" "test_02" { 7 | + id = (known after apply) 8 | + location = "eastus2" 9 | + name = "test-02-rg" 10 | } 11 | 12 | Plan: 1 to add, 0 to change, 0 to destroy. 13 |  14 | Changes to Outputs: 15 | + two = (known after apply) 16 | 17 | ------------------------------------------------------------------------ 18 | 19 | This plan was saved to: tfplan 20 | 21 | To perform exactly these actions, run the following command to apply: 22 | terraform apply "tfplan" 23 | -------------------------------------------------------------------------------- /tasks/terraform-cli/src/tests/features/publish-plan-results/plan-output-with-adds-destroys-and-updates.txt: -------------------------------------------------------------------------------- 1 | azurerm_resource_group.test_02: Refreshing state... [id=/subscriptions/00000000-aaaa-0000-0000-aaaaaaaaaaaa/resourceGroups/test-02-rg] 2 | azurerm_resource_group.test_01: Refreshing state... [id=/subscriptions/00000000-aaaa-0000-0000-aaaaaaaaaaaa/resourceGroups/test-01-rg] 3 | 4 | Terraform will perform the following actions: 5 | 6 |  # azurerm_resource_group.test_01 will be updated in-place 7 |  ~ resource "azurerm_resource_group" "test_01" { 8 | id = "/subscriptions/00000000-aaaa-0000-0000-aaaaaaaaaaaa/resourceGroups/test-01-rg" 9 | name = "test-01-rg" 10 | ~ tags = { 11 | ~ "test" = "test" -> "test1" 12 | } 13 | # (1 unchanged attribute hidden) 14 | } 15 | 16 |  # azurerm_resource_group.test_02 will be destroyed 17 |  - resource "azurerm_resource_group" "test_02" { 18 | - id = "/subscriptions/00000000-aaaa-0000-0000-aaaaaaaaaaaa/resourceGroups/test-02-rg" -> null 19 | - location = "eastus2" -> null 20 | - name = "test-02-rg" -> null 21 | - tags = {} -> null 22 | } 23 | 24 |  # azurerm_resource_group.test_03 will be created 25 |  + resource "azurerm_resource_group" "test_03" { 26 | + id = (known after apply) 27 | + location = "eastus2" 28 | + name = "test-03-rg" 29 | } 30 | 31 | Plan: 1 to add, 1 to change, 1 to destroy. 32 |  33 | Changes to Outputs: 34 | + three = (known after apply) 35 | - two = "/subscriptions/00000000-aaaa-0000-0000-aaaaaaaaaaaa/resourceGroups/test-02-rg" -> null 36 | 37 | ------------------------------------------------------------------------ 38 | 39 | This plan was saved to: tfplan 40 | 41 | To perform exactly these actions, run the following command to apply: 42 | terraform apply "tfplan" 43 | -------------------------------------------------------------------------------- /tasks/terraform-cli/src/tests/features/publish-plan-results/plan-output-with-more-than-nine-changes.txt: -------------------------------------------------------------------------------- 1 | azurerm_resource_group.test_02: Refreshing state... [id=/subscriptions/00000000-aaaa-0000-0000-aaaaaaaaaaaa/resourceGroups/test-02-rg] 2 | azurerm_resource_group.test_01: Refreshing state... [id=/subscriptions/00000000-aaaa-0000-0000-aaaaaaaaaaaa/resourceGroups/test-01-rg] 3 | 4 | Terraform will perform the following actions: 5 | 6 |  # azurerm_resource_group.test_01 will be updated in-place 7 |  ~ resource "azurerm_resource_group" "test_01" { 8 | id = "/subscriptions/00000000-aaaa-0000-0000-aaaaaaaaaaaa/resourceGroups/test-01-rg" 9 | name = "test-01-rg" 10 | ~ tags = { 11 | ~ "test" = "test" -> "test1" 12 | } 13 | # (1 unchanged attribute hidden) 14 | } 15 | 16 |  # azurerm_resource_group.test_02 will be destroyed 17 |  - resource "azurerm_resource_group" "test_02" { 18 | - id = "/subscriptions/00000000-aaaa-0000-0000-aaaaaaaaaaaa/resourceGroups/test-02-rg" -> null 19 | - location = "eastus2" -> null 20 | - name = "test-02-rg" -> null 21 | - tags = {} -> null 22 | } 23 | 24 |  # azurerm_resource_group.test_03 will be created 25 |  + resource "azurerm_resource_group" "test_03" { 26 | + id = (known after apply) 27 | + location = "eastus2" 28 | + name = "test-03-rg" 29 | } 30 | 31 | Plan: 152 to add, 83 to change, 13 to destroy. 32 |  33 | Changes to Outputs: 34 | + three = (known after apply) 35 | - two = "/subscriptions/00000000-aaaa-0000-0000-aaaaaaaaaaaa/resourceGroups/test-02-rg" -> null 36 | 37 | ------------------------------------------------------------------------ 38 | 39 | This plan was saved to: tfplan 40 | 41 | To perform exactly these actions, run the following command to apply: 42 | terraform apply "tfplan" 43 | -------------------------------------------------------------------------------- /tasks/terraform-cli/src/tests/features/publish-plan-results/plan-summary-no-changes.txt: -------------------------------------------------------------------------------- 1 | No changes. Infrastructure is up-to-date. 2 | 3 | This means that Terraform did not detect any differences between your 4 | configuration and real physical resources that exist. As a result, no 5 | actions need to be performed. 6 | -------------------------------------------------------------------------------- /tasks/terraform-cli/src/tests/features/publish-plan-results/plan-summary-with-1-unchanged-1-to-add.txt: -------------------------------------------------------------------------------- 1 | Terraform will perform the following actions: 2 | 3 |  # azurerm_resource_group.test_02 will be created 4 |  + resource "azurerm_resource_group" "test_02" { 5 | + id = (known after apply) 6 | + location = "eastus2" 7 | + name = "test-02-rg" 8 | } 9 | 10 | Plan: 1 to add, 0 to change, 0 to destroy. 11 |  12 | Changes to Outputs: 13 | + two = (known after apply) 14 | -------------------------------------------------------------------------------- /tasks/terraform-cli/src/tests/features/publish-plan-results/plan-summary-with-adds-destroys-and-updates.txt: -------------------------------------------------------------------------------- 1 | Terraform will perform the following actions: 2 | 3 |  # azurerm_resource_group.test_01 will be updated in-place 4 |  ~ resource "azurerm_resource_group" "test_01" { 5 | id = "/subscriptions/00000000-aaaa-0000-0000-aaaaaaaaaaaa/resourceGroups/test-01-rg" 6 | name = "test-01-rg" 7 | ~ tags = { 8 | ~ "test" = "test" -> "test1" 9 | } 10 | # (1 unchanged attribute hidden) 11 | } 12 | 13 |  # azurerm_resource_group.test_02 will be destroyed 14 |  - resource "azurerm_resource_group" "test_02" { 15 | - id = "/subscriptions/00000000-aaaa-0000-0000-aaaaaaaaaaaa/resourceGroups/test-02-rg" -> null 16 | - location = "eastus2" -> null 17 | - name = "test-02-rg" -> null 18 | - tags = {} -> null 19 | } 20 | 21 |  # azurerm_resource_group.test_03 will be created 22 |  + resource "azurerm_resource_group" "test_03" { 23 | + id = (known after apply) 24 | + location = "eastus2" 25 | + name = "test-03-rg" 26 | } 27 | 28 | Plan: 1 to add, 1 to change, 1 to destroy. 29 |  30 | Changes to Outputs: 31 | + three = (known after apply) 32 | - two = "/subscriptions/00000000-aaaa-0000-0000-aaaaaaaaaaaa/resourceGroups/test-02-rg" -> null 33 | -------------------------------------------------------------------------------- /tasks/terraform-cli/src/tests/features/publish-plan-results/plan-summary-with-more-than-nine-changes.txt: -------------------------------------------------------------------------------- 1 | Terraform will perform the following actions: 2 | 3 |  # azurerm_resource_group.test_01 will be updated in-place 4 |  ~ resource "azurerm_resource_group" "test_01" { 5 | id = "/subscriptions/00000000-aaaa-0000-0000-aaaaaaaaaaaa/resourceGroups/test-01-rg" 6 | name = "test-01-rg" 7 | ~ tags = { 8 | ~ "test" = "test" -> "test1" 9 | } 10 | # (1 unchanged attribute hidden) 11 | } 12 | 13 |  # azurerm_resource_group.test_02 will be destroyed 14 |  - resource "azurerm_resource_group" "test_02" { 15 | - id = "/subscriptions/00000000-aaaa-0000-0000-aaaaaaaaaaaa/resourceGroups/test-02-rg" -> null 16 | - location = "eastus2" -> null 17 | - name = "test-02-rg" -> null 18 | - tags = {} -> null 19 | } 20 | 21 |  # azurerm_resource_group.test_03 will be created 22 |  + resource "azurerm_resource_group" "test_03" { 23 | + id = (known after apply) 24 | + location = "eastus2" 25 | + name = "test-03-rg" 26 | } 27 | 28 | Plan: 152 to add, 83 to change, 13 to destroy. 29 |  30 | Changes to Outputs: 31 | + three = (known after apply) 32 | - two = "/subscriptions/00000000-aaaa-0000-0000-aaaaaaaaaaaa/resourceGroups/test-02-rg" -> null 33 | -------------------------------------------------------------------------------- /tasks/terraform-cli/src/tests/features/state/stdout-state-list-addressed.txt: -------------------------------------------------------------------------------- 1 | random_string.a 2 | random_string.b -------------------------------------------------------------------------------- /tasks/terraform-cli/src/tests/features/state/stdout-state-list-simple.txt: -------------------------------------------------------------------------------- 1 | random_string.a 2 | random_string.b 3 | random_string.c -------------------------------------------------------------------------------- /tasks/terraform-cli/src/tests/features/state/stdout-state-move-ok.txt: -------------------------------------------------------------------------------- 1 | Move "random_string.a" to "random_string.d" 2 | Successfully moved 1 object(s). -------------------------------------------------------------------------------- /tasks/terraform-cli/src/tests/features/state/stdout-state-remove.txt: -------------------------------------------------------------------------------- 1 | Removed random_string.a 2 | Removed random_string.b 3 | Successfully removed 2 resource instance(s). -------------------------------------------------------------------------------- /tasks/terraform-cli/src/tests/features/state/terraform.tfstate: -------------------------------------------------------------------------------- 1 | { 2 | "version": 4, 3 | "terraform_version": "1.1.2", 4 | "serial": 1, 5 | "lineage": "1796e7e5-750a-63f9-c070-7d94e5d7e7b8", 6 | "outputs": {}, 7 | "resources": [ 8 | { 9 | "mode": "managed", 10 | "type": "random_string", 11 | "name": "a", 12 | "provider": "provider[\"registry.terraform.io/hashicorp/random\"]", 13 | "instances": [ 14 | { 15 | "schema_version": 1, 16 | "attributes": { 17 | "id": "@@c%loC0YU", 18 | "keepers": null, 19 | "length": 10, 20 | "lower": true, 21 | "min_lower": 0, 22 | "min_numeric": 0, 23 | "min_special": 0, 24 | "min_upper": 0, 25 | "number": true, 26 | "override_special": null, 27 | "result": "@@c%loC0YU", 28 | "special": true, 29 | "upper": true 30 | }, 31 | "sensitive_attributes": [], 32 | "private": "eyJzY2hlbWFfdmVyc2lvbiI6IjEifQ==" 33 | } 34 | ] 35 | }, 36 | { 37 | "mode": "managed", 38 | "type": "random_string", 39 | "name": "b", 40 | "provider": "provider[\"registry.terraform.io/hashicorp/random\"]", 41 | "instances": [ 42 | { 43 | "schema_version": 1, 44 | "attributes": { 45 | "id": "]@nX(cu6)r", 46 | "keepers": null, 47 | "length": 10, 48 | "lower": true, 49 | "min_lower": 0, 50 | "min_numeric": 0, 51 | "min_special": 0, 52 | "min_upper": 0, 53 | "number": true, 54 | "override_special": null, 55 | "result": "]@nX(cu6)r", 56 | "special": true, 57 | "upper": true 58 | }, 59 | "sensitive_attributes": [], 60 | "private": "eyJzY2hlbWFfdmVyc2lvbiI6IjEifQ==" 61 | } 62 | ] 63 | }, 64 | { 65 | "mode": "managed", 66 | "type": "random_string", 67 | "name": "c", 68 | "provider": "provider[\"registry.terraform.io/hashicorp/random\"]", 69 | "instances": [ 70 | { 71 | "schema_version": 1, 72 | "attributes": { 73 | "id": "(2O@v}sBJI", 74 | "keepers": null, 75 | "length": 10, 76 | "lower": true, 77 | "min_lower": 0, 78 | "min_numeric": 0, 79 | "min_special": 0, 80 | "min_upper": 0, 81 | "number": true, 82 | "override_special": null, 83 | "result": "(2O@v}sBJI", 84 | "special": true, 85 | "upper": true 86 | }, 87 | "sensitive_attributes": [], 88 | "private": "eyJzY2hlbWFfdmVyc2lvbiI6IjEifQ==" 89 | } 90 | ] 91 | } 92 | ] 93 | } 94 | -------------------------------------------------------------------------------- /tasks/terraform-cli/src/tests/features/terraform-apply-aws.feature: -------------------------------------------------------------------------------- 1 | Feature: terraform apply for aws 2 | 3 | terraform apply [options] [dir] 4 | 5 | Scenario: apply without aws service connection 6 | Given terraform exists 7 | And terraform command is "apply" 8 | And running command "terraform apply -auto-approve" returns successful result 9 | When the terraform cli task is run 10 | Then the terraform cli task executed command "terraform apply -auto-approve" without the following environment variables 11 | | AWS_ACCESS_KEY_ID | 12 | | AWS_SECRET_ACCESS_KEY | 13 | | AWS_REGION | 14 | And the terraform cli task is successful 15 | And pipeline variable "TERRAFORM_LAST_EXITCODE" is set to "0" 16 | 17 | Scenario: apply with aws 18 | Given terraform exists 19 | And terraform command is "apply" 20 | And aws provider service connection "env_test_aws" exists as 21 | | username | foo | 22 | | password | bar | 23 | And aws provider region is configured as "us-east-1" 24 | And running command "terraform apply -auto-approve" returns successful result 25 | When the terraform cli task is run 26 | Then the terraform cli task executed command "terraform apply -auto-approve" with the following environment variables 27 | | AWS_ACCESS_KEY_ID | foo | 28 | | AWS_SECRET_ACCESS_KEY | bar | 29 | | AWS_REGION | us-east-1 | 30 | And the terraform cli task is successful 31 | And pipeline variable "TERRAFORM_LAST_EXITCODE" is set to "0" 32 | -------------------------------------------------------------------------------- /tasks/terraform-cli/src/tests/features/terraform-destroy-aws.feature: -------------------------------------------------------------------------------- 1 | Feature: terraform destroy for aws 2 | 3 | terraform destroy [options] [dir] 4 | 5 | Scenario: destroy without aws service connection 6 | Given terraform exists 7 | And terraform command is "destroy" 8 | And running command "terraform destroy -auto-approve" returns successful result 9 | When the terraform cli task is run 10 | Then the terraform cli task executed command "terraform destroy -auto-approve" without the following environment variables 11 | | AWS_ACCESS_KEY_ID | 12 | | AWS_SECRET_ACCESS_KEY | 13 | | AWS_REGION | 14 | And the terraform cli task is successful 15 | And pipeline variable "TERRAFORM_LAST_EXITCODE" is set to "0" 16 | 17 | Scenario: destroy with aws 18 | Given terraform exists 19 | And terraform command is "destroy" 20 | And aws provider service connection "env_test_aws" exists as 21 | | username | foo | 22 | | password | bar | 23 | And aws provider region is configured as "us-east-1" 24 | And running command "terraform destroy -auto-approve" returns successful result 25 | When the terraform cli task is run 26 | Then the terraform cli task executed command "terraform destroy -auto-approve" with the following environment variables 27 | | AWS_ACCESS_KEY_ID | foo | 28 | | AWS_SECRET_ACCESS_KEY | bar | 29 | | AWS_REGION | us-east-1 | 30 | And the terraform cli task is successful 31 | And pipeline variable "TERRAFORM_LAST_EXITCODE" is set to "0" 32 | -------------------------------------------------------------------------------- /tasks/terraform-cli/src/tests/features/terraform-fmt.feature: -------------------------------------------------------------------------------- 1 | Feature: terraform fmt 2 | 3 | terraform fmt [options] [dir] 4 | 5 | Scenario: fmt without command options 6 | Given terraform exists 7 | And terraform command is "fmt" 8 | And running command "terraform fmt --check --diff --recursive" returns successful result 9 | When the terraform cli task is run 10 | Then the terraform cli task executed command "terraform fmt --check --diff --recursive" 11 | And the terraform cli task is successful 12 | And pipeline variable "TERRAFORM_LAST_EXITCODE" is set to "0" 13 | 14 | Scenario: fmt with command options 15 | Given terraform exists 16 | And terraform command is "fmt" with options "--diff -list=false" 17 | And running command "terraform fmt --check --recursive --diff -list=false" returns successful result 18 | When the terraform cli task is run 19 | Then the terraform cli task executed command "terraform fmt --check --recursive --diff -list=false" 20 | And the terraform cli task is successful 21 | And pipeline variable "TERRAFORM_LAST_EXITCODE" is set to "0" 22 | 23 | Scenario: fmt violation fails task 24 | Given terraform exists 25 | And terraform command is "fmt" 26 | And running command "terraform fmt --check --diff --recursive" fails with error "invalid fmt" 27 | When the terraform cli task is run 28 | Then the terraform cli task executed command "terraform fmt --check --diff --recursive" 29 | And the terraform cli task fails with message "invalid fmt" 30 | And pipeline variable "TERRAFORM_LAST_EXITCODE" is set to "1" -------------------------------------------------------------------------------- /tasks/terraform-cli/src/tests/features/terraform-force-unlock-aws.feature: -------------------------------------------------------------------------------- 1 | Feature: terraform force-unlock for aws 2 | 3 | terraform force-unlock [options] [lockID] 4 | 5 | Scenario: force-unlock with aws 6 | Given terraform exists 7 | And terraform command is "forceunlock" 8 | And aws provider service connection "env_test_aws" exists as 9 | | username | foo | 10 | | password | bar | 11 | And aws provider region is configured as "us-east-1" 12 | And force-unlock is run with lock id "3ea12870-968e-b9b9-cf3b-f4c3fbe36684" 13 | And running command "terraform force-unlock -force 3ea12870-968e-b9b9-cf3b-f4c3fbe36684" returns successful result 14 | When the terraform cli task is run 15 | Then the terraform cli task executed command "terraform force-unlock -force 3ea12870-968e-b9b9-cf3b-f4c3fbe36684" with the following environment variables 16 | | AWS_ACCESS_KEY_ID | foo | 17 | | AWS_SECRET_ACCESS_KEY | bar | 18 | | AWS_REGION | us-east-1 | 19 | And the terraform cli task is successful 20 | And pipeline variable "TERRAFORM_LAST_EXITCODE" is set to "0" 21 | -------------------------------------------------------------------------------- /tasks/terraform-cli/src/tests/features/terraform-import-aws.feature: -------------------------------------------------------------------------------- 1 | @tf-import 2 | Feature: terraform import for aws 3 | 4 | terraform import [options] [resource address] [resource id] 5 | 6 | Scenario: import without aws service connection 7 | Given terraform exists 8 | And terraform command is "import" 9 | And resource target provided with address "azurerm_resource_group.rg" and id "/subscriptions/sub1/resourceGroups/rg-tffoo-dev-eastus" 10 | And running command "terraform import azurerm_resource_group.rg /subscriptions/sub1/resourceGroups/rg-tffoo-dev-eastus" returns successful result 11 | When the terraform cli task is run 12 | Then the terraform cli task executed command "terraform import azurerm_resource_group.rg /subscriptions/sub1/resourceGroups/rg-tffoo-dev-eastus" without the following environment variables 13 | | AWS_ACCESS_KEY_ID | 14 | | AWS_SECRET_ACCESS_KEY | 15 | | AWS_REGION | 16 | And the terraform cli task is successful 17 | And pipeline variable "TERRAFORM_LAST_EXITCODE" is set to "0" 18 | 19 | Scenario: import with aws 20 | Given terraform exists 21 | And terraform command is "import" 22 | And aws provider service connection "env_test_aws" exists as 23 | | username | foo | 24 | | password | bar | 25 | And aws provider region is configured as "us-east-1" 26 | And resource target provided with address "azurerm_resource_group.rg" and id "/subscriptions/sub1/resourceGroups/rg-tffoo-dev-eastus" 27 | And running command "terraform import azurerm_resource_group.rg /subscriptions/sub1/resourceGroups/rg-tffoo-dev-eastus" returns successful result 28 | When the terraform cli task is run 29 | Then the terraform cli task executed command "terraform import azurerm_resource_group.rg /subscriptions/sub1/resourceGroups/rg-tffoo-dev-eastus" with the following environment variables 30 | | AWS_ACCESS_KEY_ID | foo | 31 | | AWS_SECRET_ACCESS_KEY | bar | 32 | | AWS_REGION | us-east-1 | 33 | And the terraform cli task is successful 34 | And pipeline variable "TERRAFORM_LAST_EXITCODE" is set to "0" 35 | -------------------------------------------------------------------------------- /tasks/terraform-cli/src/tests/features/terraform-init-gcp.feature: -------------------------------------------------------------------------------- 1 | Feature: terraform init on gcp 2 | 3 | terraform init [options for aws] [dir] 4 | 5 | Scenario: init with gcs backend all parameters 6 | Given terraform exists 7 | And terraform command is "init" 8 | And running command "terraform version" returns successful result with stdout "Terraform v1.0.11\non windows_amd64\n" 9 | And gcs backend type selected with the following bucket 10 | | bucket | gcstrfrmeusczp | 11 | | prefix | azure-pipelines-terraform/infrax | 12 | And gcs backend credential file specified with id "2ab84611-6012-46cf-921f-7f7fb7ee27cd" and name "./src/tests/gcp-fake-key.json" 13 | And running command "terraform init" with the following options returns successful result 14 | | option | 15 | | -backend-config=bucket=gcstrfrmeusczp | 16 | | -backend-config=prefix=azure-pipelines-terraform/infrax | 17 | | -backend-config=credentials=./src/tests/gcp-fake-key.json | 18 | When the terraform cli task is run 19 | Then terraform is initialized with the following options 20 | | option | 21 | | -backend-config=bucket=gcstrfrmeusczp | 22 | | -backend-config=prefix=azure-pipelines-terraform/infrax | 23 | | -backend-config=credentials=./src/tests/gcp-fake-key.json | 24 | And the terraform cli task is successful 25 | And pipeline variable "TERRAFORM_LAST_EXITCODE" is set to "0" -------------------------------------------------------------------------------- /tasks/terraform-cli/src/tests/features/terraform-output/console_tf_output_all_types.txt: -------------------------------------------------------------------------------- 1 | ##vso[task.debug]looking up mock answers for "which", key '"terraform"' 2 | ##vso[task.debug]found mock response 3 | ##vso[task.debug]check path : terraform 4 | ##vso[task.debug]looking up mock answers for "checkPath", key '"terraform"' 5 | ##vso[task.debug]found mock response 6 | ##vso[task.debug]terraform arg: version 7 | ##vso[task.debug]exec tool: terraform 8 | ##vso[task.debug]Arguments: 9 | ##vso[task.debug] version 10 | ##vso[task.debug]ignoreTempPath=undefined 11 | ##vso[task.debug]tempPath=undefined 12 | [command]terraform version 13 | version successful 14 | rc:0 15 | success:true 16 | ##vso[task.debug]looking up mock answers for "which", key '"terraform"' 17 | ##vso[task.debug]found mock response 18 | ##vso[task.debug]check path : terraform 19 | ##vso[task.debug]looking up mock answers for "checkPath", key '"terraform"' 20 | ##vso[task.debug]found mock response 21 | ##vso[task.debug]terraform arg: output 22 | ##vso[task.debug]terraform arg: -json 23 | ##vso[task.debug]exec tool: terraform 24 | ##vso[task.debug]Arguments: 25 | ##vso[task.debug] output 26 | ##vso[task.debug] -json 27 | ##vso[task.debug]ignoreTempPath=undefined 28 | ##vso[task.debug]tempPath=undefined 29 | TF_OUT_SOME_BOOL = true 30 | TF_OUT_SOME_STRING = some-string-value 31 | TF_OUT_SOME_SECRET_STRING = ********* (sensitive) 32 | TF_OUT_SOME_NUMBER = 1 33 | ##vso[task.issue type=warning;]Currently only keys of type "string", "number", and "bool" will returned. The key "some_tuple" is not supported! 34 | ##vso[task.issue type=warning;]Currently only keys of type "string", "number", and "bool" will returned. The key "some_map" is not supported! 35 | ##vso[task.debug]executed command 'output' 36 | -------------------------------------------------------------------------------- /tasks/terraform-cli/src/tests/features/terraform-output/console_tf_output_string.txt: -------------------------------------------------------------------------------- 1 | ##vso[task.debug]looking up mock answers for "which", key '"terraform"' 2 | ##vso[task.debug]found mock response 3 | ##vso[task.debug]check path : terraform 4 | ##vso[task.debug]looking up mock answers for "checkPath", key '"terraform"' 5 | ##vso[task.debug]found mock response 6 | ##vso[task.debug]terraform arg: version 7 | ##vso[task.debug]exec tool: terraform 8 | ##vso[task.debug]Arguments: 9 | ##vso[task.debug] version 10 | ##vso[task.debug]ignoreTempPath=undefined 11 | ##vso[task.debug]tempPath=undefined 12 | [command]terraform version 13 | version successful 14 | rc:0 15 | success:true 16 | ##vso[task.debug]looking up mock answers for "which", key '"terraform"' 17 | ##vso[task.debug]found mock response 18 | ##vso[task.debug]check path : terraform 19 | ##vso[task.debug]looking up mock answers for "checkPath", key '"terraform"' 20 | ##vso[task.debug]found mock response 21 | ##vso[task.debug]terraform arg: output 22 | ##vso[task.debug]terraform arg: -json 23 | ##vso[task.debug]terraform arg: -no-color 24 | ##vso[task.debug]exec tool: terraform 25 | ##vso[task.debug]Arguments: 26 | ##vso[task.debug] output 27 | ##vso[task.debug] -json 28 | ##vso[task.debug] -no-color 29 | ##vso[task.debug]ignoreTempPath=undefined 30 | ##vso[task.debug]tempPath=undefined 31 | TF_OUT_SOME_STRING = some-string-value 32 | ##vso[task.debug]executed command 'output' 33 | -------------------------------------------------------------------------------- /tasks/terraform-cli/src/tests/features/terraform-output/stdout_tf_output_all_types.json: -------------------------------------------------------------------------------- 1 | { 2 | "some_bool": { 3 | "sensitive": false, 4 | "type": "bool", 5 | "value": true 6 | }, 7 | "some_string": { 8 | "sensitive": false, 9 | "type": "string", 10 | "value": "some-string-value" 11 | }, 12 | "some_secret_string": { 13 | "sensitive": true, 14 | "type": "string", 15 | "value": "some-secret-string-value" 16 | }, 17 | "some_number": { 18 | "sensitive": false, 19 | "type": "number", 20 | "value": 1 21 | }, 22 | "some_tuple": { 23 | "sensitive": false, 24 | "type": [ "tuple", [ "string", "number", "string" ]], 25 | "value": [ "1", 2, "3" ] 26 | }, 27 | "some_map": { 28 | "sensitive": false, 29 | "type": [ "object", { "A": "number", "B": "number", "C": "number" }], 30 | "value": { 31 | "A": 1, 32 | "B": 2, 33 | "C": 3 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tasks/terraform-cli/src/tests/features/terraform-output/stdout_tf_output_string.json: -------------------------------------------------------------------------------- 1 | { 2 | "some_string": { 3 | "sensitive": false, 4 | "type": "string", 5 | "value": "some-string-value" 6 | } 7 | } -------------------------------------------------------------------------------- /tasks/terraform-cli/src/tests/features/terraform-output/terraform-output.feature: -------------------------------------------------------------------------------- 1 | Feature: terraform output 2 | 3 | terraform output [options] [name] 4 | 5 | Scenario: output with variables defined 6 | Given terraform exists 7 | And terraform command is "output" 8 | And running command "terraform output -json" returns successful result with stdout from file "./src/tests/features/terraform-output/stdout_tf_output_all_types.json" 9 | When the terraform cli task is run 10 | Then the terraform cli task executed command "terraform output -json" 11 | And the terraform cli task is successful 12 | And pipeline variable "TERRAFORM_LAST_EXITCODE" is set to "0" 13 | And pipeline variable "TF_OUT_SOME_BOOL" is set to "true" 14 | And pipeline variable "TF_OUT_SOME_STRING" is set to "some-string-value" 15 | And pipeline secret "TF_OUT_SOME_SECRET_STRING" is set to "some-secret-string-value" 16 | And pipeline variable "TF_OUT_SOME_NUMBER" is set to "1" 17 | 18 | Scenario: output with no variables defined 19 | Given terraform exists 20 | And terraform command is "output" 21 | And running command "terraform output -json" returns successful result with no stdout 22 | When the terraform cli task is run 23 | Then the terraform cli task executed command "terraform output -json" 24 | And the terraform cli task is successful 25 | And pipeline variable "TERRAFORM_LAST_EXITCODE" is set to "0" 26 | And no pipeline variables starting with "TF_OUT" are set 27 | 28 | Scenario: output with json flag defined 29 | Given terraform exists 30 | And terraform command is "output" with options "-json -no-color" 31 | And running command "terraform output -json -no-color" returns successful result with stdout from file "./src/tests/features/terraform-output/stdout_tf_output_string.json" 32 | When the terraform cli task is run 33 | Then the terraform cli task executed command "terraform output -json -no-color" 34 | And the terraform cli task is successful 35 | And pipeline variable "TERRAFORM_LAST_EXITCODE" is set to "0" 36 | And pipeline variable "TF_OUT_SOME_STRING" is set to "some-string-value" 37 | -------------------------------------------------------------------------------- /tasks/terraform-cli/src/tests/features/terraform-plan-aws.feature: -------------------------------------------------------------------------------- 1 | Feature: terraform plan for aws 2 | 3 | terraform plan [options] [dir] 4 | 5 | Scenario: plan without aws service connection 6 | Given terraform exists 7 | And terraform command is "plan" 8 | And running command "terraform plan" returns successful result 9 | When the terraform cli task is run 10 | Then the terraform cli task executed command "terraform plan" without the following environment variables 11 | | AWS_ACCESS_KEY_ID | 12 | | AWS_SECRET_ACCESS_KEY | 13 | | AWS_REGION | 14 | And the terraform cli task is successful 15 | And pipeline variable "TERRAFORM_LAST_EXITCODE" is set to "0" 16 | 17 | Scenario: plan with aws 18 | Given terraform exists 19 | And terraform command is "plan" 20 | And aws provider service connection "env_test_aws" exists as 21 | | username | foo | 22 | | password | bar | 23 | And aws provider region is configured as "us-east-1" 24 | And running command "terraform plan" returns successful result 25 | When the terraform cli task is run 26 | Then the terraform cli task executed command "terraform plan" with the following environment variables 27 | | AWS_ACCESS_KEY_ID | foo | 28 | | AWS_SECRET_ACCESS_KEY | bar | 29 | | AWS_REGION | us-east-1 | 30 | And the terraform cli task is successful 31 | And pipeline variable "TERRAFORM_LAST_EXITCODE" is set to "0" 32 | -------------------------------------------------------------------------------- /tasks/terraform-cli/src/tests/features/terraform-refresh-aws.feature: -------------------------------------------------------------------------------- 1 | Feature: terraform refresh for aws 2 | 3 | terraform refresh [options] [dir] 4 | 5 | Scenario: refresh without aws service connection 6 | Given terraform exists 7 | And terraform command is "refresh" 8 | And running command "terraform refresh" returns successful result 9 | When the terraform cli task is run 10 | Then the terraform cli task executed command "terraform refresh" without the following environment variables 11 | | AWS_ACCESS_KEY_ID | 12 | | AWS_SECRET_ACCESS_KEY | 13 | | AWS_REGION | 14 | And the terraform cli task is successful 15 | And pipeline variable "TERRAFORM_LAST_EXITCODE" is set to "0" 16 | 17 | Scenario: refresh with aws 18 | Given terraform exists 19 | And terraform command is "refresh" 20 | And aws provider service connection "env_test_aws" exists as 21 | | username | foo | 22 | | password | bar | 23 | And aws provider region is configured as "us-east-1" 24 | And running command "terraform refresh" returns successful result 25 | When the terraform cli task is run 26 | Then the terraform cli task executed command "terraform refresh" with the following environment variables 27 | | AWS_ACCESS_KEY_ID | foo | 28 | | AWS_SECRET_ACCESS_KEY | bar | 29 | | AWS_REGION | us-east-1 | 30 | And the terraform cli task is successful 31 | And pipeline variable "TERRAFORM_LAST_EXITCODE" is set to "0" 32 | -------------------------------------------------------------------------------- /tasks/terraform-cli/src/tests/features/terraform.feature: -------------------------------------------------------------------------------- 1 | Feature: Terraform 2 | 3 | terraform 4 | 5 | Scenario: terraform not exists 6 | Given terraform not exists 7 | And terraform command is "version" 8 | When the terraform cli task is run 9 | Then the terraform cli task fails with message "Error: Not found terraform" 10 | 11 | Scenario: does not fail when stderr exists and exit code is 0 12 | Given terraform exists 13 | And terraform command is "version" 14 | And running command "terraform version" returns the following result 15 | | code | 0 | 16 | | stdout | success with warnings | 17 | | stderr | some warning message | 18 | When the terraform cli task is run 19 | Then the terraform cli task executed command "terraform version" 20 | And the terraform cli task is successful 21 | -------------------------------------------------------------------------------- /tasks/terraform-cli/src/tests/features/version/stdout_version_0_11_14.txt: -------------------------------------------------------------------------------- 1 | Terraform v0.11.14 2 | 3 | Your version of Terraform is out of date! The latest version 4 | is 0.15.3. You can update by downloading from www.terraform.io/downloads.html -------------------------------------------------------------------------------- /tasks/terraform-cli/src/tests/features/version/stdout_version_0_14_10.txt: -------------------------------------------------------------------------------- 1 | Terraform v0.14.10 2 | + provider registry.terraform.io/hashicorp/azurerm v2.59.0 3 | 4 | Your version of Terraform is out of date! The latest version 5 | is 0.15.3. You can update by downloading from https://www.terraform.io/downloads.html -------------------------------------------------------------------------------- /tasks/terraform-cli/src/tests/features/version/stdout_version_0_15_3.txt: -------------------------------------------------------------------------------- 1 | Terraform v0.15.3 2 | on windows_amd64 3 | + provider registry.terraform.io/hashicorp/azurerm v2.59.0 -------------------------------------------------------------------------------- /tasks/terraform-cli/src/tests/features/version/terraform-version.feature: -------------------------------------------------------------------------------- 1 | Feature: terraform version 2 | 3 | terraform version 4 | 5 | Scenario: terraform version 0.14.4 6 | Given terraform exists 7 | And terraform command is "version" 8 | And running command "terraform version" returns successful result with stdout from file "./src/tests/features/version/stdout_version_0_14_10.txt" 9 | When the terraform cli task is run 10 | Then the terraform cli task executed command "terraform version" 11 | And the terraform cli task is successful 12 | And the resolved terraform version is 13 | | full | 0.14.10 | 14 | | major | 0 | 15 | | minor | 14 | 16 | | patch | 10 | 17 | And the following warnings are logged 18 | | Your version of Terraform is out of date! The latest version is 0.15.3. | 19 | 20 | Scenario: terraform version 0.11.14 21 | Given terraform exists 22 | And terraform command is "version" 23 | And running command "terraform version" returns successful result with stdout from file "./src/tests/features/version/stdout_version_0_11_14.txt" 24 | When the terraform cli task is run 25 | Then the terraform cli task executed command "terraform version" 26 | And the terraform cli task is successful 27 | And the resolved terraform version is 28 | | full | 0.11.14 | 29 | | major | 0 | 30 | | minor | 11 | 31 | | patch | 14 | 32 | And the following warnings are logged 33 | | Your version of Terraform is out of date! The latest version is 0.15.3. | 34 | 35 | Scenario: terraform version 0.15.0 36 | Given terraform exists 37 | And terraform command is "version" 38 | And running command "terraform version" returns successful result with stdout from file "./src/tests/features/version/stdout_version_0_15_3.txt" 39 | When the terraform cli task is run 40 | Then the terraform cli task executed command "terraform version" 41 | And the terraform cli task is successful 42 | And the resolved terraform version is 43 | | full | 0.15.3 | 44 | | major | 0 | 45 | | minor | 15 | 46 | | patch | 3 | 47 | And the following warnings are not logged 48 | | Your version of Terraform is out of date! The latest version is 0.15.3. | -------------------------------------------------------------------------------- /tasks/terraform-cli/src/tests/features/workspace/stdout-new-workspace-bar-skipExisting.txt: -------------------------------------------------------------------------------- 1 | \u001b[31mWorkspace \"bar\" already exists\u001b[0m\u001b[0m\n -------------------------------------------------------------------------------- /tasks/terraform-cli/src/tests/features/workspace/stdout-new-workspace-bar.txt: -------------------------------------------------------------------------------- 1 | \u001b[0m\u001b[32m\u001b[1mCreated and switched to workspace \"bar\"!\u001b[0m\u001b[32m\n\nYou're now on a new, empty workspace. Workspaces isolate their state,\nso if you run \"terraform plan\" Terraform will not see any existing state\nfor this configuration.\u001b[0m\n -------------------------------------------------------------------------------- /tasks/terraform-cli/src/tests/features/workspace/terraform-workspace-new.feature: -------------------------------------------------------------------------------- 1 | Feature: terraform workspace new 2 | 3 | terraform workspace new [OPTIONS] [name] [DIR] 4 | 5 | Scenario: new workspace 6 | Given terraform exists 7 | And terraform command is "workspace" 8 | And workspace command is "new" with name "bar" 9 | And running command "terraform workspace new bar" returns successful result with stdout from file "./src/tests/features/workspace/stdout-new-workspace-bar.txt" 10 | When the terraform cli task is run 11 | Then the terraform cli task executed command "terraform workspace new bar" 12 | And the terraform cli task is successful 13 | 14 | Scenario: new workspace with command options 15 | Given terraform exists 16 | And terraform command is "workspace" with options "-state=terraform.tfstate" 17 | And workspace command is "new" with name "bar" 18 | And running command "terraform workspace new -state=terraform.tfstate bar" returns successful result with stdout from file "./src/tests/features/workspace/stdout-new-workspace-bar.txt" 19 | When the terraform cli task is run 20 | Then the terraform cli task executed command "terraform workspace new -state=terraform.tfstate bar" 21 | And the terraform cli task is successful 22 | 23 | Scenario: new workspace already exists 24 | Given terraform exists 25 | And terraform command is "workspace" 26 | And workspace command is "new" with name "bar" 27 | And running command "terraform workspace new bar" fails with error "\u001b[31mWorkspace \"bar\" already exists\u001b[0m\u001b[0m\n" 28 | When the terraform cli task is run 29 | Then the terraform cli task executed command "terraform workspace new bar" 30 | And the terraform cli task fails with message "\u001b[31mWorkspace \"bar\" already exists\u001b[0m\u001b[0m\n" 31 | 32 | Scenario: new workspace already exists but it's allowed 33 | Given terraform exists 34 | And terraform command is "workspace" 35 | And workspace command is "new" with name "bar" and command is set to succeed if existing 36 | And running command "terraform workspace new bar" returns successful result with stdout from file "./src/tests/features/workspace/stdout-new-workspace-bar-skipExisting.txt" 37 | When the terraform cli task is run 38 | Then the terraform cli task executed command "terraform workspace new bar" 39 | And the terraform cli task is successful 40 | -------------------------------------------------------------------------------- /tasks/terraform-cli/src/tests/features/workspace/terraform-workspace-select.feature: -------------------------------------------------------------------------------- 1 | Feature: terraform workspace select 2 | 3 | terraform workspace select [name] [DIR] 4 | 5 | Scenario: switch to existing workspace 6 | Given terraform exists 7 | And terraform command is "workspace" 8 | And workspace command is "select" with name "foo" 9 | And running command "terraform workspace select foo" returns successful result with stdout "Switched to workspace \"foo\"." 10 | When the terraform cli task is run 11 | Then the terraform cli task executed command "terraform workspace select foo" 12 | And the terraform cli task is successful 13 | 14 | Scenario: switch to workspace that doesnt exist 15 | Given terraform exists 16 | And terraform command is "workspace" 17 | And workspace command is "select" with name "bar" 18 | And running command "terraform workspace select bar" fails with error "Workspace \"bar\" doesn't exist. \n\rYou can create this workspace with the \"new\" subcommand" 19 | When the terraform cli task is run 20 | Then the terraform cli task executed command "terraform workspace select bar" 21 | And the terraform cli task fails with message "Workspace \"bar\" doesn't exist. \n\rYou can create this workspace with the \"new\" subcommand" 22 | 23 | Scenario: select current workspace 24 | Given terraform exists 25 | And terraform command is "workspace" 26 | And workspace command is "select" with name "foo" 27 | And running command "terraform workspace select foo" returns successful result with no stdout 28 | When the terraform cli task is run 29 | Then the terraform cli task executed command "terraform workspace select foo" 30 | And the terraform cli task is successful 31 | 32 | 33 | -------------------------------------------------------------------------------- /tasks/terraform-cli/src/tests/gcp-fake-key.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "service_account", 3 | "project_id": "azure-pipelines-terraform", 4 | "private_key_id": "foo", 5 | "private_key": "bar", 6 | "client_email": "foo@bar.iam.gserviceaccount.com", 7 | "client_id": "12345", 8 | "auth_uri": "https://accounts.google.com/o/oauth2/auth", 9 | "token_uri": "https://oauth2.googleapis.com/token", 10 | "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", 11 | "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/foo%40bar.iam.gserviceaccount.com" 12 | } -------------------------------------------------------------------------------- /tasks/terraform-cli/src/tests/stdout_tf_show_tfplan_no_destroy.json: -------------------------------------------------------------------------------- 1 | { 2 | "resource_changes": [ 3 | { 4 | "address": "azurerm_resource_group.rg", 5 | "mode": "managed", 6 | "type": "azurerm_resource_group", 7 | "name": "rg", 8 | "provider_name": "azurerm", 9 | "change": { 10 | "actions": [ 11 | "create" 12 | ], 13 | "before": null, 14 | "after": { 15 | "location": "", 16 | "name": "rg---" 17 | }, 18 | "after_unknown": { 19 | "id": true, 20 | "tags": true 21 | } 22 | } 23 | } 24 | ] 25 | } -------------------------------------------------------------------------------- /tasks/terraform-cli/src/tests/stdout_tf_show_tfplan_output_only.json: -------------------------------------------------------------------------------- 1 | { 2 | "format_version": "0.1", 3 | "terraform_version": "0.13.4", 4 | "planned_values": { 5 | "outputs": { 6 | "some_string": { 7 | "sensitive": false, 8 | "value": "NOT_EXISTS" 9 | } 10 | }, 11 | "root_module": {} 12 | }, 13 | "output_changes": { 14 | "some_string": { 15 | "actions": [ 16 | "create" 17 | ], 18 | "before": null, 19 | "after": "NOT_EXISTS", 20 | "after_unknown": false 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tasks/terraform-cli/src/tests/stdout_tf_show_tfplan_with_destroy.json: -------------------------------------------------------------------------------- 1 | { 2 | "resource_changes": [ 3 | { 4 | "address": "azurerm_resource_group.rg", 5 | "mode": "managed", 6 | "type": "azurerm_resource_group", 7 | "name": "rg", 8 | "provider_name": "azurerm", 9 | "change": { 10 | "actions": [ 11 | "delete" 12 | ], 13 | "before": null, 14 | "after": { 15 | "location": "", 16 | "name": "rg---" 17 | }, 18 | "after_unknown": { 19 | "id": true, 20 | "tags": true 21 | } 22 | } 23 | } 24 | ] 25 | } -------------------------------------------------------------------------------- /tasks/terraform-cli/src/tests/stdout_tf_show_tfplan_with_destroy_eol.json: -------------------------------------------------------------------------------- 1 | { 2 | "resource_changes": [ 3 | { 4 | "address": "azurerm_resource_group.rg", 5 | "mode": "managed", 6 | "type": "azurerm_resource_group", 7 | "name": "rg", 8 | "provider_name": "azurerm", 9 | "change": { 10 | "actions": [ 11 | "delete" 12 | ], 13 | "before": null, 14 | "after": { 15 | "location": "", 16 | "name": "rg---" 17 | }, 18 | "after_unknown": { 19 | "id": true, 20 | "tags": true 21 | } 22 | } 23 | } 24 | ] 25 | }\n -------------------------------------------------------------------------------- /tasks/terraform-cli/src/tests/stdout_tf_show_tfstate_version_only.json: -------------------------------------------------------------------------------- 1 | {"format_version":"0.112"} -------------------------------------------------------------------------------- /tasks/terraform-cli/src/tests/steps/mock-answer-spy.ts: -------------------------------------------------------------------------------- 1 | import * as ma from 'azure-pipelines-task-lib/mock-answer' 2 | 3 | export { TaskLibAnswerExecResult } from 'azure-pipelines-task-lib/mock-answer' 4 | export { TaskLibAnswers } from 'azure-pipelines-task-lib/mock-answer' 5 | export { MockedCommand } from 'azure-pipelines-task-lib/mock-answer' 6 | 7 | export const requestedAnswers: { [key: string]: string[] } = {}; 8 | 9 | export class MockAnswers { 10 | private readonly mockAnswers: ma.MockAnswers; 11 | constructor(){ 12 | this.mockAnswers = new ma.MockAnswers(); 13 | } 14 | 15 | public initialize(answers: ma.TaskLibAnswers){ 16 | this.mockAnswers.initialize(answers); 17 | } 18 | 19 | public getResponse(cmd: ma.MockedCommand, key: string, debug: (message: string) => void): any { 20 | if(!requestedAnswers[cmd]){ 21 | requestedAnswers[cmd] = []; 22 | } 23 | requestedAnswers[cmd].push(key); 24 | let response = this.mockAnswers.getResponse(cmd, key, debug); 25 | if(!response && cmd == `exec`){ 26 | // TODO: Why doesn't this bubble up to the tests? 27 | throw new Error(`No exec answer found for command "${key}". Make sure to mock answer for commands!`); 28 | } 29 | return response; 30 | } 31 | } 32 | 33 | export function resetRequestedAnswers(){ 34 | for(let k in requestedAnswers){ 35 | delete requestedAnswers[k]; 36 | } 37 | } -------------------------------------------------------------------------------- /tasks/terraform-cli/src/tests/steps/task-runner.ts: -------------------------------------------------------------------------------- 1 | import { MockTaskAgent } from "../../task-agent"; 2 | import * as ma from "azure-pipelines-task-lib/mock-answer"; 3 | import * as tasks from "azure-pipelines-task-lib/mock-task"; 4 | import MockToolFactory, { setAnswers } from "../../runners/mock-tool-factory"; 5 | import { AzdoRunner } from "../../runners"; 6 | import { Task } from "../../task"; 7 | import { CommandResponse } from "../../commands"; 8 | import { ITaskContext } from "../../context"; 9 | import intercept from 'intercept-stdout'; 10 | import TaskLogger from "../../logger/task-logger"; 11 | 12 | export default class TaskRunner { 13 | error?: Error; 14 | response?: CommandResponse; 15 | logs: string[] = []; 16 | public readonly taskAgent: MockTaskAgent; 17 | 18 | constructor() { 19 | this.taskAgent = new MockTaskAgent(); 20 | } 21 | 22 | public async run(taskContext: ITaskContext, taskAnswers: ma.TaskLibAnswers) { 23 | const toolFactory = new MockToolFactory(); 24 | const logger = new TaskLogger(taskContext, tasks); 25 | const runner = new AzdoRunner(toolFactory, logger); 26 | const task = new Task(taskContext, runner, this.taskAgent, logger); 27 | setAnswers(taskAnswers); 28 | try{ 29 | //separate the stdout from task and cucumbers test 30 | const unhook_intercept = intercept((text: string) => { 31 | this.logs.push(this.stripColoring(this.convertCRLFtoLF(text))); 32 | return ''; 33 | }) 34 | this.response = await task.exec(); 35 | unhook_intercept(); 36 | } 37 | catch(error){ 38 | this.error = (error instanceof Error) ? error : undefined; 39 | } 40 | } 41 | 42 | private convertCRLFtoLF(text: string){ 43 | return text.replace(/\r\n/g,'\n'); 44 | } 45 | 46 | private stripColoring(text: string){ 47 | return text.replace(/\x1B\[(\d+(;\d+)?)?[m|K]/g,''); 48 | } 49 | } -------------------------------------------------------------------------------- /tasks/terraform-cli/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "sourceMap": false, 6 | "strict": true, 7 | "outDir": ".bin", 8 | "typeRoots": [ 9 | "node_modules/@types" 10 | ], 11 | "types": [ 12 | "node", 13 | "chai" 14 | ], 15 | "esModuleInterop": true, 16 | "experimentalDecorators": true, 17 | "emitDecoratorMetadata": true, 18 | "resolveJsonModule": true, 19 | }, 20 | "exclude": [ 21 | "node_modules" 22 | ] 23 | } -------------------------------------------------------------------------------- /tasks/terraform-installer/.env-sample: -------------------------------------------------------------------------------- 1 | # These indicate where the task will create installed tools and temp files 2 | # These can remain as-is. 3 | AGENT_TOOLSDIRECTORY=./../_test-agent/tools 4 | AGENT_TEMPDIRECTORY=./../_test-agent/temp 5 | 6 | # The version of terraform to install on the build agent 7 | INPUT_TERRAFORMVERSION=0.11.8 -------------------------------------------------------------------------------- /tasks/terraform-installer/README.md: -------------------------------------------------------------------------------- 1 | # Terraform Installer for Azure Devops 2 | 3 | This tasks installs a specified version of terraform on the build agent. 4 | ** Hosted Ubuntu build agents already have terraform installed. This should only be needed for these agents a different version of terraform is required 5 | 6 | ## Development Setup 7 | While not required, its strongly suggested to use Visual Studio Code. The repo includes configuration for executing tasks and debugging in Visual Studio Code 8 | ### Dependencies 9 | The cli for Azure DevOps `tfx-cli` must be installed to upload the task and test in an Azure DevOps project. 10 | ``` 11 | npm install -g tfx-cli 12 | ``` 13 | ### Install NPM Packages 14 | Ensure that your command line's current directory is set to the TerraformInstaller dir 15 | ``` 16 | cd d:\code\azure-pipelines-tasks-terraform\tasks\terraform-installer 17 | ``` 18 | Run `npm install` to install the task dependencies. 19 | ``` 20 | npm install 21 | ``` 22 | ### Create environment file 23 | In order to execute the task locally, a .env file needs to be created. The .env file will set the parameters that are typically set when editing the task in Build or Release pipeline. This file should be created within the root of the TerrformInstaller folder. 24 | 25 | The example below contains all the possible inputs the task supports 26 | 27 | ```shell 28 | # These indicate where the task will create installed tools and temp files 29 | # These can remain as-is. 30 | AGENT_TOOLSDIRECTORY=./../_test-agent/tools 31 | AGENT_TEMPDIRECTORY=./../_test-agent/temp 32 | 33 | # The version of terraform to install on the build agent 34 | INPUT_TERRAFORMVERSION=0.14.3 35 | ``` 36 | ## Compile 37 | The `npm run build` script will compile the typescript down to standard es6 javascript 38 | ``` 39 | npm run build 40 | ``` 41 | ## Run 42 | The `npm start` script will compile the typescript and execute the task using the values specified in .env. 43 | ``` 44 | npm start 45 | ``` 46 | ## Debug (Using Visual Studio Code) 47 | From the Debug panel, set the configuration to `debug - tasks/terraform-installer` and press F5. 48 | -------------------------------------------------------------------------------- /tasks/terraform-installer/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jason-johnson/azure-pipelines-tasks-terraform/756f2ba62a644865933be62c2f5f94cdd36a9e75/tasks/terraform-installer/icon.png -------------------------------------------------------------------------------- /tasks/terraform-installer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "azure-pipelines-tasks-terraform-installer", 3 | "version": "1.0.0", 4 | "description": "Azure devops pipeline task to install terraform", 5 | "main": ".bin/index.js", 6 | "scripts": { 7 | "build": "tsc --build", 8 | "pack": "copyfiles package.json task.json icon.png \".bin/*.js\" .dist && cd .dist && npm install --only=prod", 9 | "start": "ts-node --require dotenv/config ./src/index.ts" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/jason-johnson/azure-pipelines-tasks-terraform.git" 14 | }, 15 | "keywords": [ 16 | "terraform", 17 | "azure-devops", 18 | "vsts" 19 | ], 20 | "author": "Charles Zipp", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/jason-johnson/azure-pipelines-tasks-terraform/issues" 24 | }, 25 | "homepage": "https://github.com/jason-johnson/azure-pipelines-tasks-terraform#readme", 26 | "devDependencies": { 27 | "@types/node": "^22.10.0", 28 | "@types/q": "^1.5.8", 29 | "copyfiles": "^2.4.0", 30 | "dotenv": "^16.4.5", 31 | "tfx-cli": "^0.17.0", 32 | "ts-node": "^10.9.2", 33 | "typescript": "^5.7.2" 34 | }, 35 | "dependencies": { 36 | "azure-pipelines-task-lib": "^4.17.3", 37 | "azure-pipelines-tool-lib": "^2.0.8", 38 | "node-fetch-with-proxy": "^0.1.6" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tasks/terraform-installer/src/index.ts: -------------------------------------------------------------------------------- 1 | import * as tasks from 'azure-pipelines-task-lib/task'; 2 | import * as installer from './installer' 3 | import * as tools from 'azure-pipelines-tool-lib/tool'; 4 | import * as path from 'path'; 5 | 6 | async function configureTerraform(){ 7 | var inputVersion = tasks.getInput("terraformVersion", true) || 'latest'; 8 | var downloadUrl = tasks.getInput("downloadUrl"); 9 | var terraformPath = await installer.download(inputVersion, downloadUrl); 10 | var envPath = process.env['PATH']; 11 | if(envPath && !envPath.startsWith(path.dirname(terraformPath))){ 12 | tools.prependPath(path.dirname(terraformPath)); 13 | } 14 | } 15 | 16 | async function verifyTerraform(){ 17 | console.log("Verifying Terraform installation. Executing 'terraform version'"); 18 | var terraformToolPath = tasks.which("terraform", true); 19 | var terraformTool = tasks.tool(terraformToolPath); 20 | terraformTool.arg("version"); 21 | return terraformTool.exec() 22 | } 23 | 24 | configureTerraform() 25 | .then(() => verifyTerraform()) 26 | .then(() => { 27 | tasks.setResult(tasks.TaskResult.Succeeded, ""); 28 | }) 29 | .catch((error) => { 30 | tasks.setResult(tasks.TaskResult.Failed, error) 31 | }) -------------------------------------------------------------------------------- /tasks/terraform-installer/src/sanitizer.ts: -------------------------------------------------------------------------------- 1 | import { cleanVersion } from 'azure-pipelines-tool-lib'; 2 | 3 | export function sanitizeVersion(inputVersion: string) : string { 4 | var version = cleanVersion(inputVersion); 5 | if(!version){ 6 | throw new Error("The input version '" + inputVersion + "' is not a valid semantic version"); 7 | } 8 | return version; 9 | } -------------------------------------------------------------------------------- /tasks/terraform-installer/task.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "2b4600b9-5cd9-4e3b-9c8b-553c8e58383a", 3 | "name": "TerraformInstaller", 4 | "friendlyName": "Terraform Installer", 5 | "description": "Installs a specific version of terraform", 6 | "author": "Charles Zipp", 7 | "helpMarkDown": "", 8 | "category": "Utility", 9 | "visibility": [ 10 | "Build", 11 | "Release" 12 | ], 13 | "demands": [], 14 | "version": { 15 | "Major": "#{majorNumber}#", 16 | "Minor": "#{GitVersion.Minor}#", 17 | "Patch": "#{GitVersion.Patch}#" 18 | }, 19 | "minimumAgentVersion": "3.248.0", 20 | "instanceNameFormat": "Use Terraform $(terraformVersion)", 21 | "inputs": [ 22 | { 23 | "name": "terraformVersion", 24 | "type": "string", 25 | "label": "Version", 26 | "defaultValue": "latest", 27 | "required": true, 28 | "helpMarkDown": "Specify the version of terraform that should be installed" 29 | }, 30 | { 31 | "name": "downloadUrl", 32 | "type": "string", 33 | "label": "Download URL", 34 | "required": false, 35 | "helpMarkDown": "(optional) Specify a custom URL to pull terraform from. Defaults to HashiCorp" 36 | } 37 | ], 38 | "execution": { 39 | "Node20_1": { 40 | "target": ".bin/index.js", 41 | "argumentFormat": "" 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tasks/terraform-installer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "sourceMap": false, 6 | "outDir": "./.bin", 7 | "strict": true, 8 | "types": ["node"], 9 | "esModuleInterop": true 10 | } 11 | } -------------------------------------------------------------------------------- /templates/.terraform/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /templates/aws/.terraform/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /templates/aws/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | backend "s3" { 3 | } 4 | } 5 | 6 | provider "aws" { 7 | } 8 | 9 | resource "aws_s3_bucket" "s3b_test" { 10 | bucket = "s3-test-eus-czp" 11 | acl = "private" 12 | } -------------------------------------------------------------------------------- /templates/default.env: -------------------------------------------------------------------------------- 1 | TF_VAR_region="eastus" 2 | TF_VAR_app-short-name="tffoo" 3 | TF_VAR_env-short-name="dev" -------------------------------------------------------------------------------- /templates/default.vars: -------------------------------------------------------------------------------- 1 | region="eastus" 2 | app-short-name="tffoo" 3 | env-short-name="dev" -------------------------------------------------------------------------------- /templates/gcp/.terraform/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /templates/gcp/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | backend "gcs" { 3 | } 4 | } 5 | 6 | provider "google" { 7 | } 8 | 9 | resource "google_storage_bucket" "gcsb_test" { 10 | name = "gcs-test-eus-czp" 11 | location = "US" 12 | } -------------------------------------------------------------------------------- /templates/local-exec-az-cli/local-exec-az-cli.vars: -------------------------------------------------------------------------------- 1 | location="eastus" 2 | location_suffix="usea" 3 | app="tffoo" 4 | env="dev" -------------------------------------------------------------------------------- /templates/local-exec-az-cli/main.tf: -------------------------------------------------------------------------------- 1 | variable "location" { 2 | type = string 3 | description = "Primary location to which the tracing resources are deployed." 4 | } 5 | 6 | variable "location_suffix" { 7 | type = string 8 | description = "Primary location short name to which the tracing resources are deployed." 9 | } 10 | 11 | variable "app" { 12 | type = string 13 | description = "An abbreviated version of the application name." 14 | } 15 | 16 | variable "env" { 17 | type = string 18 | description = "An abbreviated version of the environment name." 19 | } 20 | 21 | provider "azurerm" { 22 | features { 23 | } 24 | } 25 | 26 | terraform { 27 | backend "azurerm" { 28 | use_azuread_auth = true 29 | } 30 | required_version = ">= 0.12" 31 | } 32 | 33 | locals { 34 | suffix = "-core-${var.env}-${var.location_suffix}-${var.app}" 35 | suffix_2 = "core${var.env}${var.location_suffix}${var.app}" 36 | } 37 | resource "azurerm_resource_group" "rg_core" { 38 | name = "rg${local.suffix}" 39 | location = var.location 40 | } 41 | 42 | resource "azurerm_storage_account" "st_core" { 43 | name = "st${local.suffix_2}" 44 | location = var.location 45 | resource_group_name = azurerm_resource_group.rg_core.name 46 | account_kind = "StorageV2" 47 | account_tier = "Standard" 48 | account_replication_type = "LRS" 49 | 50 | 51 | provisioner "local-exec" { 52 | command = "az storage blob service-properties update --auth-mode login --account-name ${azurerm_storage_account.st_core.name} --static-website --index-document index.html --404-document index.html" 53 | } 54 | } -------------------------------------------------------------------------------- /templates/main.tf: -------------------------------------------------------------------------------- 1 | provider "azurerm" { 2 | features { 3 | } 4 | } 5 | 6 | terraform { 7 | backend "azurerm" { 8 | use_azuread_auth = true 9 | } 10 | } -------------------------------------------------------------------------------- /templates/sample.tf: -------------------------------------------------------------------------------- 1 | resource "azurerm_resource_group" "rg" { 2 | name = join( 3 | "-", 4 | ["rg", var.app-short-name, var.env-short-name, var.region], 5 | ) 6 | location = var.region 7 | } 8 | 9 | resource "random_string" "rs" { 10 | length = 12 11 | } 12 | 13 | output "some_string" { 14 | sensitive = false 15 | value = "somestringvalue" 16 | } 17 | 18 | output "some_bool" { 19 | sensitive = false 20 | value = true 21 | } 22 | 23 | output "some_number" { 24 | sensitive = false 25 | value = 1 26 | } 27 | 28 | output "some_sensitive_string" { 29 | sensitive = true 30 | value = "some-string-value" 31 | } 32 | 33 | output "some_object" { 34 | sensitive = false 35 | value = { A = "1", B = "2", C = "3"} 36 | } 37 | 38 | output "some_tuple" { 39 | sensitive = false 40 | value = ["1", 2, "3"] 41 | } 42 | 43 | output "some_map" { 44 | sensitive = false 45 | value = { "A" : 1, "B" : 2, "C" : 3 } 46 | } -------------------------------------------------------------------------------- /templates/variables.tf: -------------------------------------------------------------------------------- 1 | variable "region" { 2 | type = string 3 | description = "Primary region to which the tracing resources are deployed." 4 | } 5 | 6 | variable "app-short-name" { 7 | type = string 8 | description = "An abbreviated version of the application name." 9 | } 10 | 11 | variable "env-short-name" { 12 | type = string 13 | description = "An abbreviated version of the environment name." 14 | } 15 | 16 | -------------------------------------------------------------------------------- /templates/version11/.terraform/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /templates/version11/main.tf: -------------------------------------------------------------------------------- 1 | variable "location" { 2 | type = "string" 3 | description = "Primary location to which the tracing resources are deployed." 4 | } 5 | 6 | variable "location_suffix" { 7 | type = "string" 8 | description = "Primary location short name to which the tracing resources are deployed." 9 | } 10 | 11 | variable "app" { 12 | type = "string" 13 | description = "An abbreviated version of the application name." 14 | } 15 | 16 | variable "env" { 17 | type = "string" 18 | description = "An abbreviated version of the environment name." 19 | } 20 | 21 | provider "azurerm" { 22 | features {} 23 | } 24 | 25 | terraform { 26 | backend "azurerm" {} 27 | } 28 | 29 | locals { 30 | suffix = "-core-${var.env}-${var.location_suffix}-${var.app}" 31 | suffix_2 = "core${var.env}${var.location_suffix}${var.app}" 32 | } 33 | resource "azurerm_resource_group" "rg_core" { 34 | name = "rg${local.suffix}" 35 | location = "${var.location}" 36 | } -------------------------------------------------------------------------------- /templates/version11/version11.vars: -------------------------------------------------------------------------------- 1 | location="eastus" 2 | location_suffix="usea" 3 | app="tffoo" 4 | env="dev" -------------------------------------------------------------------------------- /templates/versions.tf: -------------------------------------------------------------------------------- 1 | 2 | terraform { 3 | required_version = ">= 0.12" 4 | } 5 | -------------------------------------------------------------------------------- /views/terraform-plan/README.md: -------------------------------------------------------------------------------- 1 | # Terraform Plan View 2 | 3 | This view enabled quick inspection of terraform plans produced by a Azure Pipeline run. 4 | The view is a simple React web page. 5 | It is compiled with Webpack and is shown to the user inside of an Azure DevOps Build Pipeline if the Terraform extension is used. 6 | 7 | ## Development Setup 8 | 9 | While not required, its strongly suggested to use Visual Studio Code. The repo includes configuration for executing tasks and debugging in Visual Studio Code 10 | 11 | ### Dependencies 12 | 13 | - [VS Code Debugger For Edge](https://marketplace.visualstudio.com/items?itemName=msjsdiag.debugger-for-edge) - Used in launch configurations that require a browser. 14 | 15 | ### Working Directory 16 | 17 | For the following, ensure that your command line's current directory is set to the `views\terraform-plan` dir 18 | 19 | ```cmd 20 | cd d:\code\azure-pipelines-terraform\tasks\terraform-cli 21 | ``` 22 | 23 | ### Install NPM Packages 24 | 25 | Run `npm install` to install the task dependencies. 26 | 27 | ```cmd 28 | npm install 29 | ``` 30 | 31 | ## Compile 32 | 33 | The `npm run build` script will execute `webpack` to compile and bundle the React app 34 | 35 | ```cmd 36 | npm run build 37 | ``` 38 | 39 | ## Run 40 | 41 | The `npm start` script open the webpack dev server and serve content from `views\terraform-plan\.bin\` at `http://localhost:3000` 42 | 43 | ```cmd 44 | npm start 45 | ``` 46 | 47 | **Note**: This will not immediately open a browser. After starting open browser and navigate to `http://localhost:3000` 48 | 49 | ## Debug (Using Visual Studio Code) 50 | 51 | From the Debug panel, set the configuration to `debug - views/terraform-plan` and press F5. 52 | 53 | This launch config will automatically start the webpack dev server and open chrome window with debugger attached at port 9222. 54 | 55 | The following configurations have also been provided to support debugging tests 56 | 57 | - `debug:tests - views/terraform-plan` - Runs jest with debugger attached. 58 | 59 | ## Test 60 | 61 | The `npm run test` script will execute all unit tests using jest. 62 | 63 | ```cmd 64 | npm run test 65 | ``` 66 | 67 | Tests can also be debugged in VS Code using launch configuration `debug:tests - views/terraform-plan`. 68 | 69 | ## Pack 70 | 71 | The `npm pack` will copy files required by overall extension to `views\terraform-plan\.dist` directory. 72 | 73 | ```cmd 74 | npm run pack 75 | ``` -------------------------------------------------------------------------------- /views/terraform-plan/src/__mocks__/styleMock.js: -------------------------------------------------------------------------------- 1 | // __mocks__/styleMock.js 2 | 3 | module.exports = {}; -------------------------------------------------------------------------------- /views/terraform-plan/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /views/terraform-plan/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from "react-dom"; 3 | import * as SDK from "azure-devops-extension-sdk"; 4 | import TerraformPlanDisplay from "./plan-summary-tab/plan-summary-tab"; 5 | import { MockAttachmentService, AzdoAttachmentService, IAttachmentService } from './services/attachments'; 6 | 7 | const renderComponent = (attachments: IAttachmentService) => { 8 | ReactDOM.render(, document.getElementById("root")); 9 | } 10 | 11 | if(process.env.TEST){ 12 | const mockAttachments = new MockAttachmentService(); 13 | const testData = require('./plan-summary-tab/test-data') 14 | mockAttachments.setAttachments(...[{ 15 | name: 'test_deploy.tfplan', 16 | type: 'terraform-plan-results', 17 | content: testData.examplePlan1 18 | }, { 19 | name: 'stage_deploy.tfplan', 20 | type: 'terraform-plan-results', 21 | content: testData.examplePlan1 22 | }]) 23 | renderComponent(mockAttachments); 24 | } 25 | else{ 26 | SDK.init().then(() => { 27 | const taskId: string = "51355d76-dd54-4754-919d-bba27fdf59e4" 28 | const azdoAttachments = new AzdoAttachmentService(taskId); 29 | renderComponent(azdoAttachments); 30 | }); 31 | } 32 | 33 | -------------------------------------------------------------------------------- /views/terraform-plan/src/plan-summary-tab/plan-summary-tab.scss: -------------------------------------------------------------------------------- 1 | @use "azure-devops-ui/Core/_platformCommon.scss"; 2 | 3 | #root { 4 | height: 100%; 5 | width: 100%; 6 | display: flex; 7 | } 8 | 9 | .content { 10 | height: 100%; 11 | width: 100%; 12 | object-fit: scale-down; 13 | } 14 | -------------------------------------------------------------------------------- /views/terraform-plan/src/plan-summary-tab/plan-summary-tab.test.tsx: -------------------------------------------------------------------------------- 1 | import { expect, test, describe, it } from '@jest/globals' 2 | import React from 'react' 3 | import { render, unmountComponentAtNode } from 'react-dom'; 4 | import { act } from 'react-dom/test-utils'; 5 | import MockAttachmentService from '../services/attachments/mock-attachment-service'; 6 | import TerraformPlanDisplay, { LoadingMessage, NoPublishedPlanMessage } from './plan-summary-tab'; 7 | 8 | let container: HTMLDivElement | null; 9 | let attachments: MockAttachmentService; 10 | 11 | beforeEach(() => { 12 | container = document.createElement("div"); 13 | container.id = "root"; 14 | document.body.appendChild(container); 15 | attachments = new MockAttachmentService(); 16 | }); 17 | 18 | afterEach(() => { 19 | if(container){ 20 | unmountComponentAtNode(container); 21 | container.remove(); 22 | } 23 | container = null; 24 | }) 25 | 26 | test("still loading", () => { 27 | act(() => { 28 | render(, container); 29 | }); 30 | // select the first flex row of the card content section 31 | const elements = container?.querySelectorAll("div.bolt-card-content div.flex-column div.flex-row div"); 32 | expect(elements).toBeDefined(); 33 | 34 | if(elements){ 35 | expect(elements.length).toBe(1); 36 | expect(elements[0].innerHTML).toBe(LoadingMessage); 37 | } 38 | }); 39 | 40 | -------------------------------------------------------------------------------- /views/terraform-plan/src/plan-summary-tab/table-data.tsx: -------------------------------------------------------------------------------- 1 | import { ISimpleListCell } from "azure-devops-ui/List"; 2 | import { IStatusProps, Status, Statuses, StatusSize } from "azure-devops-ui/Status"; 3 | import { 4 | ISimpleTableCell 5 | } from "azure-devops-ui/Table"; 6 | import { css } from "azure-devops-ui/Util"; 7 | import * as React from "react"; 8 | 9 | 10 | export interface ITableItem extends ISimpleTableCell { 11 | action: ISimpleListCell; 12 | resources: number; 13 | outputs: number; 14 | } 15 | 16 | interface IconSelector { 17 | statusProps: IStatusProps; 18 | label: string; 19 | } 20 | 21 | export const renderNoChange = (className?: string) => { 22 | return ( 23 | 29 | ); 30 | }; 31 | 32 | export const renderAdd = (className?: string) => { 33 | return ( 34 | 40 | ); 41 | }; 42 | 43 | export const renderChange = (className?: string) => { 44 | return ( 45 | 51 | ); 52 | }; 53 | 54 | export const renderDestroy = (className?: string) => { 55 | return ( 56 | 62 | ); 63 | }; 64 | -------------------------------------------------------------------------------- /views/terraform-plan/src/plan-summary-tab/test-data.tsx: -------------------------------------------------------------------------------- 1 | export const examplePlan1 = ` 2 | Terraform will perform the following actions: 3 | 4 |  # azurerm_resource_group.test_01 will be updated in-place 5 |  ~ resource "azurerm_resource_group" "test_01" { 6 | id = "/subscriptions/00000000-aaaa-0000-0000-aaaaaaaaaaaa/resourceGroups/test-01-rg" 7 | name = "test-01-rg" 8 | ~ tags = { 9 | ~ "test" = "test" -> "test1" 10 | } 11 | # (1 unchanged attribute hidden) 12 | } 13 | 14 |  # azurerm_resource_group.test_02 will be destroyed 15 |  - resource "azurerm_resource_group" "test_02" { 16 | - id = "/subscriptions/00000000-aaaa-0000-0000-aaaaaaaaaaaa/resourceGroups/test-02-rg" -> null 17 | - location = "eastus2" -> null 18 | - name = "test-02-rg" -> null 19 | - tags = {} -> null 20 | } 21 | 22 |  # azurerm_resource_group.test_03 will be created 23 |  + resource "azurerm_resource_group" "test_03" { 24 | + id = (known after apply) 25 | + location = "eastus2" 26 | + name = "test-03-rg" 27 | } 28 | 29 | Plan: 1 to add, 1 to change, 1 to destroy. 30 |  31 | Changes to Outputs: 32 | + three = (known after apply) 33 | - two = "/subscriptions/00000000-aaaa-0000-0000-aaaaaaaaaaaa/resourceGroups/test-02-rg" -> null 34 | `; -------------------------------------------------------------------------------- /views/terraform-plan/src/services/attachments/index.ts: -------------------------------------------------------------------------------- 1 | export interface Attachment { 2 | name: string; 3 | type: string; 4 | content: string; 5 | } 6 | 7 | export interface IAttachmentService { 8 | getAttachments(type: string): Promise 9 | } 10 | 11 | export { default as AzdoAttachmentService } from './azdo-attachment-service'; 12 | export { default as MockAttachmentService } from './mock-attachment-service'; -------------------------------------------------------------------------------- /views/terraform-plan/src/services/attachments/mock-attachment-service.ts: -------------------------------------------------------------------------------- 1 | import { Attachment, IAttachmentService } from "."; 2 | 3 | export default class MockAttachmentService implements IAttachmentService{ 4 | private readonly attachments: { [type: string]: Attachment[] } = {} 5 | 6 | setAttachments(...attachments: Attachment[]){ 7 | attachments.forEach(attachment => { 8 | if(!this.attachments[attachment.type]){ 9 | this.attachments[attachment.type] = [attachment]; 10 | } 11 | else{ 12 | this.attachments[attachment.type].push(attachment); 13 | } 14 | }); 15 | 16 | } 17 | async getAttachments(type: string): Promise { 18 | return this.attachments[type]; 19 | } 20 | } -------------------------------------------------------------------------------- /views/terraform-plan/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "amd", 4 | "moduleResolution": "node", 5 | "sourceMap": true, 6 | "noImplicitAny": true, 7 | "noImplicitThis": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "strict": true, 10 | "target": "es6", 11 | "outDir": ".bin", 12 | "jsx": "react", 13 | "lib": ["es6", "dom"], 14 | "types": ["react", "node", "jest"], 15 | "skipLibCheck": true, 16 | "esModuleInterop": true, 17 | }, 18 | "exclude": [ 19 | "node_modules", 20 | "tasks" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /views/terraform-plan/tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "charset": "utf8", 4 | "experimentalDecorators": true, 5 | "module": "amd", 6 | "moduleResolution": "node", 7 | "sourceMap": true, 8 | "noImplicitAny": true, 9 | "noImplicitThis": true, 10 | "strict": true, 11 | "target": "es5", 12 | "rootDir": "src/", 13 | "outDir": ".bin", 14 | "jsx": "react", 15 | "lib": [ 16 | "es5", 17 | "es6", 18 | "dom", 19 | "es2015.promise" 20 | ], 21 | "types": [ 22 | "react", 23 | "jest", 24 | "node" 25 | ], 26 | "allowJs": true, 27 | "skipLibCheck": true, 28 | "esModuleInterop": true, 29 | "allowSyntheticDefaultImports": true, 30 | "allowUmdGlobalAccess": true, 31 | "forceConsistentCasingInFileNames": true, 32 | "resolveJsonModule": true, 33 | "isolatedModules": false, 34 | "noEmit": false, 35 | }, 36 | "include": [ 37 | "src" 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /views/terraform-plan/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const CopyWebpackPlugin = require("copy-webpack-plugin"); 3 | const webpack = require('webpack'); 4 | 5 | module.exports = (env) => { 6 | return { 7 | target: "web", 8 | context: path.resolve(__dirname, './src'), 9 | entry: './index.tsx', 10 | output: { 11 | filename: "index.js", 12 | path: path.resolve(__dirname, "./.bin"), 13 | }, 14 | devtool: "source-map", 15 | devServer: { 16 | port: 3000, 17 | static: path.resolve(__dirname, "./.bin") 18 | }, 19 | resolve: { 20 | extensions: [".ts", ".tsx", ".js"], 21 | alias: { 22 | "azure-devops-extension-sdk": path.resolve( 23 | "node_modules/azure-devops-extension-sdk" 24 | ) 25 | } 26 | }, 27 | stats: { 28 | warnings: false 29 | }, 30 | module: { 31 | rules: [ 32 | { 33 | test: /\.tsx?$/, 34 | use: "ts-loader" 35 | }, 36 | { 37 | test: /\.scss$/, 38 | use: [ 39 | "style-loader", 40 | "css-loader", 41 | "azure-devops-ui/buildScripts/css-variables-loader", 42 | "sass-loader" 43 | ] 44 | }, 45 | { 46 | test: /\.css$/, 47 | use: ["style-loader", "css-loader"] 48 | }, 49 | { 50 | test: /\.woff$/, 51 | use: [ 52 | { 53 | loader: "base64-inline-loader" 54 | } 55 | ] 56 | }, 57 | { 58 | test: /\.html$/, 59 | use: "file-loader" 60 | } 61 | ] 62 | }, 63 | plugins: [ 64 | new CopyWebpackPlugin({ 65 | patterns: [{ from: "**/*.html" }] 66 | }), 67 | new webpack.DefinePlugin({ 68 | 'process.env.TEST': env.test 69 | }) 70 | ] 71 | } 72 | }; 73 | -------------------------------------------------------------------------------- /vss-extension-ga.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "azure-pipelines-tasks-terraform", 3 | "name": "Azure Pipelines Terraform Tasks", 4 | "galleryFlags": [ 5 | "Public", 6 | "Preview" 7 | ], 8 | "public": true, 9 | "contributions": [ 10 | { 11 | "description": "A tab to show terraform plan output", 12 | "id": "azure-pipelines-tasks-terraform-plan", 13 | "type": "ms.vss-build-web.build-results-tab", 14 | "targets": [ 15 | "ms.vss-build-web.build-results-view" 16 | ], 17 | "properties": { 18 | "name": "Terraform Plan", 19 | "supportsTasks": [ 20 | "51355d76-dd54-4754-919d-bba27fdf59e4" 21 | ], 22 | "uri": "views/terraform-plan/index.html" 23 | } 24 | } 25 | ] 26 | } -------------------------------------------------------------------------------- /vss-extension-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jason-johnson/azure-pipelines-tasks-terraform/756f2ba62a644865933be62c2f5f94cdd36a9e75/vss-extension-icon.png -------------------------------------------------------------------------------- /vss-extension-rc.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "azure-pipelines-tasks-terraform-rc", 3 | "name": "Azure Pipelines Terraform Tasks (RC)", 4 | "galleryFlags": [ 5 | "Preview" 6 | ], 7 | "public": false, 8 | "contributions": [ 9 | { 10 | "description": "A tab to show terraform plan output", 11 | "id": "azure-pipelines-tasks-terraform-plan", 12 | "type": "ms.vss-build-web.build-results-tab", 13 | "targets": [ 14 | "ms.vss-build-web.build-results-view" 15 | ], 16 | "properties": { 17 | "name": "Terraform Plan (RC)", 18 | "supportsTasks": [ 19 | "51355d76-dd54-4754-919d-bba27fdf59e4" 20 | ], 21 | "uri": "views/terraform-plan/index.html" 22 | } 23 | } 24 | ] 25 | } --------------------------------------------------------------------------------