├── .gitignore
├── .vscode
├── launch.json
└── tasks.json
├── GitVersion.yml
├── LICENSE
├── README.md
├── Tasks
├── Terraform
│ ├── icon.png
│ ├── package-lock.json
│ ├── package.json
│ ├── src
│ │ ├── Index.ts
│ │ ├── Options.ts
│ │ ├── Provider
│ │ │ ├── Azure
│ │ │ │ ├── ARMAuthenticationMethod.ts
│ │ │ │ ├── ARMConnectedServiceOptions.ts
│ │ │ │ ├── AzureOptions.ts
│ │ │ │ ├── AzureProvider.ts
│ │ │ │ └── AzureStorageService.ts
│ │ │ ├── Remote
│ │ │ │ ├── RemoteConnectedServiceOptions.ts
│ │ │ │ └── RemoteProvider.ts
│ │ │ ├── TerraformProvider.ts
│ │ │ └── TerraformProviderType.ts
│ │ ├── TaskOptions.ts
│ │ ├── TerraformCommandRunner.ts
│ │ ├── TerraformTask.ts
│ │ └── types.ts
│ ├── task.json
│ ├── test
│ │ └── Provider
│ │ │ └── Azure
│ │ │ └── AzureProvider.spec.ts
│ └── tsconfig.json
├── TerraformEnterprise
│ ├── icon.png
│ ├── package-lock.json
│ ├── package.json
│ ├── src
│ │ ├── Index.ts
│ │ ├── TaskOptions.ts
│ │ ├── TerraformApi.ts
│ │ └── TerraformEnterpriseTask.ts
│ ├── task.json
│ └── tsconfig.json
└── TerraformInstaller
│ ├── icon.png
│ ├── package-lock.json
│ ├── package.json
│ ├── src
│ ├── Index.ts
│ └── TerraformInstaller.ts
│ ├── task.json
│ └── tsconfig.json
├── azure-pipelines-extension-build.yml
├── azure-pipelines-extension-release.yml
├── azure-pipelines-task-build.yml
├── azure-pipelines.yml
├── images
└── icon.png
├── tests
├── azure-pipelines.yml
├── test-cli.yml
├── test-init-validate-plan-apply-destroy.yml
├── test-installer.yml
├── test-template-azure
│ ├── main.tf
│ └── output.plan
├── test-template-no-backend
│ └── main.tf
└── test-template-remote
│ └── main.tf
└── vss-extension.json
/.gitignore:
--------------------------------------------------------------------------------
1 | *.vsix
2 | node_modules/
3 | .taskkey
4 | .env
5 | .bin/
6 | .dist/
7 | .terraform/
8 | *.tfstate*
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | // Terraform init
8 | {
9 | "type": "node",
10 | "request": "launch",
11 | "name": "Terraform Init - Azure",
12 | "envFile": "${workspaceFolder}/Tasks/Terraform/.env",
13 | "env": {
14 | "INPUT_command": "init",
15 | "INPUT_cwd": "./tests/test-template-azure",
16 | "INPUT_backend": "Azure",
17 | "INPUT_backendAzureProviderStorageAccountName": "mtcdenterraformsandbox",
18 | "INPUT_providerAzureConnectedServiceName": "AzureProviderConnection",
19 | "INPUT_backendAzureUseProviderConnectedServiceForBackend": "true"
20 | },
21 | "program": "${workspaceFolder}/Tasks/Terraform/src/Index.ts",
22 | "preLaunchTask": "Compile Terraform",
23 | "internalConsoleOptions": "openOnSessionStart",
24 | "sourceMaps": true,
25 | "outputCapture": "std",
26 | "outFiles": [
27 | "${workspaceFolder}/Tasks/Terraform/.bin/**/*.js"
28 | ]
29 | },
30 |
31 | {
32 | "type": "node",
33 | "request": "launch",
34 | "name": "Terraform Plan - Azure",
35 | "envFile": "${workspaceFolder}/Tasks/Terraform/.env",
36 | "env": {
37 | "INPUT_command": "plan",
38 | "INPUT_cwd": "./tests/test-template-azure",
39 | "INPUT_backend": "Azure",
40 | "INPUT_backendAzureProviderStorageAccountName": "mtcdenterraformsandbox",
41 | "INPUT_providerAzureConnectedServiceName": "AzureProviderConnection",
42 | "INPUT_backendAzureUseProviderConnectedServiceForBackend": "true",
43 | "INPUT_outputFile": "output.plan",
44 | "INPUT_variables": "{\"environment\": \"dev\"}"
45 | },
46 | "program": "${workspaceFolder}/Tasks/Terraform/src/Index.ts",
47 | "preLaunchTask": "Compile Terraform",
48 | "internalConsoleOptions": "openOnSessionStart",
49 | "sourceMaps": true,
50 | "outputCapture": "std",
51 | "outFiles": [
52 | "${workspaceFolder}/Tasks/Terraform/.bin/**/*.js"
53 | ]
54 | },
55 |
56 | {
57 | "type": "node",
58 | "request": "launch",
59 | "name": "Terraform Apply - Azure",
60 | "envFile": "${workspaceFolder}/Tasks/Terraform/.env",
61 | "env": {
62 | "INPUT_command": "apply",
63 | "INPUT_cwd": "./tests/test-template-azure",
64 | "INPUT_backend": "Azure",
65 | "INPUT_backendAzureProviderStorageAccountName": "mtcdenterraformsandbox",
66 | "INPUT_providerAzureConnectedServiceName": "AzureProviderConnection",
67 | "INPUT_backendAzureUseProviderConnectedServiceForBackend": "true",
68 | "INPUT_planFile": "output.plan"
69 | },
70 | "program": "${workspaceFolder}/Tasks/Terraform/src/Index.ts",
71 | "preLaunchTask": "Compile Terraform",
72 | "internalConsoleOptions": "openOnSessionStart",
73 | "sourceMaps": true,
74 | "outputCapture": "std",
75 | "outFiles": [
76 | "${workspaceFolder}/Tasks/Terraform/.bin/**/*.js"
77 | ]
78 | },
79 |
80 | // Terraform CLI
81 | {
82 | "type": "node",
83 | "request": "launch",
84 | "name": "Terraform CLI - Azure",
85 | "envFile": "${workspaceFolder}/Tasks/Terraform/.env",
86 | "env": {
87 | "INPUT_command": "CLI",
88 | "INPUT_script": "terraform init \n terraform plan",
89 | "INPUT_cwd": "./tests/test-template-azure",
90 | "INPUT_provider": "Azure",
91 | "INPUT_backend": "Azure",
92 | "INPUT_backendAzureProviderStorageAccountName": "mtcdenterraformsandbox",
93 | "INPUT_providerAzureConnectedServiceName": "AzureProviderConnection",
94 | "INPUT_backendAzureUseProviderConnectedServiceForBackend": "true"
95 | },
96 | "program": "${workspaceFolder}/Tasks/Terraform/src/Index.ts",
97 | "preLaunchTask": "Compile Terraform",
98 | "internalConsoleOptions": "openOnSessionStart",
99 | "sourceMaps": true,
100 | "outputCapture": "std",
101 | "outFiles": [
102 | "${workspaceFolder}/Tasks/Terraform/.bin/**/*.js"
103 | ]
104 | },
105 |
106 | // Terraform Init Remote Backend
107 | {
108 | "type": "node",
109 | "request": "launch",
110 | "name": "Terraform Init - Remote",
111 | "envFile": "${workspaceFolder}/Tasks/Terraform/.env",
112 | "env": {
113 | "INPUT_command": "init",
114 | "INPUT_cwd": "./tests/test-template-remote",
115 | "INPUT_backend": "Remote",
116 | "INPUT_provider": "Remote"
117 | },
118 | "program": "${workspaceFolder}/Tasks/Terraform/src/Index.ts",
119 | "preLaunchTask": "Compile Terraform",
120 | "internalConsoleOptions": "openOnSessionStart",
121 | "sourceMaps": true,
122 | "outputCapture": "std",
123 | "outFiles": [
124 | "${workspaceFolder}/Tasks/Terraform/.bin/**/*.js"
125 | ]
126 | },
127 |
128 | // Terraform Installer
129 | {
130 | "type": "node",
131 | "request": "launch",
132 | "name": "TerraformInstaller",
133 | "envFile": "${workspaceFolder}/Tasks/TerraformInstaller/.env",
134 | "program": "${workspaceFolder}/Tasks/TerraformInstaller/src/Index.ts",
135 | "preLaunchTask": "Compile TerraformInstaller",
136 | "internalConsoleOptions": "openOnSessionStart",
137 | "sourceMaps": true,
138 | //"smartStep": false,
139 | "outputCapture": "std",
140 | "outFiles": [
141 | "${workspaceFolder}/Tasks/TerraformInstaller/.bin/**/*.js"
142 | ]
143 | }
144 | ]
145 | }
146 |
147 |
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | // See https://go.microsoft.com/fwlink/?LinkId=733558
3 | // for the documentation about the tasks.json format
4 | "version": "2.0.0",
5 | "tasks": [
6 | {
7 | "label": "Compile Terraform",
8 | "type": "typescript",
9 | "tsconfig": "Tasks/Terraform/tsconfig.json",
10 | "problemMatcher": [
11 | "$tsc"
12 | ],
13 | "group": {
14 | "kind": "build",
15 | "isDefault": true
16 | }
17 | },
18 | {
19 | "label": "Compile Terraform Installer",
20 | "type": "typescript",
21 | "tsconfig": "Tasks/TerraformInstaller/tsconfig.json",
22 | "problemMatcher": [
23 | "$tsc"
24 | ],
25 | "group": {
26 | "kind": "build",
27 | "isDefault": true
28 | }
29 | },
30 | {
31 | "label": "Compile Terraform Enterprise",
32 | "type": "typescript",
33 | "tsconfig": "Tasks/TerraformEnterprise/tsconfig.json",
34 | "problemMatcher": [
35 | "$tsc"
36 | ],
37 | "group": {
38 | "kind": "build",
39 | "isDefault": true
40 | }
41 | }
42 | ]
43 | }
--------------------------------------------------------------------------------
/GitVersion.yml:
--------------------------------------------------------------------------------
1 | mode: Mainline
2 | branches: {}
3 | ignore:
4 | sha: []
5 | merge-message-formats: {}
6 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2019 HashiCorp, Inc.
2 |
3 | Mozilla Public License Version 2.0
4 | ==================================
5 |
6 | 1. Definitions
7 | --------------
8 |
9 | 1.1. "Contributor"
10 | means each individual or legal entity that creates, contributes to
11 | the creation of, or owns Covered Software.
12 |
13 | 1.2. "Contributor Version"
14 | means the combination of the Contributions of others (if any) used
15 | by a Contributor and that particular Contributor's Contribution.
16 |
17 | 1.3. "Contribution"
18 | means Covered Software of a particular Contributor.
19 |
20 | 1.4. "Covered Software"
21 | means Source Code Form to which the initial Contributor has attached
22 | the notice in Exhibit A, the Executable Form of such Source Code
23 | Form, and Modifications of such Source Code Form, in each case
24 | including portions thereof.
25 |
26 | 1.5. "Incompatible With Secondary Licenses"
27 | means
28 |
29 | (a) that the initial Contributor has attached the notice described
30 | in Exhibit B to the Covered Software; or
31 |
32 | (b) that the Covered Software was made available under the terms of
33 | version 1.1 or earlier of the License, but not also under the
34 | terms of a Secondary License.
35 |
36 | 1.6. "Executable Form"
37 | means any form of the work other than Source Code Form.
38 |
39 | 1.7. "Larger Work"
40 | means a work that combines Covered Software with other material, in
41 | a separate file or files, that is not Covered Software.
42 |
43 | 1.8. "License"
44 | means this document.
45 |
46 | 1.9. "Licensable"
47 | means having the right to grant, to the maximum extent possible,
48 | whether at the time of the initial grant or subsequently, any and
49 | all of the rights conveyed by this License.
50 |
51 | 1.10. "Modifications"
52 | means any of the following:
53 |
54 | (a) any file in Source Code Form that results from an addition to,
55 | deletion from, or modification of the contents of Covered
56 | Software; or
57 |
58 | (b) any new file in Source Code Form that contains any Covered
59 | Software.
60 |
61 | 1.11. "Patent Claims" of a Contributor
62 | means any patent claim(s), including without limitation, method,
63 | process, and apparatus claims, in any patent Licensable by such
64 | Contributor that would be infringed, but for the grant of the
65 | License, by the making, using, selling, offering for sale, having
66 | made, import, or transfer of either its Contributions or its
67 | Contributor Version.
68 |
69 | 1.12. "Secondary License"
70 | means either the GNU General Public License, Version 2.0, the GNU
71 | Lesser General Public License, Version 2.1, the GNU Affero General
72 | Public License, Version 3.0, or any later versions of those
73 | licenses.
74 |
75 | 1.13. "Source Code Form"
76 | means the form of the work preferred for making modifications.
77 |
78 | 1.14. "You" (or "Your")
79 | means an individual or a legal entity exercising rights under this
80 | License. For legal entities, "You" includes any entity that
81 | controls, is controlled by, or is under common control with You. For
82 | purposes of this definition, "control" means (a) the power, direct
83 | or indirect, to cause the direction or management of such entity,
84 | whether by contract or otherwise, or (b) ownership of more than
85 | fifty percent (50%) of the outstanding shares or beneficial
86 | ownership of such entity.
87 |
88 | 2. License Grants and Conditions
89 | --------------------------------
90 |
91 | 2.1. Grants
92 |
93 | Each Contributor hereby grants You a world-wide, royalty-free,
94 | non-exclusive license:
95 |
96 | (a) under intellectual property rights (other than patent or trademark)
97 | Licensable by such Contributor to use, reproduce, make available,
98 | modify, display, perform, distribute, and otherwise exploit its
99 | Contributions, either on an unmodified basis, with Modifications, or
100 | as part of a Larger Work; and
101 |
102 | (b) under Patent Claims of such Contributor to make, use, sell, offer
103 | for sale, have made, import, and otherwise transfer either its
104 | Contributions or its Contributor Version.
105 |
106 | 2.2. Effective Date
107 |
108 | The licenses granted in Section 2.1 with respect to any Contribution
109 | become effective for each Contribution on the date the Contributor first
110 | distributes such Contribution.
111 |
112 | 2.3. Limitations on Grant Scope
113 |
114 | The licenses granted in this Section 2 are the only rights granted under
115 | this License. No additional rights or licenses will be implied from the
116 | distribution or licensing of Covered Software under this License.
117 | Notwithstanding Section 2.1(b) above, no patent license is granted by a
118 | Contributor:
119 |
120 | (a) for any code that a Contributor has removed from Covered Software;
121 | or
122 |
123 | (b) for infringements caused by: (i) Your and any other third party's
124 | modifications of Covered Software, or (ii) the combination of its
125 | Contributions with other software (except as part of its Contributor
126 | Version); or
127 |
128 | (c) under Patent Claims infringed by Covered Software in the absence of
129 | its Contributions.
130 |
131 | This License does not grant any rights in the trademarks, service marks,
132 | or logos of any Contributor (except as may be necessary to comply with
133 | the notice requirements in Section 3.4).
134 |
135 | 2.4. Subsequent Licenses
136 |
137 | No Contributor makes additional grants as a result of Your choice to
138 | distribute the Covered Software under a subsequent version of this
139 | License (see Section 10.2) or under the terms of a Secondary License (if
140 | permitted under the terms of Section 3.3).
141 |
142 | 2.5. Representation
143 |
144 | Each Contributor represents that the Contributor believes its
145 | Contributions are its original creation(s) or it has sufficient rights
146 | to grant the rights to its Contributions conveyed by this License.
147 |
148 | 2.6. Fair Use
149 |
150 | This License is not intended to limit any rights You have under
151 | applicable copyright doctrines of fair use, fair dealing, or other
152 | equivalents.
153 |
154 | 2.7. Conditions
155 |
156 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
157 | in Section 2.1.
158 |
159 | 3. Responsibilities
160 | -------------------
161 |
162 | 3.1. Distribution of Source Form
163 |
164 | All distribution of Covered Software in Source Code Form, including any
165 | Modifications that You create or to which You contribute, must be under
166 | the terms of this License. You must inform recipients that the Source
167 | Code Form of the Covered Software is governed by the terms of this
168 | License, and how they can obtain a copy of this License. You may not
169 | attempt to alter or restrict the recipients' rights in the Source Code
170 | Form.
171 |
172 | 3.2. Distribution of Executable Form
173 |
174 | If You distribute Covered Software in Executable Form then:
175 |
176 | (a) such Covered Software must also be made available in Source Code
177 | Form, as described in Section 3.1, and You must inform recipients of
178 | the Executable Form how they can obtain a copy of such Source Code
179 | Form by reasonable means in a timely manner, at a charge no more
180 | than the cost of distribution to the recipient; and
181 |
182 | (b) You may distribute such Executable Form under the terms of this
183 | License, or sublicense it under different terms, provided that the
184 | license for the Executable Form does not attempt to limit or alter
185 | the recipients' rights in the Source Code Form under this License.
186 |
187 | 3.3. Distribution of a Larger Work
188 |
189 | You may create and distribute a Larger Work under terms of Your choice,
190 | provided that You also comply with the requirements of this License for
191 | the Covered Software. If the Larger Work is a combination of Covered
192 | Software with a work governed by one or more Secondary Licenses, and the
193 | Covered Software is not Incompatible With Secondary Licenses, this
194 | License permits You to additionally distribute such Covered Software
195 | under the terms of such Secondary License(s), so that the recipient of
196 | the Larger Work may, at their option, further distribute the Covered
197 | Software under the terms of either this License or such Secondary
198 | License(s).
199 |
200 | 3.4. Notices
201 |
202 | You may not remove or alter the substance of any license notices
203 | (including copyright notices, patent notices, disclaimers of warranty,
204 | or limitations of liability) contained within the Source Code Form of
205 | the Covered Software, except that You may alter any license notices to
206 | the extent required to remedy known factual inaccuracies.
207 |
208 | 3.5. Application of Additional Terms
209 |
210 | You may choose to offer, and to charge a fee for, warranty, support,
211 | indemnity or liability obligations to one or more recipients of Covered
212 | Software. However, You may do so only on Your own behalf, and not on
213 | behalf of any Contributor. You must make it absolutely clear that any
214 | such warranty, support, indemnity, or liability obligation is offered by
215 | You alone, and You hereby agree to indemnify every Contributor for any
216 | liability incurred by such Contributor as a result of warranty, support,
217 | indemnity or liability terms You offer. You may include additional
218 | disclaimers of warranty and limitations of liability specific to any
219 | jurisdiction.
220 |
221 | 4. Inability to Comply Due to Statute or Regulation
222 | ---------------------------------------------------
223 |
224 | If it is impossible for You to comply with any of the terms of this
225 | License with respect to some or all of the Covered Software due to
226 | statute, judicial order, or regulation then You must: (a) comply with
227 | the terms of this License to the maximum extent possible; and (b)
228 | describe the limitations and the code they affect. Such description must
229 | be placed in a text file included with all distributions of the Covered
230 | Software under this License. Except to the extent prohibited by statute
231 | or regulation, such description must be sufficiently detailed for a
232 | recipient of ordinary skill to be able to understand it.
233 |
234 | 5. Termination
235 | --------------
236 |
237 | 5.1. The rights granted under this License will terminate automatically
238 | if You fail to comply with any of its terms. However, if You become
239 | compliant, then the rights granted under this License from a particular
240 | Contributor are reinstated (a) provisionally, unless and until such
241 | Contributor explicitly and finally terminates Your grants, and (b) on an
242 | ongoing basis, if such Contributor fails to notify You of the
243 | non-compliance by some reasonable means prior to 60 days after You have
244 | come back into compliance. Moreover, Your grants from a particular
245 | Contributor are reinstated on an ongoing basis if such Contributor
246 | notifies You of the non-compliance by some reasonable means, this is the
247 | first time You have received notice of non-compliance with this License
248 | from such Contributor, and You become compliant prior to 30 days after
249 | Your receipt of the notice.
250 |
251 | 5.2. If You initiate litigation against any entity by asserting a patent
252 | infringement claim (excluding declaratory judgment actions,
253 | counter-claims, and cross-claims) alleging that a Contributor Version
254 | directly or indirectly infringes any patent, then the rights granted to
255 | You by any and all Contributors for the Covered Software under Section
256 | 2.1 of this License shall terminate.
257 |
258 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all
259 | end user license agreements (excluding distributors and resellers) which
260 | have been validly granted by You or Your distributors under this License
261 | prior to termination shall survive termination.
262 |
263 | ************************************************************************
264 | * *
265 | * 6. Disclaimer of Warranty *
266 | * ------------------------- *
267 | * *
268 | * Covered Software is provided under this License on an "as is" *
269 | * basis, without warranty of any kind, either expressed, implied, or *
270 | * statutory, including, without limitation, warranties that the *
271 | * Covered Software is free of defects, merchantable, fit for a *
272 | * particular purpose or non-infringing. The entire risk as to the *
273 | * quality and performance of the Covered Software is with You. *
274 | * Should any Covered Software prove defective in any respect, You *
275 | * (not any Contributor) assume the cost of any necessary servicing, *
276 | * repair, or correction. This disclaimer of warranty constitutes an *
277 | * essential part of this License. No use of any Covered Software is *
278 | * authorized under this License except under this disclaimer. *
279 | * *
280 | ************************************************************************
281 |
282 | ************************************************************************
283 | * *
284 | * 7. Limitation of Liability *
285 | * -------------------------- *
286 | * *
287 | * Under no circumstances and under no legal theory, whether tort *
288 | * (including negligence), contract, or otherwise, shall any *
289 | * Contributor, or anyone who distributes Covered Software as *
290 | * permitted above, be liable to You for any direct, indirect, *
291 | * special, incidental, or consequential damages of any character *
292 | * including, without limitation, damages for lost profits, loss of *
293 | * goodwill, work stoppage, computer failure or malfunction, or any *
294 | * and all other commercial damages or losses, even if such party *
295 | * shall have been informed of the possibility of such damages. This *
296 | * limitation of liability shall not apply to liability for death or *
297 | * personal injury resulting from such party's negligence to the *
298 | * extent applicable law prohibits such limitation. Some *
299 | * jurisdictions do not allow the exclusion or limitation of *
300 | * incidental or consequential damages, so this exclusion and *
301 | * limitation may not apply to You. *
302 | * *
303 | ************************************************************************
304 |
305 | 8. Litigation
306 | -------------
307 |
308 | Any litigation relating to this License may be brought only in the
309 | courts of a jurisdiction where the defendant maintains its principal
310 | place of business and such litigation shall be governed by laws of that
311 | jurisdiction, without reference to its conflict-of-law provisions.
312 | Nothing in this Section shall prevent a party's ability to bring
313 | cross-claims or counter-claims.
314 |
315 | 9. Miscellaneous
316 | ----------------
317 |
318 | This License represents the complete agreement concerning the subject
319 | matter hereof. If any provision of this License is held to be
320 | unenforceable, such provision shall be reformed only to the extent
321 | necessary to make it enforceable. Any law or regulation which provides
322 | that the language of a contract shall be construed against the drafter
323 | shall not be used to construe this License against a Contributor.
324 |
325 | 10. Versions of the License
326 | ---------------------------
327 |
328 | 10.1. New Versions
329 |
330 | Mozilla Foundation is the license steward. Except as provided in Section
331 | 10.3, no one other than the license steward has the right to modify or
332 | publish new versions of this License. Each version will be given a
333 | distinguishing version number.
334 |
335 | 10.2. Effect of New Versions
336 |
337 | You may distribute the Covered Software under the terms of the version
338 | of the License under which You originally received the Covered Software,
339 | or under the terms of any subsequent version published by the license
340 | steward.
341 |
342 | 10.3. Modified Versions
343 |
344 | If you create software not governed by this License, and you want to
345 | create a new license for such software, you may create and use a
346 | modified version of this License if you rename the license and remove
347 | any references to the name of the license steward (except to note that
348 | such modified license differs from this License).
349 |
350 | 10.4. Distributing Source Code Form that is Incompatible With Secondary
351 | Licenses
352 |
353 | If You choose to distribute Source Code Form that is Incompatible With
354 | Secondary Licenses under the terms of this version of the License, the
355 | notice described in Exhibit B of this License must be attached.
356 |
357 | Exhibit A - Source Code Form License Notice
358 | -------------------------------------------
359 |
360 | This Source Code Form is subject to the terms of the Mozilla Public
361 | License, v. 2.0. If a copy of the MPL was not distributed with this
362 | file, You can obtain one at http://mozilla.org/MPL/2.0/.
363 |
364 | If it is not possible or desirable to put the notice in a particular
365 | file, then You may include the notice in a location (such as a LICENSE
366 | file in a relevant directory) where a recipient would be likely to look
367 | for such a notice.
368 |
369 | You may add additional accurate notices of copyright ownership.
370 |
371 | Exhibit B - "Incompatible With Secondary Licenses" Notice
372 | ---------------------------------------------------------
373 |
374 | This Source Code Form is "Incompatible With Secondary Licenses", as
375 | defined by the Mozilla Public License, v. 2.0.
376 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Azure Pipelines Extension for Terraform
2 |
3 | This repository contains the source for an Azure Pipelines extension that provides Tasks to easily install and use Terraform.
4 |
5 | This extension provides a `TerraformInstaller` task to ease in installing specific Terraform versions, as well as a `Terraform` task to help call Terraform without needing to manage authentication yourself. The `Terraform` task wraps `init`, `plan`, `validate`, `apply`, and `destroy` commands, as well as providing a `CLI` option.
6 |
7 | `CLI` is used to execute a custom script in a pre-authenticated environment. This can be a great option if you need to use more complex terraform scripts, such as gathering output and setting it to a Piplelines variable (see example below).
8 |
9 | Once this task has been added to your Organization from the Azure DevOps Marketplace you can use it in any Azure Pipelines build or release job. It is available in both the GUI pipeline editor as well as yaml templates.
10 |
11 | ## Options
12 |
13 |
14 | #### General
15 | *Common options available in most configurations*
16 |
17 | | Name | Type | Description |
18 | |-|-|-|
19 | | `command` | `pickList` | The Terraform command to run.
*Options*: `init`, `validate`, `plan`, `apply`, `destroy`, `cli` |
20 | | `provider` | `pickList` | The Cloud provider to authenticate with.
*Options*: `Azure`, `Remote` (`AWS` and `GCP` Coming soon) |
21 | | `backend` | `pickList` | Where to store the Terraform backend state.
*Options*: `Azure`, `Remote` (`AWS` and `GCP` Coming soon)
22 |
23 |
24 | #### Azure
25 | *Options which are available when Azure Backend and Providers are selected.*
26 |
27 | | Name | Type | Description |
28 | |-|-|-|
29 | | `providerAzureConnectedServiceName` | `serviceConnection` | The Azure Subscription to execute Terraform against|
30 | | `backendAzureUseProviderConnectedServiceForBackend` | bool | Should the specified provider connection be re-used to talk to the backend storage account? |
31 | | `backendAzureConnectedServiceName` | `serviceConnection` | The Azure Subscription to be used to talk to the backend storage accoutn |
32 | | `backendAzureStorageAccountName` | `serviceConnection` | If a separate backend connection is specified: the Storage Account to store the backend state in. |
33 | | `backendAzureProviderStorageAccountName` | `serviceConnection` | If no separate backend connection is specified: the Storage Account to store the backend state in. |
34 | | `backendAzureContainerName` | `string` | The Storage Account Container name |
35 | | `backendAzureStateFileKey` | `string` | The name of the terraform state file |
36 |
37 | #### CLI
38 | *Options which are available when command is set to CLI*
39 |
40 | | Name | Type | Description |
41 | |-|-|-|
42 | | `initialize` | `bool` | Should `terraform init` run before executing the CLI script |
43 | | `scriptLocation` | `pickList` | How will the CLI script be provided?
*Options*: `Inline script`, `Script path`|
44 | | `scriptPath` | `filePath` | The path to the CLI script to execute |
45 | | `script` | `string` | The inline script to execute |
46 |
47 | #### Advanced
48 | *Advanced options available for all non-CLI commands*
49 |
50 | | Name | Type | Description |
51 | |-|-|-|
52 | | `args` | `string` | Additional arguments to pass to the Terraform command being run |
53 |
54 |
55 | ## YAML Pipeline Examples
56 |
57 | #### Install Terraform
58 |
59 | ```yaml
60 | pool:
61 | vmImage: 'Ubuntu-16.04'
62 |
63 | steps:
64 | - task: terraformInstaller@0
65 | inputs:
66 | terraformVersion: '0.12.12'
67 | displayName: Install Terraform
68 | ```
69 |
70 | #### Init, plan, and apply
71 | You can invoke the task in a yaml template using the following syntax:
72 |
73 | ```yaml
74 | pool:
75 | vmImage: 'Ubuntu-16.04'
76 |
77 | steps:
78 | - task: terraform@0
79 | inputs:
80 | command: 'init'
81 | providerAzureConnectedServiceName: 'MTC Denver Sandbox'
82 | backendAzureProviderStorageAccountName: 'mtcdenterraformsandbox'
83 | displayName: Terraform Init
84 |
85 | - task: terraform@0
86 | inputs:
87 | command: 'plan'
88 | providerAzureConnectedServiceName: 'MTC Denver Sandbox'
89 | args: -var=environment=demo -out=tfplan.out
90 | displayName: Terraform Plan
91 |
92 | - task: terraform@0
93 | inputs:
94 | command: 'apply'
95 | providerAzureConnectedServiceName: 'MTC Denver Sandbox'
96 | args: tfplan.out
97 | displayName: Terraform Apply
98 | ```
99 |
100 | #### Execute a Terraform-authenticated CLI Script
101 |
102 |
103 | ```yaml
104 | pool:
105 | vmImage: 'Ubuntu-16.04'
106 |
107 | steps:
108 | - task: terraform@0
109 | inputs:
110 | command: 'CLI'
111 | providerAzureConnectedServiceName: 'MTC Denver Sandbox'
112 | backendAzureProviderStorageAccountName: 'mtcdenterraformsandbox'
113 | script: |
114 | # Validate
115 | terraform validate
116 |
117 | # Plan
118 | terraform plan -input=false -out=testplan.tf
119 |
120 | # Get output
121 | STORAGE_ACCOUNT=`terraform output storage_account`
122 |
123 | # Set storageAccountName variable from terraform output
124 | echo "##vso[task.setvariable variable=storageAccountName]$STORAGE_ACCOUNT"
125 | displayName: Execute Terraform CLI Script
126 |
127 | ```
128 |
--------------------------------------------------------------------------------
/Tasks/Terraform/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hashicorp/azure-pipelines-extension-terraform/1f7b31dbc2ae32cb9b700b6f048ed7eb9ebddbaa/Tasks/Terraform/icon.png
--------------------------------------------------------------------------------
/Tasks/Terraform/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "azure-pipelines-terraform-task",
3 | "version": "0.0.1",
4 | "description": "A Terraform task for Azure Pipelines",
5 | "main": ".bin/src/Index.js",
6 | "author": "",
7 | "license": "MPL-2.0",
8 | "scripts": {
9 | "build": "tsc --build",
10 | "start": "node -r dotenv/config .bin/src/Index.js",
11 | "test": "ts-mocha test/**/*.spec.ts"
12 | },
13 | "dependencies": {
14 | "@azure/arm-storage": "^11.0.0",
15 | "@azure/ms-rest-nodeauth": "^3.0.3",
16 | "azure-pipelines-task-lib": "^2.9.3",
17 | "config": "^3.2.1",
18 | "inversify": "^5.0.1",
19 | "reflect-metadata": "^0.1.13",
20 | "tsyringe": "^3.3.0"
21 | },
22 | "devDependencies": {
23 | "@types/chai": "^4.2.3",
24 | "@types/expect": "^1.20.4",
25 | "@types/mocha": "^5.2.7",
26 | "@types/node": "^12.6.8",
27 | "@types/q": "^1.5.2",
28 | "chai": "^4.2.0",
29 | "copyfiles": "^2.1.0",
30 | "dotenv": "^8.2.0",
31 | "mocha": "^5.2.0",
32 | "mocha-junit-reporter": "^1.18.0",
33 | "tfx-cli": "^0.6.4",
34 | "ts-mocha": "^6.0.0",
35 | "ts-node": "^8.4.1",
36 | "tsconfig-paths": "^3.9.0",
37 | "typescript": "^3.3.3333"
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/Tasks/Terraform/src/Index.ts:
--------------------------------------------------------------------------------
1 | import "reflect-metadata";
2 | import { Container } from "inversify";
3 | import { TaskResult } from "azure-pipelines-task-lib/task";
4 | import task = require('azure-pipelines-task-lib/task');
5 |
6 | import { TerraformTask } from './TerraformTask';
7 | import { TerraformCommandRunner } from "./TerraformCommandRunner";
8 | import { TaskOptions } from './TaskOptions';
9 | import { Options } from './Options';
10 |
11 | import { TerraformProvider } from "./Provider/TerraformProvider";
12 | import { AzureProvider } from './Provider/Azure/AzureProvider'
13 | import { RemoteProvider } from './Provider/Remote/RemoteProvider'
14 | import { RemoteConnectedServiceOptions } from "./Provider/Remote/RemoteConnectedServiceOptions";
15 | import { AzureOptions } from "./Provider/Azure/AzureOptions"
16 |
17 | // Configure DI
18 | let container = new Container();
19 |
20 | // Bind Task
21 | container.bind(TerraformTask).toSelf();
22 | container.bind(TerraformCommandRunner).toSelf();
23 |
24 | // Bind Options
25 | container.bind(TaskOptions).toDynamicValue((context) => {
26 | return Options.load(TaskOptions);
27 | });
28 |
29 | container.bind(AzureOptions).toDynamicValue((context) => {
30 | return Options.load(AzureOptions);
31 | });
32 |
33 | container.bind(RemoteConnectedServiceOptions).toDynamicValue((context) => {
34 | return Options.load(RemoteConnectedServiceOptions);
35 | });
36 |
37 | // Bind Terraform Provider
38 | let options = container.get(TaskOptions);
39 |
40 | switch (options.provider || options.backend) {
41 | case "Azure":
42 | container.bind(TerraformProvider).to(AzureProvider);
43 | break;
44 | case "Remote":
45 | container.bind(TerraformProvider).to(RemoteProvider)
46 | default:
47 | break;
48 | }
49 |
50 |
51 | // Get and run the task
52 | var terraformTask = container.get(TerraformTask);
53 |
54 | terraformTask.run().then(function()
55 | {
56 | task.setResult(TaskResult.Succeeded, "Terraform successfully ran");
57 | }, function(reason) {
58 | task.setResult(TaskResult.Failed, "Terraform failed to run" + reason);
59 | });
60 |
--------------------------------------------------------------------------------
/Tasks/Terraform/src/Options.ts:
--------------------------------------------------------------------------------
1 | import task = require('azure-pipelines-task-lib/task');
2 | import { injectable } from "inversify";
3 | import "reflect-metadata";
4 |
5 | const valueMetadataKey = Symbol("devopsTaskMetadataKey");
6 |
7 | enum TaskInputType {
8 | Default = 1,
9 | TaskVariable,
10 | EndpointAuthorizationParameter,
11 | EndpointDataParameter,
12 | EndpointAuthorizationScheme
13 | }
14 |
15 | class OptionMetadata {
16 | constructor (
17 | public type: TaskInputType = TaskInputType.Default,
18 | public id: string = "",
19 | public key : string = "",
20 | public required : boolean = false) {
21 |
22 | }
23 | }
24 |
25 | export function taskVariable(id : string, required : boolean = false) :any {
26 | return function(target : any, propertyKey: string, descriptor: PropertyDescriptor) {
27 | Reflect.defineMetadata(valueMetadataKey, new OptionMetadata(TaskInputType.TaskVariable, id, "", required), target, propertyKey)
28 | }
29 | }
30 |
31 | export function endpointAuthorizationParameter(id : string, key : string, required : boolean = false) :any {
32 | return function(target : any, propertyKey: string, descriptor: PropertyDescriptor) {
33 | Reflect.defineMetadata(valueMetadataKey, new OptionMetadata(TaskInputType.EndpointAuthorizationParameter, id, key, required), target, propertyKey)
34 | }
35 | }
36 |
37 | export function endpointDataParameter(id : string, key : string, required : boolean = false) :any {
38 | return function(target : any, propertyKey: string, descriptor: PropertyDescriptor) {
39 | Reflect.defineMetadata(valueMetadataKey, new OptionMetadata(TaskInputType.EndpointDataParameter, id, key, required), target, propertyKey)
40 | }
41 | }
42 |
43 | export function endpointAuthorizationScheme(id : string, required = false) :any {
44 | return function(target : any, propertyKey: string, descriptor: PropertyDescriptor) {
45 | Reflect.defineMetadata(valueMetadataKey, new OptionMetadata(TaskInputType.EndpointAuthorizationScheme, id, "", required), target, propertyKey)
46 | }
47 | }
48 |
49 |
50 | /**
51 | * Strong-type accessor for Task configuration
52 | */
53 | @injectable()
54 | export class Options {
55 |
56 | private getProperty(o: T, name: K) {
57 | return o[name];
58 | }
59 |
60 | private static getTypeofProperty(o: T, name: K) {
61 | return typeof o[name];
62 | }
63 |
64 | private static setProperty(o: T, name: K, value : any) {
65 | return o[name] = value;
66 | }
67 |
68 | /**
69 | * Returns the parsed value of this config
70 | */
71 | public static load(type: { new (): T }) : T {
72 | let options : T = new type();
73 |
74 | for (let propertyKey in options) {
75 | let metadata = Reflect.getMetadata(valueMetadataKey, options, propertyKey) as OptionMetadata;
76 |
77 | let type = metadata && metadata.type || TaskInputType.Default;
78 | let value : any;
79 |
80 | switch (type) {
81 | case TaskInputType.Default:
82 | value = this.getInputVariableFromProperty(options, propertyKey)
83 | break;
84 | case TaskInputType.TaskVariable:
85 | value = task.getTaskVariable(metadata.id);
86 | break;
87 | case TaskInputType.EndpointAuthorizationScheme:
88 | value = task.getEndpointAuthorizationScheme(metadata.id, false);
89 | break;
90 | case TaskInputType.EndpointAuthorizationParameter:
91 | value = task.getEndpointAuthorizationParameter(metadata.id, metadata.key, false);
92 | break;
93 | case TaskInputType.EndpointDataParameter:
94 | value = task.getEndpointDataParameter(metadata.id, metadata.key, false);
95 | break;
96 | }
97 |
98 | this.setProperty(options, propertyKey, value);
99 | }
100 |
101 | return options;
102 | }
103 |
104 | private static getInputVariableFromProperty(options : any, id : string) : any{
105 | let propertyType = this.getTypeofProperty(options, id);
106 |
107 | let value : any;
108 |
109 | switch (propertyType){
110 | case "string":
111 | value = task.getInput(id);
112 | break;
113 | case "boolean":
114 | value = task.getBoolInput(id);
115 | break;
116 | default:
117 | value = "";
118 | }
119 |
120 | return value
121 | }
122 | }
--------------------------------------------------------------------------------
/Tasks/Terraform/src/Provider/Azure/ARMAuthenticationMethod.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Different ways to authenticate from an ARM connected service
3 | */
4 | export enum ARMAuthenticationMethod {
5 | Unknown = 0,
6 | ServicePrincipalKey,
7 | ServicePrincipalCertificate,
8 | ManagedIdentity,
9 | }
--------------------------------------------------------------------------------
/Tasks/Terraform/src/Provider/Azure/ARMConnectedServiceOptions.ts:
--------------------------------------------------------------------------------
1 | import { injectable } from "inversify";
2 | import task = require('azure-pipelines-task-lib/task');
3 | import fs = require("fs");
4 | import path = require("path");
5 | import { ARMAuthenticationMethod } from "./ARMAuthenticationMethod";
6 |
7 | /**
8 | * Loads connected service data for ARM from the Task into a strongly-typed object
9 | */
10 | @injectable()
11 | export class ARMConnectedServiceOptions {
12 | public clientId: string = "";
13 | public clientSecret: string = "";
14 | public clientCertificatePath: string = "";
15 | public tenantId: string = "";
16 | public subscriptionId: string = "";
17 | public authenticationMethod: ARMAuthenticationMethod = ARMAuthenticationMethod.Unknown;
18 |
19 | constructor(private connectedServiceName: string) {
20 | let authScheme = task.getEndpointAuthorizationScheme(connectedServiceName, true) as string;
21 |
22 | if (!authScheme) {
23 | throw Error("No authentication sceme provided");
24 | }
25 |
26 | this.loadArmDetails();
27 |
28 | switch(authScheme.toLowerCase()) {
29 | case "serviceprincipal":
30 | this.loadServicePrincipalDetails();
31 | break;
32 | case "managedserviceidentity":
33 | this.loadManagedIdentityDetails();
34 | break;
35 | }
36 | }
37 |
38 | /**
39 | * Sets ARM Tenant and subscription details based on the connected service
40 | */
41 | private loadArmDetails() {
42 | this.tenantId = task.getEndpointAuthorizationParameter(this.connectedServiceName, "tenantid", true) as string;
43 | this.subscriptionId = task.getEndpointDataParameter(this.connectedServiceName, 'subscriptionid', true);
44 | }
45 |
46 | /**
47 | * Sets service principal details based on the connected service
48 | */
49 | private loadServicePrincipalDetails() {
50 | let authType = task.getEndpointAuthorizationParameter(this.connectedServiceName, 'authenticationType', true) as string;
51 | this.clientId = task.getEndpointAuthorizationParameter(this.connectedServiceName, "serviceprincipalid", true) as string;
52 |
53 | switch(authType) {
54 | case "spnCertificate":
55 | this.loadServicePrincipalCertificateDetails();
56 | break;
57 | default:
58 | this.loadServicePrincipalKeyDetails();
59 | }
60 | }
61 |
62 | /**
63 | * Sets service principal key details based on the connected service
64 | */
65 | private loadServicePrincipalKeyDetails() {
66 | this.authenticationMethod = ARMAuthenticationMethod.ServicePrincipalKey;
67 | this.clientSecret = task.getEndpointAuthorizationParameter(this.connectedServiceName, "serviceprincipalkey", true) as string;
68 | }
69 |
70 | /**
71 | * Sets service principal certificate details based on the connected service
72 | */
73 | private loadServicePrincipalCertificateDetails() {
74 | this.authenticationMethod = ARMAuthenticationMethod.ServicePrincipalCertificate;
75 | let certificateContent = task.getEndpointAuthorizationParameter(this.connectedServiceName, "servicePrincipalCertificate", true);
76 | let certificatePath = path.join(task.getVariable('Agent.TempDirectory') as string || task.getVariable('system.DefaultWorkingDirectory') as string, 'spnCert.pem');
77 |
78 | fs.writeFileSync(certificatePath, certificateContent);
79 |
80 | this.clientCertificatePath = certificatePath;
81 | }
82 |
83 | /**
84 | * Sets service principal certificate details based on the connected service
85 | */
86 | private loadManagedIdentityDetails() {
87 | this.authenticationMethod = ARMAuthenticationMethod.ManagedIdentity;
88 | }
89 | }
--------------------------------------------------------------------------------
/Tasks/Terraform/src/Provider/Azure/AzureOptions.ts:
--------------------------------------------------------------------------------
1 | import { injectable } from "inversify";
2 |
3 | /**
4 | * Loads connected service data for ARM from the Task into a strongly-typed object
5 | */
6 | @injectable()
7 | export class AzureOptions {
8 | public providerAzureConnectedServiceName : string = "";
9 | public backendAzureUseProviderConnectedServiceForBackend : boolean = true;
10 | public backendAzureConnectedServiceName : string = "";
11 | public backendAzureStorageAccountName : string = "";
12 | public backendAzureProviderStorageAccountName : string = "";
13 | public backendAzureContainerName : string = "";
14 | public backendAzureStateFileKey : string = "";
15 | }
--------------------------------------------------------------------------------
/Tasks/Terraform/src/Provider/Azure/AzureProvider.ts:
--------------------------------------------------------------------------------
1 | import { injectable } from "inversify";
2 | import task = require('azure-pipelines-task-lib/task');
3 | import { ARMConnectedServiceOptions } from "./ARMConnectedServiceOptions";
4 | import { ARMAuthenticationMethod } from "./ARMAuthenticationMethod";
5 | import { TerraformProvider } from "../TerraformProvider";
6 | import { AzureStorageService } from "./AzureStorageService"
7 | import { AzureOptions } from "./AzureOptions";
8 |
9 | /**
10 | * Terraform Azure Provider and Backend
11 | */
12 | @injectable()
13 | export class AzureProvider extends TerraformProvider {
14 | private armConnectedService: ARMConnectedServiceOptions | undefined = undefined;
15 |
16 | constructor(private options : AzureOptions) {
17 | super();
18 | }
19 |
20 | /**
21 | * Loads the ARM connected service information into the environment
22 | */
23 | public async authenticate() : Promise<{ [key: string]: string; }> {
24 | if (!this.options.providerAzureConnectedServiceName) {
25 | throw new Error("No Azure connection specified")
26 | }
27 |
28 | this.armConnectedService = new ARMConnectedServiceOptions(this.options.providerAzureConnectedServiceName);
29 |
30 | let env : { [key: string]: string; };
31 |
32 | switch (this.armConnectedService.authenticationMethod) {
33 | case ARMAuthenticationMethod.ServicePrincipalKey:
34 | env = this.getServicePrincipalKeyEnv(this.armConnectedService);
35 | break;
36 | case ARMAuthenticationMethod.ServicePrincipalCertificate:
37 | env = this.getServicePrincipalCertificateEnv(this.armConnectedService);
38 | break;
39 | case ARMAuthenticationMethod.ManagedIdentity:
40 | env = this.getManagedIdentityEnv();
41 | break;
42 | default:
43 | env = {};
44 | }
45 |
46 | return {
47 | ARM_TENANT_ID: this.armConnectedService.tenantId,
48 | ARM_SUBSCRIPTION_ID: this.armConnectedService.subscriptionId,
49 | ...env
50 | };
51 | }
52 |
53 | /**
54 | * Builds an object containing all the apporpriate values needed to set as backend-config
55 | * for Terraform to be use an Azure Storage Account as the backend
56 | */
57 | public async getBackendConfigOptions(): Promise<{ [key: string]: string; }> {
58 |
59 | let connectedService : ARMConnectedServiceOptions;
60 |
61 | if (this.options.backendAzureUseProviderConnectedServiceForBackend) {
62 | if (!this.options.providerAzureConnectedServiceName) {
63 | throw new Error("No Azure provider connection speficied");
64 | }
65 |
66 | connectedService = new ARMConnectedServiceOptions(this.options.providerAzureConnectedServiceName);
67 | } else {
68 | if (!this.options.backendAzureConnectedServiceName) {
69 | throw new Error("Backend connected service not specified");
70 | }
71 |
72 | connectedService = new ARMConnectedServiceOptions(this.options.backendAzureConnectedServiceName);
73 | }
74 |
75 | let storage = new AzureStorageService(connectedService);
76 | let storageAccount = this.options.backendAzureUseProviderConnectedServiceForBackend ? this.options.backendAzureProviderStorageAccountName : this.options.backendAzureStorageAccountName;
77 |
78 | if (!storageAccount) {
79 | throw new Error("Storage account not specified");
80 | }
81 |
82 | if (!this.options.backendAzureContainerName) {
83 | throw new Error("Storage container name not specified");
84 | }
85 |
86 | if (!this.options.backendAzureStateFileKey) {
87 | throw new Error("State file key not specified");
88 | }
89 |
90 | // I'd much prefer to use a SAS here but generating SAS isn't supported via the JS SDK without using a key
91 | let storage_key = await storage.getKey(
92 | storageAccount,
93 | this.options.backendAzureContainerName);
94 |
95 | return {
96 | storage_account_name: storageAccount,
97 | container_name: this.options.backendAzureContainerName,
98 | key: this.options.backendAzureStateFileKey,
99 | access_key: storage_key
100 | }
101 | }
102 |
103 | /**
104 | * Gets the appropraite ENV vars for Service Principal Key authentication
105 | */
106 | private getServicePrincipalKeyEnv(armConnectedService : ARMConnectedServiceOptions): { [key: string]: string; } {
107 | return {
108 | ARM_CLIENT_ID: armConnectedService.clientId,
109 | ARM_CLIENT_SECRET: armConnectedService.clientSecret,
110 | };
111 | }
112 |
113 | /**
114 | * Gets the appropraite ENV vars for Service Principal Cert authentication
115 | */
116 | private getServicePrincipalCertificateEnv(armConnectedService : ARMConnectedServiceOptions): { [key: string]: string; } {
117 | return {
118 | ARM_CLIENT_ID: armConnectedService.clientId,
119 | ARM_CLIENT_SECRET: armConnectedService.clientSecret,
120 | };
121 | }
122 |
123 | /**
124 | * Gets the appropraite ENV vars for Managed Identity authentication
125 | */
126 | private getManagedIdentityEnv(): { [key: string]: string; } {
127 | return {
128 | ARM_USE_MSI: "true",
129 | };
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/Tasks/Terraform/src/Provider/Azure/AzureStorageService.ts:
--------------------------------------------------------------------------------
1 | import * as msRestNodeAuth from "@azure/ms-rest-nodeauth";
2 | import { injectable } from "inversify";
3 | import { ARMAuthenticationMethod } from "./ARMAuthenticationMethod";
4 | import { ARMConnectedServiceOptions } from "./ARMConnectedServiceOptions";
5 | import { AzureTokenCredentialsOptions } from "@azure/ms-rest-nodeauth";
6 | import { StorageManagementClient } from "@azure/arm-storage";
7 | import { ServiceClientCredentials } from "@azure/ms-rest-js";
8 |
9 | /**
10 | * Access Azure Storage to get a key for the account
11 | */
12 | @injectable()
13 | export class AzureStorageService {
14 | private readonly StorageUrl = "https://management.azure.com/"
15 |
16 | constructor(private connectedService: ARMConnectedServiceOptions) {
17 |
18 | }
19 |
20 | public async getKey(storageAccountName: string, containerName: string) : Promise {
21 | var creds = await this.login();
22 | var client = new StorageManagementClient(creds, this.connectedService.subscriptionId);
23 | var storageAccounts = await client.storageAccounts.list();
24 |
25 | var account = storageAccounts.find(item => item.name == storageAccountName);
26 |
27 | if (account == null) {
28 | throw new Error("Storage account not found");
29 | }
30 |
31 | var id = account.id || "";
32 | var regex = new RegExp("resourceGroups\/([a-zA-Z0-9_-]+)\/");
33 | var res = regex.exec(id) || [];
34 | var resourceGroupName = res[1];
35 |
36 | var keysResult = await client.storageAccounts.listKeys(resourceGroupName, storageAccountName);
37 |
38 | if (
39 | keysResult == null ||
40 | keysResult.keys == null ||
41 | keysResult.keys.length == 0 ||
42 | keysResult.keys[0].value == null
43 | ) {
44 | throw new Error("Could not get storage account keys");
45 | }
46 |
47 | return keysResult.keys[0].value;
48 | }
49 |
50 | private async login() : Promise {
51 | switch(this.connectedService.authenticationMethod) {
52 | case ARMAuthenticationMethod.ManagedIdentity:
53 | return this.loginWithManagedIdentity();
54 | case ARMAuthenticationMethod.ServicePrincipalKey:
55 | return this.loginWithServicePrincipalKey();
56 | case ARMAuthenticationMethod.ServicePrincipalCertificate:
57 | return this.loginWithServicePrincipalCertificate();
58 | }
59 |
60 | throw new Error("No valid authentication method specified");
61 | }
62 |
63 | private async loginWithManagedIdentity() : Promise {
64 | var creds = await msRestNodeAuth.loginWithVmMSI({
65 | "resource": this.StorageUrl
66 | });
67 |
68 | return creds;
69 | }
70 |
71 | private async loginWithServicePrincipalKey() : Promise {
72 | return msRestNodeAuth.loginWithServicePrincipalSecretWithAuthResponse(
73 | this.connectedService.clientId,
74 | this.connectedService.clientSecret,
75 | this.connectedService.tenantId,
76 | {
77 | "tokenAudience": this.StorageUrl
78 | } as AzureTokenCredentialsOptions
79 | ).then((authres) => {
80 | return authres.credentials;
81 | });
82 | }
83 |
84 | private async loginWithServicePrincipalCertificate() : Promise {
85 | var creds = await msRestNodeAuth.loginWithServicePrincipalCertificate(
86 | this.connectedService.clientId,
87 | this.connectedService.clientCertificatePath,
88 | this.StorageUrl
89 | );
90 |
91 | return creds;
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/Tasks/Terraform/src/Provider/Remote/RemoteConnectedServiceOptions.ts:
--------------------------------------------------------------------------------
1 | import { injectable } from "inversify";
2 | import task = require('azure-pipelines-task-lib/task');
3 |
4 | /**
5 | * Loads connected service data for ARM from the Task into a strongly-typed object
6 | */
7 | @injectable()
8 | export class RemoteConnectedServiceOptions {
9 | public backendRemoteUrl: string = "";
10 | public backendRemoteToken: string = "";
11 | }
--------------------------------------------------------------------------------
/Tasks/Terraform/src/Provider/Remote/RemoteProvider.ts:
--------------------------------------------------------------------------------
1 | import { injectable } from "inversify";
2 | import path = require("path");
3 | import fs = require("fs");
4 | import task = require('azure-pipelines-task-lib/task');
5 | import { TerraformProvider } from "../TerraformProvider";
6 | import { TaskOptions } from "../../TaskOptions";
7 | import { RemoteConnectedServiceOptions } from "./RemoteConnectedServiceOptions"
8 | /**
9 | * Terraform Remote Provider and Backend
10 | */
11 | @injectable()
12 | export class RemoteProvider extends TerraformProvider {
13 | private cliConfigFileLocation : string = "";
14 |
15 | constructor(private taskOptions : TaskOptions, private options : RemoteConnectedServiceOptions) {
16 | super();
17 | }
18 |
19 | /**
20 | * Create a terraform.rc file in the temp directory with the appropriate credentials
21 | */
22 | public async authenticate() : Promise<{ [key: string]: string; }> {
23 | var config = `
24 | credentials "${this.options.backendRemoteUrl}" {
25 | token = "${this.options.backendRemoteToken}"
26 | }
27 | `;
28 |
29 | this.cliConfigFileLocation = path.join(this.taskOptions.tempDir as string, "terraform.rc");
30 |
31 | fs.writeFileSync(this.cliConfigFileLocation, config);
32 |
33 | return {
34 | "TF_CLI_CONFIG_FILE ": this.cliConfigFileLocation
35 | }
36 | }
37 |
38 | /**
39 | * Returns an empty object as no config specification is needed with a Remote backend
40 | */
41 | public async getBackendConfigOptions(): Promise<{ [key: string]: string; }> {
42 | return {}
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/Tasks/Terraform/src/Provider/TerraformProvider.ts:
--------------------------------------------------------------------------------
1 | import { injectable } from "inversify";
2 |
3 | /**
4 | * A Provider for Terraform
5 | */
6 | @injectable()
7 | export abstract class TerraformProvider {
8 | /**
9 | * Configures the file system and process environment variables necessary to run Terraform
10 | * in an authenticated manner.
11 | *
12 | * @returns Variables to set for auth in the spawned process env
13 | */
14 | abstract authenticate() : Promise<{ [key: string]: string; }>;
15 |
16 | /**
17 | * Get's a dictionary containing the backend-config parameters
18 | * to set on init
19 | */
20 | abstract getBackendConfigOptions() : Promise<{ [key: string]: string; }>;
21 | }
--------------------------------------------------------------------------------
/Tasks/Terraform/src/Provider/TerraformProviderType.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Different ways to authenticate from an ARM connected service
3 | */
4 | export enum TerraformProviderType {
5 | Unknown = 0,
6 | Azure,
7 | Aws,
8 | Gcp,
9 | Remote
10 | }
--------------------------------------------------------------------------------
/Tasks/Terraform/src/TaskOptions.ts:
--------------------------------------------------------------------------------
1 | import task = require('azure-pipelines-task-lib/task');
2 | import { injectable } from "inversify";
3 | import { taskVariable } from "./Options";
4 |
5 | /**
6 | * Strong-type accessor for Task configuration
7 | */
8 | @injectable()
9 | export class TaskOptions {
10 |
11 | // Basic
12 | public command : string = "";
13 | public provider : string = "";
14 | public backend : string = "";
15 |
16 | // CLI
17 | public scriptLocation : string = "";
18 | public scriptPath : string = "";
19 | public script : string = "";
20 | public initialize : boolean = true;
21 |
22 | // Plan
23 | public variables : string = "";
24 | public outputFile : string = "";
25 | public planFile : string = "";
26 |
27 | // Advanced
28 | public cwd : string = "";
29 | public args : string = "";
30 |
31 | @taskVariable("Agent.TempDirectory")
32 | public tempDir : string = "";
33 | }
--------------------------------------------------------------------------------
/Tasks/Terraform/src/TerraformCommandRunner.ts:
--------------------------------------------------------------------------------
1 | import os = require("os");
2 | import task = require('azure-pipelines-task-lib/task');
3 | import fs = require("fs");
4 | import { injectable } from "inversify";
5 | import { ToolRunner, IExecOptions, IExecSyncResult } from "azure-pipelines-task-lib/toolrunner";
6 | import { TaskOptions } from "./TaskOptions";
7 | import { TerraformProvider } from "./Provider/TerraformProvider";
8 | import { AzureProvider } from "./Provider/Azure/AzureProvider";
9 | import path = require("path");
10 |
11 | /**
12 | * Class to handle running Terraform commands
13 | */
14 | @injectable()
15 | export class TerraformCommandRunner {
16 | private readonly terraform : ToolRunner;
17 |
18 | public constructor(
19 | private provider : TerraformProvider,
20 | private options: TaskOptions
21 |
22 | ) {
23 | this.terraform = this.createTerraformToolRunner();
24 | }
25 |
26 | /**
27 | * Initializes Terraform with the backend configuration specified from the provider
28 | */
29 | public async init(args: Array = [], authenticate: boolean = false) {
30 | let backendConfigOptions = await this.provider.getBackendConfigOptions();
31 |
32 | // Set the backend configuration values
33 | //
34 | // Values are not quoted intentionally - the way node spawns processes it will
35 | // see quotes as part of the values
36 | for (let key in backendConfigOptions) {
37 | let value = backendConfigOptions[key];
38 | args.push(`-backend-config=${key}=${value}`);
39 | }
40 |
41 | await this.exec(["init", ...args], authenticate);
42 | }
43 |
44 | /**
45 | * Initializes Terraform with the backend configuration specified from the provider
46 | */
47 | public async plan(args: Array = [], variables : string, outputFile : string) {
48 |
49 | if (variables && variables != ""){
50 | let parsedVariables = JSON.parse(variables);
51 |
52 | // Set the variables
53 | //
54 | // Values are not quoted intentionally - the way node spawns processes it will
55 | // see quotes as part of the values
56 | for (let key in parsedVariables) {
57 | let value = parsedVariables[key];
58 | args.push(`-var=${key}=${value}`);
59 | }
60 | }
61 |
62 | if (outputFile != "") {
63 | args.push("-out=" + outputFile);
64 | }
65 |
66 | await this.exec(["plan", ...args], true);
67 | }
68 |
69 | /**
70 | * Executes a script within an authenticated Terraform environment
71 | * @param script The location of the script to run
72 | */
73 | public async exec(args: Array = [], authenticate: boolean = true) {
74 | console.log("Executing terraform command");
75 |
76 | if (!this.options.command) {
77 | throw new Error("No command specified");
78 | }
79 |
80 | // Handle authentication for this command
81 | let authenticationEnv : { [key: string]: string; } = {};
82 |
83 | if (authenticate) {
84 | authenticationEnv = await this.provider.authenticate();
85 | }
86 |
87 | let command = this.terraform;
88 |
89 | for (let arg of args) {
90 | command.arg(arg);
91 | }
92 |
93 | if (this.options.args) {
94 | command.line(this.options.args);
95 | }
96 |
97 | let result = await command.exec({
98 | cwd: path.join(process.cwd(), this.options.cwd || ""),
99 | env: {
100 | ...process.env,
101 | ...authenticationEnv
102 | },
103 | windowsVerbatimArguments: true
104 | } as unknown as IExecOptions);
105 |
106 | if (result > 0) {
107 | throw new Error("Terraform initalize failed");
108 | }
109 | }
110 |
111 | /**
112 | * Executes a script within an authenticated Terraform environment
113 | * @param script The location of the script to run
114 | */
115 | public async cli(script: string) {
116 | // Handle authentication for this command
117 | let authenticationEnv = await this.provider.authenticate();
118 |
119 | let content = fs.readFileSync(script,'utf8');
120 |
121 | console.log(content);
122 |
123 | let tool = this.createCliToolRunner(script);
124 |
125 | let result = await tool.exec({
126 | cwd: this.options.cwd,
127 | env: {
128 | ...process.env,
129 | ...authenticationEnv
130 | },
131 | windowsVerbatimArguments: true
132 | } as unknown as IExecOptions);
133 |
134 | if (result > 0) {
135 | throw new Error("Terraform CLI failed");
136 | }
137 | }
138 |
139 | /**
140 | * Creates an Azure Pipelines ToolRunner for Terraform
141 | */
142 | private createTerraformToolRunner() : ToolRunner {
143 | let terraformPath = task.which("terraform", true);
144 | let terraform: ToolRunner = task.tool(terraformPath);
145 |
146 | return terraform;
147 | }
148 |
149 | /**
150 | * Creates an Azure Pipelines ToolRunner for Bash or CMD
151 | */
152 | private createCliToolRunner(scriptPath : string) : ToolRunner {
153 | var tool;
154 |
155 | if (os.type() != "Windows_NT") {
156 | tool = task.tool(task.which("bash", true));
157 | tool.arg(scriptPath);
158 | } else {
159 | tool = task.tool(task.which(scriptPath, true));
160 | }
161 |
162 | return tool;
163 | }
164 | }
165 |
--------------------------------------------------------------------------------
/Tasks/Terraform/src/TerraformTask.ts:
--------------------------------------------------------------------------------
1 | import path = require("path");
2 | import fs = require("fs");
3 | import os = require("os");
4 |
5 | import { injectable } from "inversify";
6 | import { TerraformCommandRunner } from "./TerraformCommandRunner";
7 | import { TaskOptions } from './TaskOptions';
8 |
9 | @injectable()
10 | export class TerraformTask {
11 |
12 | constructor(
13 | private terraform : TerraformCommandRunner,
14 | private options: TaskOptions)
15 | {
16 |
17 | }
18 |
19 | public async run() {
20 | switch(this.options.command) {
21 | case "init":
22 | let authenticate = this.options.provider == "Remote";
23 | await this.terraform.init(["-input=false"], authenticate);
24 | break;
25 | case "validate":
26 | await this.terraform.exec(["validate"], false);
27 | break;
28 | case "plan":
29 | await this.terraform.plan(["-input=false"], this.options.variables, this.options.outputFile);
30 | break;
31 | case "apply":
32 | let args = ["apply", "-input=false", "-auto-approve"];
33 |
34 | if (this.options.planFile != "") {
35 | args.push(this.options.planFile);
36 | }
37 |
38 | await this.terraform.exec(args);
39 | break;
40 | case "destroy":
41 | await this.terraform.exec(["destroy", "-auto-approve=true"]);
42 | break;
43 | case "CLI":
44 | if(this.options.initialize) {
45 | await this.terraform.init(["-input=false"]);
46 | }
47 |
48 | var path = this.initScriptAtPath();
49 | await this.terraform.cli(path);
50 | break;
51 | default:
52 | throw new Error("Invalid command");
53 | }
54 | }
55 |
56 | /**
57 | * Loads the specified CLI script into a file and returns the path
58 | */
59 | private initScriptAtPath(): string {
60 | let scriptPath: string;
61 |
62 | if (this.options.scriptLocation === "scriptPath") {
63 | if (!this.options.scriptPath){
64 | throw new Error("Script path not specified");
65 | }
66 |
67 | scriptPath = this.options.scriptPath;
68 | }
69 | else {
70 | if (!this.options.script){
71 | throw new Error("Script not specified");
72 | }
73 |
74 | var tmpDir = this.options.tempDir || os.tmpdir();
75 |
76 | if (os.type() != "Windows_NT") {
77 | scriptPath = path.join(tmpDir, "terraformclitaskscript" + new Date().getTime() + ".sh");
78 | }
79 | else {
80 | scriptPath = path.join(tmpDir, "terraformclitaskscript" + new Date().getTime() + ".bat");
81 | }
82 |
83 | this.createFile(scriptPath, this.options.script);
84 | }
85 |
86 | return scriptPath;
87 | }
88 |
89 | /**
90 | * Creates a file from a string at the given path
91 | */
92 | private createFile(filePath: string, data: string) {
93 | try {
94 | fs.writeFileSync(filePath, data);
95 | }
96 | catch (err) {
97 | this.deleteFile(filePath);
98 | throw err;
99 | }
100 | }
101 |
102 | /**
103 | * Deletes a file at the given path if it exists
104 | */
105 | private deleteFile(filePath: string): void {
106 | if (fs.existsSync(filePath)) {
107 | try {
108 | //delete the publishsetting file created earlier
109 | fs.unlinkSync(filePath);
110 | }
111 | catch (err) {
112 | //error while deleting should not result in task failure
113 | console.error(err.toString());
114 | }
115 | }
116 | }
117 | }
--------------------------------------------------------------------------------
/Tasks/Terraform/src/types.ts:
--------------------------------------------------------------------------------
1 | const TYPES = {
2 | TerraformProvider: Symbol.for("TerraformProvider")
3 | };
4 |
5 | export { TYPES };
--------------------------------------------------------------------------------
/Tasks/Terraform/task.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "653507a3-d921-47dc-8fed-c5649d8e9e0e",
3 | "name": "terraform",
4 | "friendlyName": "Terraform",
5 | "description": "A task for running Terraform commands",
6 | "helpMarkDown": "",
7 | "category": "Tool",
8 | "author": "HashiCorp",
9 | "visibility": [
10 | "Build",
11 | "Release"
12 | ],
13 | "version": {
14 | "Major": 0,
15 | "Minor": 0,
16 | "Patch": 1
17 | },
18 | "instanceNameFormat": "Terraform",
19 | "groups": [
20 | {
21 | "name": "backendAzure",
22 | "displayName": "Azure Backend Configuration",
23 | "isExpanded": true,
24 | "visibleRule": "backend = Azure"
25 | },
26 | {
27 | "name": "Plan",
28 | "displayName": "Plan settings",
29 | "isExpanded": true,
30 | "visibleRule": "command = Plan"
31 | },
32 | {
33 | "name": "Apply",
34 | "displayName": "Apply settings",
35 | "isExpanded": true,
36 | "visibleRule": "command = Apply"
37 | },
38 | {
39 | "name": "CLI",
40 | "displayName": "CLI Script",
41 | "isExpanded": true,
42 | "visibleRule": "command = CLI"
43 | },
44 | {
45 | "name": "advanced",
46 | "displayName": "Advanced",
47 | "isExpanded": false
48 | }
49 | ],
50 | "inputs": [
51 | {
52 | "name": "command",
53 | "type": "picklist",
54 | "label": "Command",
55 | "defaultValue": "init",
56 | "required": true,
57 | "helpMarkDown": "Select the Terraform command to run, or select CLI to run a Terraform command line script in an authenticated environment.",
58 | "options": {
59 | "init": "init",
60 | "plan": "plan",
61 | "apply": "apply",
62 | "validate": "validate",
63 | "destroy": "destroy",
64 | "CLI": "CLI"
65 | }
66 | },
67 | {
68 | "name": "provider",
69 | "type": "picklist",
70 | "label": "Provider",
71 | "defaultValue": "Azure",
72 | "required": true,
73 | "helpMarkDown": "Select a Terraform provider",
74 | "visibleRule": "command != validate",
75 | "options": {
76 | "Azure": "Azure",
77 | "AWS": "AWS",
78 | "GCP": "GCP",
79 | "Remote": "Remote"
80 | }
81 | },
82 |
83 | {
84 | "name": "providerAzureConnectedServiceName",
85 | "type": "connectedService:AzureRM",
86 | "label": "Provider Azure subscription",
87 | "required": true,
88 | "helpMarkDown": "Select the Azure Resource Manager subscription your Terraform will execute against.",
89 | "visibleRule": "provider = Azure",
90 | "properties": {
91 | "EndpointFilterRule": "ScopeLevel != ManagementGroup"
92 | }
93 | },
94 | {
95 | "name": "scriptLocation",
96 | "type": "pickList",
97 | "label": "Script Location",
98 | "defaultValue": "inlineScript",
99 | "required": false,
100 | "helpMarkDown": "Type of script: File path or Inline script",
101 | "options": {
102 | "inlineScript": "Inline script",
103 | "scriptPath": "Script path"
104 | },
105 | "groupName": "CLI"
106 | },
107 | {
108 | "name": "scriptPath",
109 | "type": "filePath",
110 | "label": "Script Path",
111 | "defaultValue": "",
112 | "required": false,
113 | "visibleRule": "scriptLocation = scriptPath",
114 | "helpMarkDown": "Fully qualified path of the script(.bat or .cmd when using Windows based agent and .sh when using linux based agent) or a path relative to the the default working directory",
115 | "groupName": "CLI"
116 | },
117 | {
118 | "name": "script",
119 | "type": "multiLine",
120 | "label": "Inline Script",
121 | "defaultValue": "",
122 | "required": false,
123 | "visibleRule": "scriptLocation = inlineScript",
124 | "helpMarkDown": "You can write your terraform scripts inline here",
125 | "properties": {
126 | "resizable": "true",
127 | "rows": "10",
128 | "maxLength": "5000"
129 | },
130 | "groupName": "CLI"
131 | },
132 | {
133 | "name": "initialize",
134 | "type": "bool",
135 | "label": "Initialize Terraform",
136 | "defaultValue": "true",
137 | "visibleRule": "command = CLI",
138 | "required": false,
139 | "helpMarkDown": "Initializes terraform for the CLI"
140 | },
141 |
142 | {
143 | "name": "backend",
144 | "type": "picklist",
145 | "label": "Backend",
146 | "defaultValue": "Azure",
147 | "required": true,
148 | "helpMarkDown": "Select a backend for your Terraform state",
149 | "visibleRule": "command = init || initialize = true",
150 | "options": {
151 | "Azure": "Azure",
152 | "AWS": "AWS",
153 | "GCP": "GCP",
154 | "Remote": "Remote"
155 | }
156 | },
157 |
158 |
159 | {
160 | "name": "backendAzureUseProviderConnectedServiceForBackend",
161 | "type": "boolean",
162 | "label": "Use provider connection for backend",
163 | "defaultValue": "true",
164 | "required": false,
165 | "helpMarkDown": "Uses the providers connected service to access the storage backend",
166 | "groupName": "backendAzure"
167 | },
168 | {
169 | "name": "backendAzureConnectedServiceName",
170 | "type": "connectedService:AzureRM",
171 | "label": "Backend Azure subscription",
172 | "required": true,
173 | "helpMarkDown": "Select the Azure Resource Manager subscription your Terraform backend will live in.",
174 | "visibleRule": "backendAzureUseProviderConnectedServiceForBackend = false",
175 | "properties": {
176 | "EndpointFilterRule": "ScopeLevel != ManagementGroup"
177 | },
178 | "groupName": "backendAzure"
179 | },
180 | {
181 | "name": "backendAzureStorageAccountName",
182 | "type": "pickList",
183 | "label": "Azure Storage Account",
184 | "defaultValue": "",
185 | "required": true,
186 | "helpMarkDown": "Specify a pre-existing ARM storage account. It will be used to store your backend Terraform state.",
187 | "visibleRule": "backendAzureUseProviderConnectedServiceForBackend = false",
188 | "properties": {
189 | "EditableOptions": "True"
190 | },
191 | "groupName": "backendAzure"
192 | },
193 | {
194 | "name": "backendAzureProviderStorageAccountName",
195 | "type": "pickList",
196 | "label": "Azure Storage Account",
197 | "defaultValue": "",
198 | "required": true,
199 | "helpMarkDown": "Specify a pre-existing ARM storage account. It will be used to store your backend Terraform state.",
200 | "visibleRule": "backendAzureUseProviderConnectedServiceForBackend = true",
201 | "properties": {
202 | "EditableOptions": "True"
203 | },
204 | "groupName": "backendAzure"
205 | },
206 | {
207 | "name": "backendAzureContainerName",
208 | "type": "string",
209 | "label": "Container name",
210 | "defaultValue": "tfstate",
211 | "required": true,
212 | "helpMarkDown": "Name of the storage container to keep the Terraform state.",
213 | "groupName": "backendAzure"
214 | },
215 | {
216 | "name": "backendAzureStateFileKey",
217 | "type": "string",
218 | "label": "State file key",
219 | "defaultValue": "terraform.tfstate",
220 | "required": true,
221 | "helpMarkDown": "Name of the state file to use in the Terraform backend.",
222 | "groupName": "backendAzure"
223 | },
224 |
225 | {
226 | "name": "variables",
227 | "type": "multiLine",
228 | "label": "Variables",
229 | "defaultValue": "",
230 | "required": false,
231 | "groupName": "Plan",
232 | "helpMarkDown": "A JSON key/value object to add as variables"
233 | },
234 | {
235 | "name": "outputFile",
236 | "type": "string",
237 | "label": "Output file",
238 | "defaultValue": "",
239 | "required": false,
240 | "groupName": "Plan",
241 | "helpMarkDown": "The plan file to output. If left blank no plan file will be generated and output will only be visible in the logs."
242 | },
243 |
244 | {
245 | "name": "planFile",
246 | "type": "filePath",
247 | "label": "Plan File",
248 | "defaultValue": "",
249 | "required": false,
250 | "groupName": "Apply",
251 | "helpMarkDown": "Path to the plan file to apply. If no plan file is specified the default values will be applied"
252 | },
253 |
254 |
255 | {
256 | "name": "args",
257 | "type": "string",
258 | "label": "Additional Arguments",
259 | "defaultValue": "",
260 | "required": false,
261 | "groupName": "advanced",
262 | "helpMarkDown": "Additional arguments to pass"
263 | }
264 | ],
265 | "dataSourceBindings": [
266 | {
267 | "target": "backendAzureProviderStorageAccountName",
268 | "endpointId": "$(providerAzureConnectedServiceName)",
269 | "dataSourceName": "AzureStorageAccountRMandClassic"
270 | },
271 | {
272 | "target": "backendAzureStorageAccountName",
273 | "endpointId": "$(backendAzureConnectedServiceName)",
274 | "dataSourceName": "AzureStorageAccountRMandClassic"
275 | }
276 | ],
277 | "execution": {
278 | "Node": {
279 | "target": ".bin/src/Index.js"
280 | }
281 | }
282 | }
--------------------------------------------------------------------------------
/Tasks/Terraform/test/Provider/Azure/AzureProvider.spec.ts:
--------------------------------------------------------------------------------
1 | import { AzureProvider } from '../../../src/Provider/Azure/AzureProvider';
2 |
3 | // import { expect } from 'chai';
4 | // if you used the '@types/mocha' method to install mocha type definitions, uncomment the following line
5 | // import 'mocha';
6 |
7 | describe('Hello function', () => {
8 | it('should return hello world', () => {
9 | AzureProvider
10 | // const result = hello();
11 | // expect(result).to.equal('Hello World!');
12 | });
13 | });
--------------------------------------------------------------------------------
/Tasks/Terraform/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES6",
4 | "module": "commonjs",
5 | "moduleResolution": "node",
6 | "sourceMap": true,
7 | "strict": true,
8 | "outDir": ".bin",
9 | // "rootDir": "src",
10 | "experimentalDecorators": true,
11 | "emitDecoratorMetadata": true,
12 | "typeRoots": [
13 | "node_modules/@types"
14 | ],
15 | "types": [
16 | "node",
17 | "mocha",
18 | "q",
19 | "reflect-metadata"
20 | ]
21 | },
22 | "exclude": [
23 | "node_modules"
24 | ]
25 | }
26 |
--------------------------------------------------------------------------------
/Tasks/TerraformEnterprise/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hashicorp/azure-pipelines-extension-terraform/1f7b31dbc2ae32cb9b700b6f048ed7eb9ebddbaa/Tasks/TerraformEnterprise/icon.png
--------------------------------------------------------------------------------
/Tasks/TerraformEnterprise/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "azure-pipelines-terraform-task",
3 | "version": "0.0.1",
4 | "description": "A Terraform task for Azure Pipelines",
5 | "main": ".bin/src/Index.js",
6 | "author": "",
7 | "license": "MPL-2.0",
8 | "scripts": {
9 | "build": "tsc --build",
10 | "start": "node -r dotenv/config .bin/src/Index.js",
11 | "test": "ts-mocha test/**/*.spec.ts"
12 | },
13 | "dependencies": {
14 | "@azure/arm-storage": "^9.0.1",
15 | "@azure/ms-rest-nodeauth": "^2.0.2",
16 | "azure-pipelines-task-lib": "^2.9.3",
17 | "config": "^3.2.1",
18 | "inversify": "^5.0.1",
19 | "reflect-metadata": "^0.1.13",
20 | "request-prom": "^4.0.1",
21 | "tsyringe": "^3.3.0"
22 | },
23 | "devDependencies": {
24 | "@types/chai": "^4.2.3",
25 | "@types/expect": "^1.20.4",
26 | "@types/mocha": "^5.2.7",
27 | "@types/node": "^12.6.8",
28 | "@types/q": "^1.5.2",
29 | "chai": "^4.2.0",
30 | "copyfiles": "^2.1.0",
31 | "dotenv": "^8.2.0",
32 | "mocha": "^5.2.0",
33 | "mocha-junit-reporter": "^1.18.0",
34 | "tfx-cli": "^0.6.4",
35 | "ts-mocha": "^6.0.0",
36 | "ts-node": "^8.4.1",
37 | "tsconfig-paths": "^3.9.0",
38 | "typescript": "^3.3.3333"
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/Tasks/TerraformEnterprise/src/Index.ts:
--------------------------------------------------------------------------------
1 | import "reflect-metadata";
2 | import { Container } from "inversify";
3 | import { TaskResult } from "azure-pipelines-task-lib/task";
4 | import task = require('azure-pipelines-task-lib/task');
5 |
6 | import { TerraformEnterpriseTask } from './TerraformEnterpriseTask';
7 | import { TerraformApi } from "./TerraformApi";
8 | import { TaskOptions } from './TaskOptions';
9 |
10 |
11 |
12 | let container = new Container();
13 | let options = new TaskOptions();
14 |
15 | container.bind(TerraformEnterpriseTask).toSelf()
16 | container.bind(TaskOptions).toSelf()
17 | container.bind(TerraformApi).toSelf();
18 |
19 |
20 |
21 | var terraformEnterpriseTask = container.get(TerraformEnterpriseTask);
22 |
23 | terraformEnterpriseTask.run().then(function()
24 | {
25 | task.setResult(TaskResult.Succeeded, "Terraform successfully ran");
26 | }, function() {
27 | task.setResult(TaskResult.Failed, "Terraform failed to run");
28 | });
29 |
--------------------------------------------------------------------------------
/Tasks/TerraformEnterprise/src/TaskOptions.ts:
--------------------------------------------------------------------------------
1 | import task = require('azure-pipelines-task-lib/task');
2 | import { injectable } from "inversify";
3 |
4 | /**
5 | * Strong-type accessor for Task configuration
6 | */
7 | @injectable()
8 | export class TaskOptions {
9 |
10 | // Basic
11 | readonly command : string | undefined;
12 | readonly url : string | undefined;
13 | readonly organization : string | undefined;
14 | readonly token : string | undefined;
15 | readonly skipcertcheck : string | undefined;
16 | readonly workspace : string | undefined;
17 |
18 | // Lookup
19 | readonly lookupcommand : string | undefined;
20 |
21 | // Workspace
22 | readonly workspacecommand : string | undefined;
23 | readonly workspaceautoapply : boolean | undefined;
24 | readonly workspacedescription : string | undefined;
25 | readonly workspacefiletriggersenabled : boolean | undefined;
26 | readonly workspacequeueallruns : boolean | undefined;
27 | readonly workspacespeculativeenabled : boolean | undefined;
28 | readonly workspaceterraformversion : string | undefined;
29 | readonly workspacetriggerprefixes : string[] | undefined;
30 | readonly workspaceworkingdirectory : string | undefined;
31 | readonly workspacevcsrepo : boolean | undefined;
32 |
33 | // Workspace VCS Repo
34 | readonly vcsrepooauthtokenid : string | undefined;
35 | readonly vcsrepobranch : string | undefined;
36 | readonly vcsrepoingresssubmodules : boolean | undefined;
37 | readonly vcsrepoidentifier : string | undefined;
38 |
39 | // Runs
40 | readonly runcommand : string | undefined;
41 | readonly runisdestroy : boolean | undefined;
42 | readonly runmessage : string | undefined;
43 | readonly runconfigversion : string | undefined;
44 | readonly runid : string | undefined;
45 | readonly runapplycomment : string | undefined;
46 |
47 | // Variables
48 | readonly variablecommand : string | undefined;
49 | readonly variablekey : string | undefined;
50 | readonly variablevalue : string | undefined;
51 | readonly variablecategory : string | undefined;
52 | readonly variablehcl : boolean | undefined;
53 | readonly variablesensitive : boolean | undefined;
54 |
55 | /**
56 | * Creates and loads a well-formed options object
57 | */
58 | constructor() {
59 | // This can be massively improved, it should be automatic
60 | this.command = task.getInput("command", true);
61 | this.url = task.getInput("url");
62 | this.organization = task.getInput("organization");
63 | this.workspace = task.getInput("workspace");
64 | this.token = task.getInput("token");
65 | this.skipcertcheck = task.getInput("skipcertcheck");
66 | this.lookupcommand = task.getInput("lookupcommand");
67 | this.workspacecommand = task.getInput("workspacecommand");
68 | this.workspaceautoapply = task.getBoolInput("workspaceautoapply");
69 | this.workspacedescription = task.getInput("workspacedescription");
70 | this.workspacefiletriggersenabled = task.getBoolInput("workspacefiletriggersenabled");
71 | this.workspacequeueallruns = task.getBoolInput("workspacequeueallruns");
72 | this.workspacespeculativeenabled = task.getBoolInput("workspacespeculativeenabled");
73 | this.workspaceterraformversion = task.getInput("workspaceterraformversion");
74 | this.workspacetriggerprefixes = task.getDelimitedInput("workspacetriggerprefixs",",");
75 | this.workspaceworkingdirectory = task.getInput("workspaceworkingdirectory");
76 | this.workspacevcsrepo = task.getBoolInput("workspacevcsrepo");
77 | this.vcsrepooauthtokenid = task.getInput("vcsrepooauthtokenid");
78 | this.vcsrepoingresssubmodules = task.getBoolInput("vcsrepoingresssubmodules");
79 | this.vcsrepoidentifier = task.getInput("vcsrepoidentifier");
80 | this.runcommand = task.getInput("runcommand");
81 | this.runisdestroy = task.getBoolInput("runisdestroy");
82 | this.runmessage = task.getInput("runmessage");
83 | this.runconfigversion = task.getInput("runconfigversion");
84 | this.runid = task.getInput("runid");
85 | this.runapplycomment = task.getInput("runapplycomment");
86 | this.variablecommand = task.getInput("variablecommand");
87 | this.variablekey = task.getInput("variablekey");
88 | this.variablevalue = task.getInput("variablevalue");
89 | this.variablecategory = task.getInput("variablecategory");
90 | this.variablehcl = task.getBoolInput("variablehcl");
91 | this.variablesensitive = task.getBoolInput("variablesensitive");
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/Tasks/TerraformEnterprise/src/TerraformApi.ts:
--------------------------------------------------------------------------------
1 | import task = require('azure-pipelines-task-lib/task');
2 | import { injectable } from "inversify";
3 | import { TaskOptions } from "./TaskOptions";
4 | const axios = require('axios');
5 |
6 | @injectable()
7 | export class TerraformApi {
8 |
9 | public constructor(
10 | private options: TaskOptions
11 | ) {
12 | }
13 |
14 | public async call(url: string, method: string, body: string = "") {
15 | const skipcertcheck = this.options.skipcertcheck;
16 | const baseUrl = this.options.url
17 | const requestUrl = url;
18 | const metadata = body;
19 | console.log(body);
20 | const requestMethod = method;
21 | console.log(requestUrl);
22 | const accessToken = this.options.token;
23 | console.log(accessToken);
24 |
25 | try {
26 | await axios({
27 | method: requestMethod,
28 | baseURL: baseUrl,
29 | url: requestUrl,
30 | data: metadata,
31 | headers: {
32 | 'Authorization': 'Bearer ' + accessToken,
33 | 'Content-Type': 'application/vnd.api+json'
34 | }
35 | }).then((response: Response) => {
36 | console.log(response.statusText);
37 | });
38 | }
39 | catch (error) {
40 | console.log("Unable to update Terraform Api, Error: " + error);
41 | throw new Error("Unable to update Terraform Api, Error: " + error);
42 | }
43 | }
44 |
45 |
46 | public async workspaceIdLookup(url: string) {
47 | const skipcertcheck = this.options.skipcertcheck;
48 | const baseUrl = this.options.url
49 | const requestUrl = url;
50 | console.log(requestUrl);
51 | const accessToken = this.options.token;
52 | interface ServerResponse {
53 | data: ServerDataWrapper
54 | }
55 | interface ServerDataWrapper {
56 | data: ServerData
57 | }
58 | interface ServerData {
59 | id: string
60 | }
61 |
62 | try {
63 | let response = await axios({
64 | method: 'get',
65 | baseURL: baseUrl,
66 | url: requestUrl,
67 | headers: {
68 | 'Authorization': 'Bearer ' + accessToken,
69 | 'Content-Type': 'application/vnd.api+json'
70 | }
71 | });
72 | console.log(response.data.data.id);
73 | return response.data.data.id;
74 | }
75 | catch (error) {
76 | console.log("Unable to update Terraform Api, Error: " + error);
77 | throw new Error("Unable to update Terraform Api, Error: " + error);
78 | }
79 | }
80 |
81 | public async latestRunIdLookup(url: string) {
82 | const skipcertcheck = this.options.skipcertcheck;
83 | const baseUrl = this.options.url
84 | const requestUrl = url;
85 | console.log(requestUrl);
86 | const accessToken = this.options.token;
87 | interface ServerResponse {
88 | data: ServerDataWrapper
89 | }
90 | interface ServerDataWrapper {
91 | data: ServerData
92 | }
93 | interface ServerData {
94 | id: string
95 | }
96 |
97 | try {
98 | let response = await axios({
99 | method: 'get',
100 | baseURL: baseUrl,
101 | url: requestUrl,
102 | headers: {
103 | 'Authorization': 'Bearer ' + accessToken,
104 | 'Content-Type': 'application/vnd.api+json'
105 | }
106 | });
107 | console.log(response.data.data[0].id);
108 | return response.data.data[0].id;
109 | }
110 | catch (error) {
111 | console.log("Unable to update Terraform Api, Error: " + error);
112 | throw new Error("Unable to update Terraform Api, Error: " + error);
113 | }
114 | }
115 |
116 | public async variableIdLookup(url: string, variable: string) {
117 | const skipcertcheck = this.options.skipcertcheck;
118 | const baseUrl = this.options.url
119 | const requestUrl = url;
120 | console.log(requestUrl);
121 | const accessToken = this.options.token;
122 | interface ServerResponse {
123 | data: ServerDataWrapper
124 | }
125 | interface ServerDataWrapper {
126 | data: ServerData
127 | }
128 | interface ServerData {
129 | id: string
130 | }
131 |
132 | try {
133 | let response = await axios({
134 | method: 'get',
135 | baseURL: baseUrl,
136 | url: requestUrl,
137 | headers: {
138 | 'Authorization': 'Bearer ' + accessToken,
139 | 'Content-Type': 'application/vnd.api+json'
140 | }
141 | });
142 | for (var i=0; i < response.data.data.length; i++) {
143 | if (response.data.data[i]['attributes']['key'] == variable) {
144 | console.log(response.data.data[i]['id']);
145 | return response.data.data[i]['id'];
146 | }
147 | }
148 | }
149 | catch (error) {
150 | console.log("Unable to update Terraform Api, Error: " + error);
151 | throw new Error("Unable to update Terraform Api, Error: " + error);
152 | }
153 | }
154 | }
155 |
--------------------------------------------------------------------------------
/Tasks/TerraformEnterprise/src/TerraformEnterpriseTask.ts:
--------------------------------------------------------------------------------
1 | import path = require("path");
2 | import fs = require("fs");
3 | import os = require("os");
4 |
5 | import { injectable } from "inversify";
6 | import { TerraformApi } from "./TerraformApi";
7 | import { TaskOptions } from './TaskOptions';
8 |
9 | @injectable()
10 | export class TerraformEnterpriseTask {
11 |
12 | constructor(
13 | private terraformapi : TerraformApi,
14 | private options: TaskOptions)
15 | {
16 |
17 | }
18 |
19 | public async run() {
20 | console.log(this.options.command);
21 | switch(this.options.command) {
22 | case "lookup":
23 | switch(this.options.lookupcommand) {
24 | case "workspaceId":
25 | var workspacelookupurl = "/organizations/" + this.options.organization + "/workspaces/" + this.options.workspace;
26 | var workspaceId = await this.terraformapi.workspaceIdLookup(workspacelookupurl)
27 | return workspaceId;
28 | break;
29 | case "latestRunId":
30 | var workspacelookupurl = "/organizations/" + this.options.organization + "/workspaces/" + this.options.workspace;
31 | var workspaceId = await this.terraformapi.workspaceIdLookup(workspacelookupurl)
32 | var runlookupurl = "/workspaces/" + workspaceId + "/runs?page%5Bsize%5D=1";
33 | var runId = await this.terraformapi.latestRunIdLookup(runlookupurl);
34 | console.log(runId);
35 | return runId;
36 | break;
37 | case "variableId":
38 | const variablekey = "" + this.options.variablekey;
39 | var variablelookupurl = "/vars?filter%5Borganization%5D%5Bname%5D=" + this.options.organization + "&filter%5Bworkspace%5D%5Bname%5D=" + this.options.workspace;
40 | var variableId = await this.terraformapi.variableIdLookup(variablelookupurl, variablekey);
41 | return variableId;
42 | default:
43 | console.log("Invalid lookup command");
44 | throw new Error("Invalid lookup command");
45 | break;
46 | }
47 | break;
48 | case "workspace":
49 | switch(this.options.workspacecommand) {
50 | case "create":
51 | console.log('create');
52 | var method = 'post';
53 | var endpoint = '/workspaces';
54 | var payload = this.workspacePayload();
55 | break;
56 | case "update":
57 | var method = 'patch';
58 | var endpoint = '/workspaces/' + this.options.workspace;
59 | var payload = this.workspacePayload();
60 | break;
61 | case "delete":
62 | var method = 'delete';
63 | var endpoint = '/workspaces/' + this.options.workspace;
64 | var payload = this.workspacePayload();
65 | break;
66 | default:
67 | console.log("Invalid workspace command");
68 | throw new Error("Invalid workspace command");
69 | break;
70 | }
71 |
72 | console.log('workspace');
73 | console.log('payload');
74 | var url = "/organizations/" + this.options.organization + endpoint;
75 | console.log(url);
76 | await this.terraformapi.call(url, method, JSON.stringify(payload));
77 | break;
78 | case "run":
79 | console.log('run');
80 | switch(this.options.runcommand) {
81 | case "create":
82 | console.log('create');
83 | const runconfigversion = this.options.runconfigversion;
84 | const runisdestroy = this.options.runisdestroy;
85 | const runmessage = this.options.runmessage;
86 | var url = '/runs';
87 | var workspacelookupurl = "/organizations/" + this.options.organization + "/workspaces/" + this.options.workspace;
88 | var method = 'post';
89 | var attributes:any = {};
90 | var relationshipsworkspacedata:any = {};
91 | var relationshipsworkspace:any = {};
92 | var relationshipsconfigversiondata:any = {};
93 | var relationshipsconfigversion:any = {};
94 | var relationships:any = {};
95 | var payloaddata:any = {};
96 | var payload:any = {};
97 | console.log("Calling workspace lookup");
98 | var workspaceId = await this.terraformapi.workspaceIdLookup(workspacelookupurl)
99 | console.log(workspaceId);
100 | relationshipsworkspacedata["type"] = "workspaces";
101 | relationshipsworkspacedata["id"] = workspaceId;
102 | relationshipsworkspace["data"] = relationshipsworkspacedata;
103 | relationships["workspace"] = relationshipsworkspace;
104 | if ( runconfigversion ) {
105 | relationshipsconfigversiondata["type"] = "configuration-versions";
106 | relationshipsconfigversiondata["id"] = runconfigversion;
107 | relationshipsconfigversion["data"] = relationshipsconfigversiondata;
108 | relationships["configuration-version"] = relationshipsconfigversion;
109 | }
110 | console.log(relationships);
111 | if ( runisdestroy === true ) {
112 | attributes["is-destroy"] = runisdestroy
113 | }
114 | if ( runmessage ) {
115 | attributes["message"] = runmessage
116 | }
117 | console.log(attributes);
118 | payloaddata["attributes"] = attributes;
119 | payloaddata["relationships"] = relationships;
120 | payloaddata["type"] = "runs";
121 | payload["data"] = payloaddata;
122 | console.log(payload);
123 | await this.terraformapi.call(url, method, JSON.stringify(payload));
124 | break;
125 | case "apply":
126 | console.log('apply');
127 | const runid = this.options.runid;
128 | const runapplycomment = this.options.runapplycomment;
129 | var url = '/runs/' + runid + '/actions/apply';
130 | var method = 'post';
131 | var payload:any = {};
132 | if ( runapplycomment ) {
133 | payload["comment"] = runapplycomment;
134 | }
135 | await this.terraformapi.call(url, method, JSON.stringify(payload));
136 | break;
137 | default:
138 | console.log("Invalid command");
139 | throw new Error("Invalid command");
140 | break;
141 | }
142 | break;
143 | case "variable":
144 | console.log('variable');
145 | switch(this.options.variablecommand) {
146 | case "create":
147 | console.log('create');
148 | var method = 'post';
149 | var url = '/vars';
150 | var workspacelookupurl = "/organizations/" + this.options.organization + "/workspaces/" + this.options.workspace;
151 | var workspaceId = await this.terraformapi.workspaceIdLookup(workspacelookupurl)
152 | var payload = this.variablePayload(workspaceId);
153 | break;
154 | case "update":
155 | var method = 'patch';
156 | var workspacelookupurl = "/organizations/" + this.options.organization + "/workspaces/" + this.options.workspace;
157 | var workspaceId = await this.terraformapi.workspaceIdLookup(workspacelookupurl)
158 | var variablekey = "" + this.options.variablekey;
159 | var variablelookupurl = "/vars?filter%5Borganization%5D%5Bname%5D=" + this.options.organization + "&filter%5Bworkspace%5D%5Bname%5D=" + this.options.workspace;
160 | var variableId = await this.terraformapi.variableIdLookup(variablelookupurl, variablekey);
161 | console.log(variableId);
162 | var url = '/vars/' + variableId;
163 | console.log('building payload');
164 | var payload = this.variablePayload(workspaceId, variableId);
165 | console.log('finish payload');
166 | break;
167 | case "delete":
168 | var method = 'delete';
169 | var variablekey = "" + this.options.variablekey;
170 | var variablelookupurl = "/vars?filter%5Borganization%5D%5Bname%5D=" + this.options.organization + "&filter%5Bworkspace%5D%5Bname%5D=" + this.options.workspace;
171 | var variableId = await this.terraformapi.variableIdLookup(variablelookupurl, variablekey);
172 | var url = '/vars/' + variableId;
173 | var payload = this.workspacePayload();
174 | break;
175 | default:
176 | console.log("Invalid workspace command");
177 | throw new Error("Invalid workspace command");
178 | break;
179 | }
180 | console.log('calling terraform api for variable');
181 | await this.terraformapi.call(url, method, JSON.stringify(payload));
182 | break;
183 | default:
184 | console.log("Invalid command");
185 | throw new Error("Invalid command");
186 | break;
187 | }
188 | }
189 |
190 | private workspacePayload() {
191 | const sourcename = 'Created by Azure DevOps Pipeline Extension for Terraform Enterprise';
192 | const sourceurl = 'https://github.com/hashicorp/azure-pipelines-extension-terraform';
193 |
194 | const workspace = this.options.workspace;
195 | const autoapply = this.options.workspaceautoapply;
196 | const description = this.options.workspacedescription;
197 | const filetriggersenabled = this.options.workspacefiletriggersenabled;
198 | const queueallruns = this.options.workspacequeueallruns;
199 | const speculativeenabled = this.options.workspacespeculativeenabled;
200 | const terraformversion = this.options.workspaceterraformversion;
201 | const triggerprefixes = this.options.workspacetriggerprefixes;
202 | const workingdirectory = this.options.workspaceworkingdirectory;
203 | const vcsrepo = this.options.workspacevcsrepo;
204 | const vcsrepotokenid = this.options.vcsrepooauthtokenid;
205 | const vcsrepobranch = this.options.vcsrepobranch;
206 | const vcsrepoingresssubmodules = this.options.vcsrepoingresssubmodules;
207 | const vcsrepoidentifier = this.options.vcsrepoidentifier;
208 |
209 | var args:any = {}
210 |
211 | args["name"] = workspace;
212 | args["source-name"] = sourcename;
213 | args["source-url"] = sourceurl;
214 | // this.getBoolInput seems to return false for undefined, so we are setting this to only run if the non-default value is set.
215 | if ( autoapply === true ) {
216 | args["auto-apply"] = autoapply
217 | }
218 | if ( description ) {
219 | args["description"] = description
220 | }
221 | // this.getBoolInput seems to return false for undefined, so we are setting this to only run if the non-default value is set.
222 | if ( filetriggersenabled === false ) {
223 | args["file-triggers-enabled"] = filetriggersenabled
224 | }
225 | // this.getBoolInput seems to return false for undefined, so we are setting this to only run if the non-default value is set.
226 | if ( queueallruns === true ) {
227 | args["queue-all-runs"] = queueallruns
228 | }
229 | // this.getBoolInput seems to return false for undefined, so we are setting this to only run if the non-default value is set.
230 | if ( speculativeenabled === false ) {
231 | args["speculative-enabled"] = speculativeenabled
232 | }
233 | if ( terraformversion ) {
234 | args["terraform-version"] = terraformversion
235 | }
236 | // this.getDelimintedInput seems to return an empty array for undefined, so we are checking for an array with data inside.
237 | if ( triggerprefixes != undefined && triggerprefixes.length > 0 ) {
238 | args["trigger-prefixes"] = triggerprefixes
239 | }
240 | if ( workingdirectory ) {
241 | args["working-directory"] = workingdirectory
242 | }
243 | // this.getBoolInput seems to return false for undefined, so we are setting this to only run if the non-default value is set.
244 | if ( vcsrepo === true ) {
245 | var vcsrepoPayload:any = {}
246 | if ( vcsrepotokenid ) {
247 | vcsrepoPayload["oauth-token-id"] = vcsrepotokenid
248 | }
249 | if ( vcsrepobranch ) {
250 | vcsrepoPayload["branch"] = vcsrepobranch
251 | }
252 | // this.getBoolInput seems to return false for undefined, so we are setting this to only run if the non-default value is set.
253 | if ( vcsrepoingresssubmodules === true ) {
254 | vcsrepoPayload["ingress-submodules"] = vcsrepoingresssubmodules
255 | }
256 | if ( vcsrepoidentifier ) {
257 | vcsrepoPayload["identifier"] = vcsrepoidentifier
258 | }
259 | args["vcs-repo"] = vcsrepoPayload;
260 | }
261 | console.log(args);
262 | var attributesPayload:any = {}
263 | console.log("created empty variable")
264 | attributesPayload["attributes"] = args
265 | console.log(attributesPayload);
266 | var payload:any = {}
267 | payload["data"] = attributesPayload;
268 | console.log(payload);
269 |
270 | return payload;
271 | }
272 |
273 | private variablePayload(workspaceId: string, variableId: string = "") {
274 | const variablekey = this.options.variablekey;
275 | const variablevalue = this.options.variablevalue;
276 | const variablecategory = this.options.variablecategory;
277 | const variablehcl = this.options.variablehcl;
278 | const variablesensitive = this.options.variablesensitive;
279 | var payload:any = {};
280 | var payloaddata:any = {};
281 | var attributes:any = {};
282 | var relationships:any = {};
283 | var relationshipsworkspace:any = {};
284 | var relationshipsworkspacedata:any = {};
285 | attributes["key"] = variablekey;
286 | attributes["value"] = variablevalue;
287 | attributes["category"] = variablecategory;
288 | // this.getBoolInput seems to return false for undefined, so we are setting this to only run if the non-default value is set.
289 | if ( variablehcl === true ) {
290 | attributes["hcl"] = variablehcl;
291 | }
292 | // this.getBoolInput seems to return false for undefined, so we are setting this to only run if the non-default value is set.
293 | if ( variablesensitive === true ) {
294 | attributes["sensitive"] = variablesensitive;
295 | }
296 | relationshipsworkspacedata["type"] = 'workspaces';
297 | relationshipsworkspacedata["id"] = workspaceId;
298 | relationshipsworkspace["data"] = relationshipsworkspacedata;
299 | relationships["workspace"] = relationshipsworkspace;
300 | payloaddata["type"] = "vars";
301 | if ( variableId != "" ) {
302 | payloaddata["id"] = variableId;
303 | }
304 | payloaddata["attributes"] = attributes;
305 | payloaddata["relationships"] = relationships;
306 | payload["data"] = payloaddata;
307 |
308 | return payload;
309 | }
310 | }
311 |
--------------------------------------------------------------------------------
/Tasks/TerraformEnterprise/task.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "506f3bf4-69c9-4812-a879-ccfc035f174c",
3 | "name": "terraformEnterprise",
4 | "friendlyName": "Terraform Enterprise",
5 | "description": "A Task for Terraform Enterprise to manage workspaces, run plan, and apply",
6 | "author": "HashiCorp",
7 | "helpMarkDown": "",
8 | "category": "Tool",
9 | "visibility": [
10 | "Build",
11 | "Release"
12 | ],
13 | "demands": [],
14 | "version": {
15 | "Major": 0,
16 | "Minor": 0,
17 | "Patch": 1
18 | },
19 | "instanceNameFormat": "Terraform Enterprise",
20 | "inputs": [
21 | {
22 | "name": "terraformCloudServiceConnection",
23 | "type": "connectedService:terraformServiceEndpoint",
24 | "label": "Terraform Cloud Connection",
25 | "required": true,
26 | "helpMarkDown": "Select the Terraform Cloud/Enterprise endpoint."
27 | },
28 | {
29 | "name": "command",
30 | "type": "picklist",
31 | "label": "Command",
32 | "defaultValue": "workspace",
33 | "required": true,
34 | "helpMarkDown": "Select the Terraform API command to call",
35 | "options": {
36 | "workspace": "Manage Workspace",
37 | "queuePlan": "Queue Plan",
38 | "confirmApply": "Confirm & Apply",
39 | "discardRun": "Discard Run",
40 | "addComment": "Add Comment",
41 | "cancelRun": "Cancel Run",
42 | "overrideContinue": "Override & Continue",
43 | "checkRun": "Check Run Status"
44 | }
45 | },
46 | {
47 | "name": "workspaceName",
48 | "type": "string",
49 | "label": "Name of Workspace",
50 | "required": true,
51 | "helpMarkDown": "Name of Workspace.",
52 | "visibleRule": "command = workspace"
53 | },
54 | {
55 | "name": "action",
56 | "type": "pickList",
57 | "label": "Action",
58 | "required": true,
59 | "helpMarkDown": "Action to take.",
60 | "visibleRule": "command = workspace",
61 | "options": {
62 | "create": "Create",
63 | "update": "Update",
64 | "delete": "Delete"
65 | }
66 | },
67 | ],
68 | "execution": {
69 | "Node": {
70 | "target": ".bin/Index.js"
71 | }
72 | },
73 | "outputVariables": [
74 | ],
75 | "messages": {
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/Tasks/TerraformEnterprise/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES6",
4 | "module": "commonjs",
5 | "moduleResolution": "node",
6 | "sourceMap": true,
7 | "strict": true,
8 | "outDir": ".bin",
9 | // "rootDir": "src",
10 | "experimentalDecorators": true,
11 | "emitDecoratorMetadata": true,
12 | "typeRoots": [
13 | "node_modules/@types"
14 | ],
15 | "types": [
16 | "node",
17 | "mocha",
18 | "q",
19 | "reflect-metadata"
20 | ]
21 | },
22 | "exclude": [
23 | "node_modules"
24 | ]
25 | }
26 |
--------------------------------------------------------------------------------
/Tasks/TerraformInstaller/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hashicorp/azure-pipelines-extension-terraform/1f7b31dbc2ae32cb9b700b6f048ed7eb9ebddbaa/Tasks/TerraformInstaller/icon.png
--------------------------------------------------------------------------------
/Tasks/TerraformInstaller/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "azure-pipelines-terraform-installer",
3 | "version": "0.0.1",
4 | "lockfileVersion": 1,
5 | "requires": true,
6 | "dependencies": {
7 | "@types/mocha": {
8 | "version": "5.2.7",
9 | "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.7.tgz",
10 | "integrity": "sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ=="
11 | },
12 | "@types/node": {
13 | "version": "6.14.7",
14 | "resolved": "https://registry.npmjs.org/@types/node/-/node-6.14.7.tgz",
15 | "integrity": "sha512-YbPXbaynBTe0pVExPhL76TsWnxSPeFAvImIsmylpBWn/yfw+lHy+Q68aawvZHsgskT44ZAoeE67GM5f+Brekew=="
16 | },
17 | "@types/q": {
18 | "version": "1.5.2",
19 | "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.2.tgz",
20 | "integrity": "sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw=="
21 | },
22 | "@types/semver": {
23 | "version": "5.5.0",
24 | "resolved": "https://registry.npmjs.org/@types/semver/-/semver-5.5.0.tgz",
25 | "integrity": "sha512-41qEJgBH/TWgo5NFSvBCJ1qkoi3Q6ONSF2avrHq1LVEZfYpdHmj0y9SuTK+u9ZhG1sYQKBL1AWXKyLWP4RaUoQ=="
26 | },
27 | "@types/uuid": {
28 | "version": "3.4.5",
29 | "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-3.4.5.tgz",
30 | "integrity": "sha512-MNL15wC3EKyw1VLF+RoVO4hJJdk9t/Hlv3rt1OL65Qvuadm4BYo6g9ZJQqoq7X8NBFSsQXgAujWciovh2lpVjA==",
31 | "requires": {
32 | "@types/node": "*"
33 | }
34 | },
35 | "azure-pipelines-task-lib": {
36 | "version": "2.8.0",
37 | "resolved": "https://registry.npmjs.org/azure-pipelines-task-lib/-/azure-pipelines-task-lib-2.8.0.tgz",
38 | "integrity": "sha512-PR8oap9z2j+o455W3PwAfB4SX1p4GdJc9OHQaQV0V+iQS1IBY6dVgcNSQMkHAXb0V1bbuLOFBLanXPe5eSgGTQ==",
39 | "requires": {
40 | "minimatch": "3.0.4",
41 | "mockery": "^1.7.0",
42 | "q": "^1.1.2",
43 | "semver": "^5.1.0",
44 | "shelljs": "^0.3.0",
45 | "uuid": "^3.0.1"
46 | }
47 | },
48 | "azure-pipelines-tool-lib": {
49 | "version": "0.11.0",
50 | "resolved": "https://registry.npmjs.org/azure-pipelines-tool-lib/-/azure-pipelines-tool-lib-0.11.0.tgz",
51 | "integrity": "sha512-Bg3usPtOs/YvM5V9dOEwxvsructz/BTsO5mVgLQ013kJsQN9DJrTjZMAgxxdaeW1H8KPHMz/40ix1T7XZ++VCw==",
52 | "requires": {
53 | "@types/semver": "^5.3.0",
54 | "@types/uuid": "^3.0.1",
55 | "azure-pipelines-task-lib": "^2.7.1",
56 | "semver": "^5.3.0",
57 | "semver-compare": "^1.0.0",
58 | "typed-rest-client": "1.0.9",
59 | "uuid": "^3.0.1"
60 | }
61 | },
62 | "balanced-match": {
63 | "version": "1.0.0",
64 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
65 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
66 | },
67 | "brace-expansion": {
68 | "version": "1.1.11",
69 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
70 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
71 | "requires": {
72 | "balanced-match": "^1.0.0",
73 | "concat-map": "0.0.1"
74 | }
75 | },
76 | "concat-map": {
77 | "version": "0.0.1",
78 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
79 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
80 | },
81 | "minimatch": {
82 | "version": "3.0.4",
83 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
84 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
85 | "requires": {
86 | "brace-expansion": "^1.1.7"
87 | }
88 | },
89 | "mockery": {
90 | "version": "1.7.0",
91 | "resolved": "https://registry.npmjs.org/mockery/-/mockery-1.7.0.tgz",
92 | "integrity": "sha1-9O3g2HUMHJcnwnLqLGBiniyaHE8="
93 | },
94 | "q": {
95 | "version": "1.5.1",
96 | "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz",
97 | "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc="
98 | },
99 | "semver": {
100 | "version": "5.7.0",
101 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz",
102 | "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA=="
103 | },
104 | "semver-compare": {
105 | "version": "1.0.0",
106 | "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz",
107 | "integrity": "sha1-De4hahyUGrN+nvsXiPavxf9VN/w="
108 | },
109 | "shelljs": {
110 | "version": "0.3.0",
111 | "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.3.0.tgz",
112 | "integrity": "sha1-NZbmMHp4FUT1kfN9phg2DzHbV7E="
113 | },
114 | "tunnel": {
115 | "version": "0.0.4",
116 | "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.4.tgz",
117 | "integrity": "sha1-LTeFoVjBdMmhbcLARuxfxfF0IhM="
118 | },
119 | "typed-rest-client": {
120 | "version": "1.0.9",
121 | "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.0.9.tgz",
122 | "integrity": "sha512-iOdwgmnP/tF6Qs+oY4iEtCf/3fnCDl7Gy9LGPJ4E3M4Wj3uaSko15FVwbsaBmnBqTJORnXBWVY5306D4HH8oiA==",
123 | "requires": {
124 | "tunnel": "0.0.4",
125 | "underscore": "1.8.3"
126 | },
127 | "dependencies": {
128 | "underscore": {
129 | "version": "1.8.3",
130 | "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.8.3.tgz",
131 | "integrity": "sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI="
132 | }
133 | }
134 | },
135 | "typescript": {
136 | "version": "3.6.4",
137 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.6.4.tgz",
138 | "integrity": "sha512-unoCll1+l+YK4i4F8f22TaNVPRHcD9PA3yCuZ8g5e0qGqlVlJ/8FSateOLLSagn+Yg5+ZwuPkL8LFUc0Jcvksg==",
139 | "dev": true
140 | },
141 | "uuid": {
142 | "version": "3.3.2",
143 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",
144 | "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA=="
145 | }
146 | }
147 | }
148 |
--------------------------------------------------------------------------------
/Tasks/TerraformInstaller/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "azure-pipelines-terraform-installer",
3 | "version": "0.0.1",
4 | "description": "A Terraform installer task for Azure Pipelines",
5 | "main": ".bin/Index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "author": "",
10 | "license": "MPL-2.0",
11 | "dependencies": {
12 | "@types/mocha": "^5.2.7",
13 | "@types/node": "^6.0.101",
14 | "@types/q": "^1.5.0",
15 | "azure-pipelines-task-lib": "2.8.0",
16 | "azure-pipelines-tool-lib": "0.11.0"
17 | },
18 | "devDependencies": {
19 | "typescript": "^3.3.3333"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Tasks/TerraformInstaller/src/Index.ts:
--------------------------------------------------------------------------------
1 | import tasks = require('azure-pipelines-task-lib/task');
2 | import tools = require('azure-pipelines-tool-lib/tool');
3 | import { ToolRunner } from 'azure-pipelines-task-lib/toolrunner';
4 | import path = require('path');
5 | import * as installer from './TerraformInstaller';
6 |
7 | async function configureTerraform() {
8 | let inputVersion = tasks.getInput("terraformVersion", true);
9 | let terraformPath = await installer.downloadTerraform(inputVersion);
10 | let envPath = process.env['PATH'];
11 |
12 | // Prepend the tools path. Instructs the agent to prepend for future tasks
13 | if (envPath && !envPath.startsWith(path.dirname(terraformPath))) {
14 | tools.prependPath(path.dirname(terraformPath));
15 | }
16 | }
17 |
18 | async function verifyTerraform() {
19 | console.log(tasks.loc("VerifyTerraformInstallation"));
20 | let terraformPath = tasks.which("terraform", true);
21 | let terraformTool : ToolRunner = tasks.tool(terraformPath);
22 | terraformTool.arg("version");
23 | return terraformTool.exec();
24 | }
25 |
26 | async function run() {
27 | tasks.setResourcePath(path.join(__dirname, '..', 'task.json'));
28 |
29 | try {
30 | await configureTerraform();
31 | await verifyTerraform();
32 | tasks.setResult(tasks.TaskResult.Succeeded, "");
33 | } catch (error) {
34 | tasks.setResult(tasks.TaskResult.Failed, error);
35 | }
36 | }
37 |
38 | run();
--------------------------------------------------------------------------------
/Tasks/TerraformInstaller/src/TerraformInstaller.ts:
--------------------------------------------------------------------------------
1 | import tasks = require('azure-pipelines-task-lib/task');
2 | import tools = require('azure-pipelines-tool-lib/tool');
3 | import path = require('path');
4 | import os = require('os');
5 | import fs = require('fs');
6 |
7 | const uuidV4 = require('uuid/v4');
8 | const terraformToolName = "terraform";
9 | const isWindows = os.type().match(/^Win/);
10 |
11 | export async function downloadTerraform(inputVersion: string): Promise {
12 | let version = tools.cleanVersion(inputVersion);
13 | if (!version) {
14 | throw new Error(tasks.loc("InputVersionNotValidSemanticVersion", inputVersion));
15 | }
16 |
17 | let cachedToolPath = tools.findLocalTool(terraformToolName, version);
18 | if (!cachedToolPath) {
19 | let terraformDownloadUrl = getTerraformDownloadUrl(version);
20 | let fileName = `${terraformToolName}-${version}-${uuidV4()}.zip`;
21 | let terraformDownloadPath;
22 |
23 | try {
24 | terraformDownloadPath = await tools.downloadTool(terraformDownloadUrl, fileName);
25 | } catch (exception) {
26 | throw new Error(tasks.loc("TerraformDownloadFailed", terraformDownloadUrl, exception));
27 | }
28 |
29 | let terraformUnzippedPath = await tools.extractZip(terraformDownloadPath);
30 | cachedToolPath = await tools.cacheDir(terraformUnzippedPath, terraformToolName, version);
31 | }
32 |
33 | let terraformPath = findTerraformExecutable(cachedToolPath);
34 | if (!terraformPath) {
35 | throw new Error(tasks.loc("TerraformNotFoundInFolder", cachedToolPath));
36 | }
37 |
38 | if (!isWindows) {
39 | fs.chmodSync(terraformPath, "777");
40 | }
41 |
42 | tasks.setVariable('terraformLocation', terraformPath);
43 |
44 | return terraformPath;
45 | }
46 |
47 | function getTerraformDownloadUrl(version: string): string {
48 | let platform: string;
49 | let architecture: string;
50 |
51 | switch(os.type()) {
52 | case "Darwin":
53 | platform = "darwin";
54 | break;
55 |
56 | case "Linux":
57 | platform = "linux";
58 | break;
59 |
60 | case "Windows_NT":
61 | platform = "windows";
62 | break;
63 |
64 | default:
65 | throw new Error(tasks.loc("OperatingSystemNotSupported", os.type()));
66 | }
67 |
68 | switch(os.arch()) {
69 | case "x64":
70 | architecture = "amd64";
71 | break;
72 |
73 | case "x32":
74 | architecture = "386";
75 | break;
76 |
77 | default:
78 | throw new Error(tasks.loc("ArchitectureNotSupported", os.arch()));
79 | }
80 |
81 | return `https://releases.hashicorp.com/terraform/${version}/terraform_${version}_${platform}_${architecture}.zip`;
82 | }
83 |
84 | function findTerraformExecutable(rootFolder: string): string {
85 | let terraformPath = path.join(rootFolder, terraformToolName + getExecutableExtension());
86 | var allPaths = tasks.find(rootFolder);
87 | var matchingResultFiles = tasks.match(allPaths, terraformPath, rootFolder);
88 | return matchingResultFiles[0];
89 | }
90 |
91 | function getExecutableExtension(): string {
92 | if (isWindows) {
93 | return ".exe";
94 | }
95 |
96 | return "";
97 | }
--------------------------------------------------------------------------------
/Tasks/TerraformInstaller/task.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "9b9f2e72-30c3-432b-a835-dd2b1160f3b5",
3 | "name": "terraformInstaller",
4 | "friendlyName": "Terraform tool installer",
5 | "description": "Find in cache or download a specific version of Terraform and prepend it to the PATH",
6 | "author": "HashiCorp",
7 | "helpMarkDown": "",
8 | "category": "Tool",
9 | "visibility": [
10 | "Build",
11 | "Release"
12 | ],
13 | "demands": [],
14 | "version": {
15 | "Major": 0,
16 | "Minor": 0,
17 | "Patch": 1
18 | },
19 | "instanceNameFormat": "Install Terraform $(terraformVersion)",
20 | "inputs": [
21 | {
22 | "name": "terraformVersion",
23 | "type": "string",
24 | "label": "Version",
25 | "defaultValue": "0.12.3",
26 | "required": true,
27 | "helpMarkDown": "The version of Terraform which should be installed on the agent if not already present"
28 | }
29 | ],
30 | "execution": {
31 | "Node": {
32 | "target": ".bin/Index.js"
33 | }
34 | },
35 | "outputVariables": [
36 | {
37 | "name": "terraformLocation",
38 | "description": "The location of the terraform binary that was installed on the agent."
39 | }
40 | ],
41 | "messages": {
42 | "VerifyTerraformInstallation": "Verifying Terraform installation...",
43 | "InputVersionNotValidSemanticVersion": "Input version %s is not a valid semantic version",
44 | "TerraformNotFoundInFolder": "Terraform executable not found in path %s",
45 | "OperatingSystemNotSupported": "Operating system %s is not supported",
46 | "ArchitectureNotSupported": "Architecture %s is not supported",
47 | "TerraformDownloadFailed": "Failed to download Terraform from url %s. Error %s"
48 | }
49 | }
--------------------------------------------------------------------------------
/Tasks/TerraformInstaller/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES6",
4 | "module": "commonjs",
5 | "moduleResolution": "node",
6 | "sourceMap": true,
7 | "strict": true,
8 | "outDir": ".bin",
9 | "experimentalDecorators": true,
10 | "emitDecoratorMetadata": true,
11 | "typeRoots": [
12 | "node_modules/@types"
13 | ],
14 | "types": [
15 | "node",
16 | "mocha",
17 | "q"
18 | ]
19 | },
20 | "exclude": [
21 | "node_modules"
22 | ]
23 | }
24 |
--------------------------------------------------------------------------------
/azure-pipelines-extension-build.yml:
--------------------------------------------------------------------------------
1 | parameters:
2 | publisher:
3 | tasks:
4 |
5 | jobs:
6 | - job: Build
7 | displayName: Build and Test
8 |
9 | pool:
10 | vmImage: 'Ubuntu-16.04'
11 |
12 | steps:
13 | # Use GitVersion to determine the appropriate Semantic Version Number
14 | - task: GitVersion@5
15 | inputs:
16 | runtime: 'core'
17 |
18 | # Set the BuildNumber
19 | - script: |
20 | echo "##vso[build.updatebuildnumber]$(GitVersion.MajorMinorPatch)"
21 | displayName: 'Set Bulid Number'
22 |
23 | # Build and test each listed task
24 | - ${{ each taskName in parameters.tasks }}:
25 | - template: azure-pipelines-task-build.yml
26 | parameters:
27 | taskName: ${{ taskName }}
28 |
29 | # Package the extension manifest
30 | - script: |
31 | # Display command output and fail immediately on any errors
32 | set -e -x
33 |
34 | # Update the extension version
35 | sed -i -E 's/"version":\s"[0-9/.]+"/"version": "$(GitVersion.MajorMinorPatch)"/g' vss-extension.json
36 |
37 | # Update the publisher
38 | sed -i -E 's/"publisher":\s"[a-zA-Z0-9_-]+"/"publisher": "${{ parameters.publisher }}"/g' vss-extension.json
39 |
40 | # Copy the extension manifest to the output directory
41 | cp ./vss-extension.json $(Build.StagingDirectory)/
42 |
43 | # Copy in any Image Assets
44 | rsync -avr --ignore-errors ./images $(Build.StagingDirectory)
45 | displayName: Package the extension manifest and metadata
46 |
47 | # Publish the finished extension to a pipeline artifact
48 | - publish: $(Build.StagingDirectory)
49 | artifact: extension
--------------------------------------------------------------------------------
/azure-pipelines-extension-release.yml:
--------------------------------------------------------------------------------
1 | parameters:
2 | serviceUrl:
3 |
4 | jobs:
5 | - job: release
6 | displayName: Publish
7 |
8 | pool:
9 | vmImage: 'Ubuntu-16.04'
10 |
11 | steps:
12 | # Do not clone from git for this job
13 | - checkout: none
14 |
15 | # Download the published extension artifact to deploy
16 | - download: current
17 | artifact: extension
18 |
19 | # Publish the extension to Azure DevOps
20 | - script: |
21 | # Display command output and fail immediately on any errors
22 | set -e -x
23 |
24 | # Install the latext command line task for publishing extensions
25 | sudo npm install -g tfx-cli
26 |
27 | # Publish the extension
28 | tfx extension publish \
29 | --token $PERSONAL_ACCESS_TOKEN \
30 | --service-url https://marketplace.visualstudio.com \
31 | --no-prompt
32 | env:
33 | PERSONAL_ACCESS_TOKEN: $(PersonalAccessToken)
34 | workingDirectory: $(Pipeline.Workspace)/extension
35 | displayName: Publish the extension
36 |
--------------------------------------------------------------------------------
/azure-pipelines-task-build.yml:
--------------------------------------------------------------------------------
1 | parameters:
2 | tasksDirectory: Tasks
3 | taskName:
4 |
5 | steps:
6 |
7 | # Update package vertions
8 | - script: |
9 | # Display command output and fail immediately on any errors
10 | set -e -x
11 |
12 | # Set the package version
13 | sed -i -E 's/"version":\s"[0-9/.]+"/"version": "$(GitVersion.MajorMinorPatch)"/g' package.json
14 |
15 | # Set the task version
16 | sed -i -E 's/"Major":\s[0-9]+/"Major": $(GitVersion.Major)/g' task.json
17 | sed -i -E 's/"Minor":\s[0-9]+/"Minor": $(GitVersion.Minor)/g' task.json
18 | sed -i -E 's/"Patch":\s[0-9]+/"Patch": $(GitVersion.Patch)/g' task.json
19 |
20 | cat task.json
21 |
22 | workingDirectory: ./${{ parameters.tasksDirectory }}/${{ parameters.taskName }}
23 | displayName: Set versions for the ${{ parameters.taskName }} task
24 |
25 | # Built and test the extension
26 | - script: |
27 | # Display command output and fail immediately on any errors
28 | set -e -x
29 |
30 | # Install NPM Packages
31 | npm install --dev
32 |
33 | # Build typescript
34 | tsc --build
35 |
36 | # Run tests
37 |
38 | workingDirectory: ./${{ parameters.tasksDirectory }}/${{ parameters.taskName }}
39 | displayName: Build and test the ${{ parameters.taskName }} task
40 |
41 | # Prune and copy files to the artifact staging directory
42 | - script: |
43 | # Display command output and fail immediately on any errors
44 | set -e -x
45 |
46 | # Remove all dev dependencies prior to release
47 | npm prune --production
48 |
49 | # Ensure the destination path exists
50 | mkdir -p $(Build.StagingDirectory)/Tasks/
51 |
52 | # Copy all task files to the staging output path
53 | rsync -avr \
54 | --include=.bin/*** \
55 | --include=node_modules/*** \
56 | --include=package.json \
57 | --include=package-lock.json \
58 | --include=task.json \
59 | --include=icon.png \
60 | --exclude=* \
61 | ./ $(Build.StagingDirectory)/Tasks/${{ parameters.taskName }}
62 | workingDirectory: ./${{ parameters.tasksDirectory }}/${{ parameters.taskName }}
63 | displayName: Package the ${{ parameters.taskName }} task for release
64 |
--------------------------------------------------------------------------------
/azure-pipelines.yml:
--------------------------------------------------------------------------------
1 | trigger:
2 | batch: true
3 | branches:
4 | include:
5 | - master
6 | paths:
7 | exclude:
8 | - tests/*
9 | - readme.md
10 |
11 | pr:
12 | autoCancel: true
13 |
14 | # Build and Test
15 | stages:
16 | - stage: Build
17 | displayName: Build and Test
18 | jobs:
19 | - template: azure-pipelines-extension-build.yml
20 | parameters:
21 | publisher: jlorich
22 | tasks:
23 | - TerraformInstaller
24 | - Terraform
25 | - TerraformEnterprise
26 |
27 | # Staging Extension release
28 | - stage: Release
29 | jobs:
30 | - template: azure-pipelines-extension-release.yml
31 | parameters:
32 | serviceUrl: https://dev.azure.com/jlorich
33 |
--------------------------------------------------------------------------------
/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hashicorp/azure-pipelines-extension-terraform/1f7b31dbc2ae32cb9b700b6f048ed7eb9ebddbaa/images/icon.png
--------------------------------------------------------------------------------
/tests/azure-pipelines.yml:
--------------------------------------------------------------------------------
1 | trigger:
2 | batch: true
3 | branches:
4 | include:
5 | - master
6 | paths:
7 | include:
8 | - tests/*
9 |
10 | stages:
11 | - stage: Test
12 | jobs:
13 | - template: test-init-validate-plan-apply-destroy.yml
14 | - template: test-cli.yml
15 | - template: test-installer.yml
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/tests/test-cli.yml:
--------------------------------------------------------------------------------
1 | jobs:
2 | # Validates a valid template
3 | - job: CLI
4 | displayName: Test CLI
5 | dependsOn:
6 | pool:
7 | vmImage: 'Ubuntu-16.04'
8 |
9 | steps:
10 | - task: terraformInstaller@0
11 | inputs:
12 | terraformVersion: '0.12.13'
13 |
14 | - task: terraform@0
15 | inputs:
16 | command: 'CLI'
17 | providerAzureConnectedServiceName: 'MTC Denver Sandbox'
18 | backendAzureProviderStorageAccountName: 'mtcdenterraformsandbox'
19 | backendAzureStateFileKey: 'tfclitest.tfstate'
20 | cwd: tests/test-template-azure
21 | script: |
22 | # Validate
23 | terraform validate
24 |
25 | # Plan
26 | terraform plan -input=false -out=testplan.tf
27 |
28 | displayName: Run Terraform CLI
29 |
30 |
--------------------------------------------------------------------------------
/tests/test-init-validate-plan-apply-destroy.yml:
--------------------------------------------------------------------------------
1 | jobs:
2 | # Validates a valid template
3 | - job: InitPlanApplyDestroy
4 | displayName: Test init, plan, apply, and destroy
5 | dependsOn:
6 | pool:
7 | vmImage: 'Ubuntu-16.04'
8 |
9 | steps:
10 | - task: terraform@0
11 | inputs:
12 | command: 'init'
13 | cwd: 'tests/test-template-azure'
14 | providerAzureConnectedServiceName: 'MTC Denver Sandbox'
15 | backendAzureProviderStorageAccountName: 'mtcdenterraformsandbox'
16 | displayName: Terraform Init
17 |
18 | - task: terraform@0
19 | inputs:
20 | command: 'validate'
21 | cwd: 'tests/test-template-azure/'
22 | displayName: 'Terraform Validate'
23 |
24 | - task: terraform@0
25 | inputs:
26 | command: 'plan'
27 | cwd: 'tests/test-template-azure'
28 | providerAzureConnectedServiceName: 'MTC Denver Sandbox'
29 | displayName: Terraform Plan
30 |
31 | - task: terraform@0
32 | inputs:
33 | command: 'apply'
34 | cwd: 'tests/test-template-azure'
35 | providerAzureConnectedServiceName: 'MTC Denver Sandbox'
36 | displayName: Terraform Apply
37 |
38 | - task: terraform@0
39 | inputs:
40 | command: 'destroy'
41 | cwd: 'tests/test-template-azure'
42 | providerAzureConnectedServiceName: 'MTC Denver Sandbox'
43 | displayName: Terraform Destroy
--------------------------------------------------------------------------------
/tests/test-installer.yml:
--------------------------------------------------------------------------------
1 | jobs:
2 | # Validates a valid template
3 | - job: Installer
4 | displayName: Test Installer
5 | dependsOn:
6 | pool:
7 | vmImage: 'Ubuntu-16.04'
8 |
9 | steps:
10 | # Run the Terraform installer. It self-validates so no need to check beyond this
11 | - task: terraformInstaller@0
12 | inputs:
13 | terraformVersion: '0.12.3'
14 | displayName: Install Terraform
15 |
--------------------------------------------------------------------------------
/tests/test-template-azure/main.tf:
--------------------------------------------------------------------------------
1 | terraform {
2 | backend "azurerm" {}
3 | }
4 |
5 | variable "environment" {
6 | default = "test"
7 | }
8 |
9 |
10 | provider "azurerm" {
11 | version = "=1.35.0"
12 | }
13 |
14 | resource "azurerm_resource_group" "test" {
15 | name = "testResourceGroup1"
16 | location = "West US"
17 | }
--------------------------------------------------------------------------------
/tests/test-template-azure/output.plan:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hashicorp/azure-pipelines-extension-terraform/1f7b31dbc2ae32cb9b700b6f048ed7eb9ebddbaa/tests/test-template-azure/output.plan
--------------------------------------------------------------------------------
/tests/test-template-no-backend/main.tf:
--------------------------------------------------------------------------------
1 | provider "azurerm" {
2 |
3 | }
4 |
5 | resource "azurerm_resource_group" "test" {
6 | name = "testResourceGroup1"
7 | location = "West US"
8 | }
--------------------------------------------------------------------------------
/tests/test-template-remote/main.tf:
--------------------------------------------------------------------------------
1 | terraform {
2 | backend "remote" {
3 | hostname = "app.terraform.io"
4 | organization = "msus"
5 |
6 | workspaces {
7 | name = "tfcloud-test"
8 | }
9 | }
10 | }
11 |
12 | provider "azurerm" {
13 | version = "=1.35.0"
14 | }
15 |
16 | resource "azurerm_resource_group" "test" {
17 | name = "testResourceGroup1"
18 | location = "West US"
19 | }
--------------------------------------------------------------------------------
/vss-extension.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifestVersion": 1,
3 | "id": "terraform-pipelines-extension-dev",
4 | "name": "Terraform",
5 | "version": "0.0.1",
6 | "publisher": "hashicorp",
7 | "targets": [
8 | {
9 | "id": "Microsoft.VisualStudio.Services"
10 | }
11 | ],
12 | "description": "Task for running Terraform on Azure, AWS, GCP, and Terraform Cloud/Enterprise",
13 | "categories": [
14 | "Azure Pipelines"
15 | ],
16 | "Tags": [
17 | "Vault",
18 | "HashiCorp",
19 | "Secrets",
20 | "Release",
21 | "DevOps"
22 | ],
23 | "icons": {
24 | "default": "images/icon.png",
25 | "large": "images/icon.png"
26 | },
27 | "files": [
28 | {
29 | "path": "Tasks/TerraformInstaller"
30 | },
31 | {
32 | "path": "Tasks/Terraform"
33 | },
34 | {
35 | "path": "Tasks/TerraformEnterprise"
36 | }
37 | ],
38 | "contributions": [
39 | {
40 | "id": "9b9f2e72-30c3-432b-a835-dd2b1160f3b5",
41 | "type": "ms.vss-distributed-task.task",
42 | "targets": [
43 | "ms.vss-distributed-task.tasks"
44 | ],
45 | "properties": {
46 | "name": "Tasks/TerraformInstaller"
47 | }
48 | },
49 | {
50 | "id": "653507a3-d921-47dc-8fed-c5649d8e9e0e",
51 | "type": "ms.vss-distributed-task.task",
52 | "targets": [
53 | "ms.vss-distributed-task.tasks"
54 | ],
55 | "properties": {
56 | "name": "Tasks/Terraform"
57 | }
58 | },
59 | {
60 | "id": "506f3bf4-69c9-4812-a879-ccfc035f174c",
61 | "type": "ms.vss-distributed-task.task",
62 | "targets": [
63 | "ms.vss-distributed-task.tasks"
64 | ],
65 | "properties": {
66 | "name": "Tasks/TerraformEnterprise"
67 | }
68 | },
69 | {
70 |
71 | "id": "terraform-cloud-endpoint-type",
72 | "description": "Credentials for tasks invoking Terraform Cloud and Enterprise",
73 | "type": "ms.vss-endpoint.service-endpoint-type",
74 | "targets": [
75 | "ms.vss-endpoint.endpoint-types"
76 | ],
77 | "properties": {
78 | "name": "terraformServiceEndpoint",
79 | "displayName": "Terraform Cloud and Enterprise",
80 | "authenticationSchemes": [
81 | {
82 | "id": "endpoint-auth-scheme-token",
83 | "name": "token",
84 | "displayName": "i18n:Access Token",
85 | "type": "ms.vss-endpoint.endpoint-auth-scheme-token",
86 | "description": "i18n:Token based endpoint authentication scheme",
87 | "inputDescriptors": [
88 | {
89 | "id": "apitoken",
90 | "name": "i18n:Token",
91 | "description": "i18n:Token to use when accessing Terraform cloud and Enterprise",
92 | "inputMode": "textbox",
93 | "isConfidential": true,
94 | "validation": {
95 | "isRequired": true,
96 | "dataType": "string",
97 | "maxLength": 300
98 | }
99 | }
100 | ]
101 | }
102 | ],
103 | "url": {
104 | "displayName": "i18n:Terraform Instance URL",
105 | "required": true,
106 | "value": "https://app.terraform.io",
107 | "helpText": "i18n:Client connection endpoint for the Terraform Cloud or Enterprise instance. Prefix the value with \"https://\"."
108 | }
109 | }
110 | }
111 | ]
112 | }
--------------------------------------------------------------------------------