├── .copywrite.hcl ├── .dockerignore ├── .eslintrc.js ├── .gitignore ├── .prettierignore ├── .yarn └── releases │ └── yarn-3.6.0.cjs ├── .yarnrc.yml ├── LICENSE ├── README.md ├── app-config.production.yaml ├── app-config.yaml ├── backstage.json ├── catalog-info.yaml ├── examples ├── entities.yaml ├── org.yaml ├── template │ ├── content │ │ ├── catalog-info.yaml │ │ ├── index.js │ │ └── package.json │ └── template.yaml └── terraform │ ├── content │ ├── README.md │ ├── catalog-info.yaml │ └── main.tf │ └── template.yaml ├── lerna.json ├── package.json ├── packages ├── README.md ├── app │ ├── .eslintignore │ ├── .eslintrc.js │ ├── cypress.json │ ├── cypress │ │ ├── .eslintrc.json │ │ └── integration │ │ │ └── app.js │ ├── package.json │ ├── public │ │ ├── android-chrome-192x192.png │ │ ├── apple-touch-icon.png │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── manifest.json │ │ ├── robots.txt │ │ └── safari-pinned-tab.svg │ └── src │ │ ├── App.test.tsx │ │ ├── App.tsx │ │ ├── apis.ts │ │ ├── components │ │ ├── Root │ │ │ ├── LogoFull.tsx │ │ │ ├── LogoIcon.tsx │ │ │ ├── Root.tsx │ │ │ └── index.ts │ │ ├── catalog │ │ │ └── EntityPage.tsx │ │ └── search │ │ │ └── SearchPage.tsx │ │ ├── index.tsx │ │ └── setupTests.ts └── backend │ ├── .eslintrc.js │ ├── Dockerfile │ ├── README.md │ ├── package.json │ └── src │ ├── index.test.ts │ ├── index.ts │ ├── plugins │ ├── app.ts │ ├── auth.ts │ ├── catalog.ts │ ├── proxy.ts │ ├── scaffolder.ts │ ├── search.ts │ └── techdocs.ts │ └── types.ts ├── plugins ├── README.md ├── scaffolder-terraform-backend │ ├── .eslintrc.js │ ├── README.md │ ├── package.json │ └── src │ │ ├── actions │ │ ├── createProject.ts │ │ ├── createRun.ts │ │ ├── createVariables.ts │ │ ├── createWorkspace.ts │ │ └── index.ts │ │ ├── api │ │ ├── index.ts │ │ └── types.ts │ │ └── index.ts ├── scaffolder-vault-backend │ ├── .eslintrc.js │ ├── README.md │ ├── package.json │ └── src │ │ ├── actions │ │ ├── authenticate.ts │ │ ├── getTerraformToken.ts │ │ └── index.ts │ │ ├── api │ │ ├── index.ts │ │ └── types.ts │ │ └── index.ts └── terraform │ ├── .eslintrc.js │ ├── README.md │ ├── dev │ ├── fixtures │ │ ├── current-state-version.json │ │ ├── outputs.json │ │ ├── raw-state.json │ │ ├── runs.json │ │ └── workspaces.json │ └── index.tsx │ ├── package.json │ └── src │ ├── annotations.ts │ ├── api │ ├── index.ts │ └── types.ts │ ├── components │ ├── TerraformOutputFetchComponent │ │ ├── TerraformOutputFetchComponent.tsx │ │ └── index.ts │ ├── TerraformOutputTable │ │ ├── TerraformOutputTable.tsx │ │ └── index.ts │ ├── TerraformResourceFetchComponent │ │ ├── TerraformResourceFetchComponent.tsx │ │ └── index.ts │ ├── TerraformResourceTable │ │ ├── TerraformResourceTable.tsx │ │ └── index.ts │ ├── TerraformRunFetchComponent │ │ ├── TerraformRunFetchComponent.tsx │ │ └── index.ts │ └── TerraformRunTable │ │ ├── TerraformRunTable.tsx │ │ └── index.ts │ ├── index.ts │ ├── plugin.test.ts │ ├── plugin.ts │ ├── routes.ts │ └── setupTests.ts ├── tsconfig.json ├── vault ├── docker-compose.yml ├── policy.hcl └── setup.sh └── yarn.lock /.copywrite.hcl: -------------------------------------------------------------------------------- 1 | schema_version = 1 2 | 3 | project { 4 | license = "MPL-2.0" 5 | copyright_year = 2023 6 | 7 | # (OPTIONAL) A list of globs that should not have copyright/license headers. 8 | # Supports doublestar glob patterns for more flexibility in defining which 9 | # files or folders should be ignored 10 | header_ignore = [ 11 | # "vendors/**", 12 | # "**autogen**", 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .yarn/cache 3 | .yarn/install-state.gz 4 | node_modules 5 | packages/*/src 6 | packages/*/node_modules 7 | plugins 8 | *.local.yaml 9 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | module.exports = { 7 | root: true, 8 | }; 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # macOS 2 | .DS_Store 3 | 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | lerna-debug.log* 11 | 12 | # Coverage directory generated when running tests with coverage 13 | coverage 14 | 15 | # Dependencies 16 | node_modules/ 17 | 18 | # Yarn 3 files 19 | .pnp.* 20 | .yarn/* 21 | !.yarn/patches 22 | !.yarn/plugins 23 | !.yarn/releases 24 | !.yarn/sdks 25 | !.yarn/versions 26 | 27 | # Node version directives 28 | .nvmrc 29 | 30 | # dotenv environment variables file 31 | .env 32 | .env.test 33 | 34 | # Build output 35 | dist 36 | dist-types 37 | 38 | # Temporary change files created by Vim 39 | *.swp 40 | 41 | # MkDocs build output 42 | site 43 | 44 | # Local configuration files 45 | *.local.yaml 46 | 47 | # Sensitive credentials 48 | *-credentials.yaml 49 | 50 | # vscode database functionality support files 51 | *.session.sql 52 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | dist-types 3 | coverage 4 | .vscode 5 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | 3 | yarnPath: .yarn/releases/yarn-3.6.0.cjs 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2023 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 | # [Backstage](https://backstage.io) 2 | 3 | This is an example Backstage app that includes two Terraform plugins: 4 | 5 | 1. Scaffolder Action - creates Terraform Cloud/Enterprise resources using scaffolder 6 | 1. Terraform frontend plugin - retrieves information for an entity based on an organization and workspace 7 | 8 | ## Prerequisites 9 | 10 | Install [Backstage prerequisites](https://backstage.io/docs/getting-started/#prerequisites). 11 | 12 | ## Install 13 | 14 | In your terminal, set the Terraform Cloud token and GitHub token 15 | 16 | ```sh 17 | export GITHUB_TOKEN="" 18 | export TF_TOKEN="" 19 | ``` 20 | 21 | To start the app, run: 22 | 23 | ```sh 24 | yarn install 25 | 26 | yarn dev 27 | ``` 28 | 29 | ## Using the Scaffolder Action 30 | 31 | This repository includes an example template for the Scaffolder to use 32 | under `examples/terraform`. 33 | 34 | Review `template.yaml` for the series of custom actions specific 35 | to Terraform. You can... 36 | 37 | - Create projects 38 | - Create workspaces 39 | - Create runs 40 | 41 | However, you will encounter a few caveats: 42 | 43 | - Scaffolder is *not* intended to be idempotent. If you have an 44 | existing project, you must remove the "Create Project" step 45 | from the template. 46 | 47 | - Variables must be passed through scaffolder. Secrets should 48 | not be passed directly through scaffolder, consider setting them 49 | separately using variable sets or using dynamic credentials 50 | from Vault. 51 | 52 | - Workspaces use VCS connections. This ensures that you can 53 | manage your infrastructure on Day 2. 54 | - If you do not specific a `vcsAuthUser`, the VCS connection will 55 | default to the first OAuth client returned by the Terraform API. 56 | - If you specify a `vcsAuthUser`, the action will return 57 | the first VCS OAuth token associated with that user. **Note that 58 | `vcsAuthUser` must have sufficient permissions to access 59 | the `vcsRepo` you are connecting.** 60 | 61 | 62 | ## Using Scaffolder with HashiCorp Vault & GitHub 63 | 64 | Ideally, you'll want to scope your Terraform token to 65 | the workspace and projects specific to a group. One approach 66 | is to use HashiCorp Vault to generate the Terraform tokens. 67 | 68 | In order to allow Backstage to access Vault, you need to configure 69 | an authentication provider for Backstage using an SCM tool (GitHub, GitLab, etc.). 70 | 71 | This is because Scaffolder has a 72 | [built-in action](https://backstage.io/docs/features/software-templates/writing-templates/#using-the-users-oauth-token) 73 | that allows you to retrieve a user OAuth token from the SCM tool for use in subsequent actions. 74 | 75 | ```text 76 | ┌─────────────► SCM Provider ◄─────────────────────┐ 77 | │ (GitHub) │ 78 | │ │ 79 | │ │ 80 | │ ┌────┴────┬──────────────────┐ 81 | │ │ │ Vault │ 82 | ┌────────────┴──────────────┐Auth with OAuth user token │ │ │ 83 | │ ├──────────────────────────────► GitHub │ │ 84 | │ Backstage │ Return Vault token │ Auth │ Terraform Cloud │ 85 | │ (GitHub Auth Provider) ◄──────────────────────────────┤ Method │ Secrets Engine │ 86 | │ │ │ │ │ 87 | └────────────▲───┬──────────┘ └─────────┴────▲──┬──────────┘ 88 | │ │ Use Vault token to get TFC token │ │ 89 | │ └────────────────────────────────────────────────────────┘ │ 90 | │ Return TFC Token │ 91 | └───────────────────────────────────────────────────────────────┘ 92 | ``` 93 | 94 | ### Set up GitHub 95 | 96 | 1. Create a [personal access token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens) 97 | with read-only access to your organization. 98 | 99 | 1. Set the personal access token as an environment variable for Backstage. 100 | This allows Backstage to read repositories and register entities into catalog. 101 | ```shell 102 | export GITHUB_TOKEN="" 103 | ``` 104 | 105 | 1. Create an OAuth App for Backstage in GitHub under **your organization**. 106 | 107 | 1. Set the client ID and secret as environment variables for Backstage. 108 | ```shell 109 | export AUTH_GITHUB_CLIENT_ID="" 110 | export AUTH_GITHUB_CLIENT_SECRET="" 111 | ``` 112 | 113 | 1. Sign into Backstage using your GitHub user and make sure 114 | you grant a user access to the organization. 115 | 116 | ### Set up Terraform Cloud 117 | 118 | 1. Set a read-only Terraform Cloud token that allows 119 | Backstage frontend components to retrieve information 120 | about workspaces, runs, and outputs. 121 | ```shell 122 | export TF_TOKEN="" 123 | ``` 124 | 125 | 1. In your Terraform Cloud organization, add a VCS provider. 126 | 127 | 1. Create an OAuth App for Terraform Cloud in GitHub under **your organization**. 128 | 129 | ### Set up Vault 130 | 131 | 1. Using Docker, create a development Vault server. 132 | ```shell 133 | cd vault && docker-compose up -d && cd .. 134 | ``` 135 | 136 | 1. Set environment variables to configure Vault Github auth method, 137 | using organization, organization ID, and a sample user. 138 | ```shell 139 | export VAULT_GITHUB_ORG="" 140 | export VAULT_GITHUB_ORG_ID="" 141 | export VAULT_GITHUB_USER="" 142 | ``` 143 | 144 | 1. Set environment variables for Terraform Cloud secrets engine, 145 | including organization token and team ID specific to backstage. 146 | ```shell 147 | export TERRAFORM_CLOUD_ORGANIZATION_TOKEN="" 148 | export TERRAFORM_CLOUD_TEAM_ID="" 149 | ``` 150 | 151 | 1. Run `bash vault/setup.sh`. This sets up the auth method, policies, 152 | and secrets engines for Backstage to authenticate and retrieve secrets 153 | from Vault. 154 | 155 | ### Run the Scaffolder Template 156 | 157 | Start Backstage. Choose the Terraform template and enter the form values. 158 | The Vault defaults are already set. When you create the repository, 159 | Backstage authenticates to Vault with its GitHub OAuth user access token, 160 | gets a Vault token, and uses that to retrieve the Terraform Cloud token. 161 | 162 | The Terraform Cloud token in this example is a team token, which has sufficient 163 | permission to create workspaces and projects in Terraform Cloud. -------------------------------------------------------------------------------- /app-config.production.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | app: 5 | # Should be the same as backend.baseUrl when using the `app-backend` plugin. 6 | baseUrl: http://localhost:7007 7 | 8 | backend: 9 | # Note that the baseUrl should be the URL that the browser and other clients 10 | # should use when communicating with the backend, i.e. it needs to be 11 | # reachable not just from within the backend host, but from all of your 12 | # callers. When its value is "http://localhost:7007", it's strictly private 13 | # and can't be reached by others. 14 | baseUrl: http://localhost:7007 15 | # The listener can also be expressed as a single : string. In this case we bind to 16 | # all interfaces, the most permissive setting. The right value depends on your specific deployment. 17 | listen: ':7007' 18 | 19 | # config options: https://node-postgres.com/api/client 20 | database: 21 | client: pg 22 | connection: 23 | host: ${POSTGRES_HOST} 24 | port: ${POSTGRES_PORT} 25 | user: ${POSTGRES_USER} 26 | password: ${POSTGRES_PASSWORD} 27 | # https://node-postgres.com/features/ssl 28 | # you can set the sslmode configuration option via the `PGSSLMODE` environment variable 29 | # see https://www.postgresql.org/docs/current/libpq-ssl.html Table 33.1. SSL Mode Descriptions (e.g. require) 30 | # ssl: 31 | # ca: # if you have a CA file and want to verify it you can uncomment this section 32 | # $file: /ca/server.crt 33 | 34 | catalog: 35 | # Overrides the default list locations from app-config.yaml as these contain example data. 36 | # See https://backstage.io/docs/features/software-catalog/#adding-components-to-the-catalog for more details 37 | # on how to get entities into the catalog. 38 | locations: [] 39 | -------------------------------------------------------------------------------- /app-config.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | app: 5 | title: Scaffolded Backstage App 6 | baseUrl: http://localhost:3000 7 | 8 | organization: 9 | name: My Company 10 | 11 | backend: 12 | # Used for enabling authentication, secret is shared by all backend plugins 13 | # See https://backstage.io/docs/auth/service-to-service-auth for 14 | # information on the format 15 | # auth: 16 | # keys: 17 | # - secret: ${BACKEND_SECRET} 18 | baseUrl: http://localhost:7007 19 | listen: 20 | port: 7007 21 | # Uncomment the following host directive to bind to specific interfaces 22 | # host: 127.0.0.1 23 | csp: 24 | connect-src: ["'self'", 'http:', 'https:'] 25 | # Content-Security-Policy directives follow the Helmet format: https://helmetjs.github.io/#reference 26 | # Default Helmet Content-Security-Policy values can be removed by setting the key to false 27 | cors: 28 | origin: http://localhost:3000 29 | methods: [GET, HEAD, PATCH, POST, PUT, DELETE] 30 | credentials: true 31 | # This is for local development only, it is not recommended to use this in production 32 | # The production database configuration is stored in app-config.production.yaml 33 | database: 34 | client: better-sqlite3 35 | connection: ':memory:' 36 | cache: 37 | store: memory 38 | # workingDirectory: /tmp # Use this to configure a working directory for the scaffolder, defaults to the OS temp-dir 39 | 40 | integrations: 41 | github: 42 | - host: github.com 43 | # This is a Personal Access Token or PAT from GitHub. You can find out how to generate this token, and more information 44 | # about setting up the GitHub integration here: https://backstage.io/docs/getting-started/configuration#setting-up-a-github-integration 45 | token: ${GITHUB_TOKEN} 46 | ### Example for how to add your GitHub Enterprise instance using the API: 47 | # - host: ghe.example.net 48 | # apiBaseUrl: https://ghe.example.net/api/v3 49 | # token: ${GHE_TOKEN} 50 | 51 | proxy: 52 | ### Example for how to add a proxy endpoint for the frontend. 53 | ### A typical reason to do this is to handle HTTPS and CORS for internal services. 54 | '/terraform': 55 | target: https://app.terraform.io 56 | headers: 57 | ### If you have Vault, you can limit 58 | ### this TF_TOKEN to read-only access 59 | ### to the organization so frontend 60 | ### components can pull information 61 | ### about Terraform Cloud runs. 62 | Authorization: Bearer ${TF_TOKEN} 63 | Accept: 'application/vnd.api+json' 64 | allowedHeaders: ['Authorization'] 65 | '/vault': 66 | target: http://127.0.0.1:8200 67 | allowedHeaders: ['X-Vault-Token'] 68 | 69 | # Reference documentation http://backstage.io/docs/features/techdocs/configuration 70 | # Note: After experimenting with basic setup, use CI/CD to generate docs 71 | # and an external cloud storage when deploying TechDocs for production use-case. 72 | # https://backstage.io/docs/features/techdocs/how-to-guides#how-to-migrate-from-techdocs-basic-to-recommended-deployment-approach 73 | techdocs: 74 | builder: 'local' # Alternatives - 'external' 75 | generator: 76 | runIn: 'docker' # Alternatives - 'local' 77 | publisher: 78 | type: 'local' # Alternatives - 'googleGcs' or 'awsS3'. Read documentation for using alternatives. 79 | 80 | auth: 81 | environment: development 82 | providers: 83 | github: 84 | development: 85 | clientId: ${AUTH_GITHUB_CLIENT_ID} 86 | clientSecret: ${AUTH_GITHUB_CLIENT_SECRET} 87 | ## uncomment if using GitHub Enterprise 88 | # enterpriseInstanceUrl: ${AUTH_GITHUB_ENTERPRISE_INSTANCE_URL} 89 | 90 | scaffolder: 91 | # see https://backstage.io/docs/features/software-templates/configuration for software template options 92 | 93 | catalog: 94 | import: 95 | entityFilename: catalog-info.yaml 96 | pullRequestBranchName: backstage-integration 97 | rules: 98 | - allow: [Component, System, API, Resource, Location] 99 | locations: 100 | # Local example data, file locations are relative to the backend process, typically `packages/backend` 101 | - type: file 102 | target: ../../examples/entities.yaml 103 | 104 | # Local example template 105 | - type: file 106 | target: ../../examples/template/template.yaml 107 | rules: 108 | - allow: [Template] 109 | 110 | - type: file 111 | target: ../../examples/terraform/template.yaml 112 | rules: 113 | - allow: [Template] 114 | 115 | # Local example organizational data 116 | - type: file 117 | target: ../../examples/org.yaml 118 | rules: 119 | - allow: [User, Group] 120 | 121 | ## Uncomment these lines to add more example data 122 | # - type: url 123 | # target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/all.yaml 124 | 125 | ## Uncomment these lines to add an example org 126 | # - type: url 127 | # target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/acme-corp.yaml 128 | # rules: 129 | # - allow: [User, Group] 130 | -------------------------------------------------------------------------------- /backstage.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.15.0" 3 | } 4 | -------------------------------------------------------------------------------- /catalog-info.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | apiVersion: backstage.io/v1alpha1 5 | kind: Component 6 | metadata: 7 | name: backstage 8 | description: An example of a Backstage application. 9 | spec: 10 | type: website 11 | owner: john@example.com 12 | lifecycle: experimental 13 | -------------------------------------------------------------------------------- /examples/entities.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | --- 5 | # https://backstage.io/docs/features/software-catalog/descriptor-format#kind-system 6 | apiVersion: backstage.io/v1alpha1 7 | kind: System 8 | metadata: 9 | name: examples 10 | spec: 11 | owner: guests 12 | --- 13 | # https://backstage.io/docs/features/software-catalog/descriptor-format#kind-component 14 | apiVersion: backstage.io/v1alpha1 15 | kind: Component 16 | metadata: 17 | name: example-website 18 | annotations: 19 | terraform.io/organization: hashicorp-team-demo 20 | terraform.io/workspace: runtime-eks 21 | spec: 22 | type: website 23 | lifecycle: experimental 24 | owner: guests 25 | system: examples 26 | providesApis: [example-grpc-api] 27 | --- 28 | # https://backstage.io/docs/features/software-catalog/descriptor-format#kind-api 29 | apiVersion: backstage.io/v1alpha1 30 | kind: API 31 | metadata: 32 | name: example-grpc-api 33 | spec: 34 | type: grpc 35 | lifecycle: experimental 36 | owner: guests 37 | system: examples 38 | definition: | 39 | syntax = "proto3"; 40 | 41 | service Exampler { 42 | rpc Example (ExampleMessage) returns (ExampleMessage) {}; 43 | } 44 | 45 | message ExampleMessage { 46 | string example = 1; 47 | }; 48 | -------------------------------------------------------------------------------- /examples/org.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | --- 5 | # https://backstage.io/docs/features/software-catalog/descriptor-format#kind-user 6 | apiVersion: backstage.io/v1alpha1 7 | kind: User 8 | metadata: 9 | name: guest 10 | spec: 11 | memberOf: [guests] 12 | --- 13 | # https://backstage.io/docs/features/software-catalog/descriptor-format#kind-group 14 | apiVersion: backstage.io/v1alpha1 15 | kind: Group 16 | metadata: 17 | name: guests 18 | spec: 19 | type: team 20 | children: [] 21 | -------------------------------------------------------------------------------- /examples/template/content/catalog-info.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | apiVersion: backstage.io/v1alpha1 5 | kind: Component 6 | metadata: 7 | name: ${{ values.name | dump }} 8 | spec: 9 | type: service 10 | owner: user:guest 11 | lifecycle: experimental 12 | -------------------------------------------------------------------------------- /examples/template/content/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | console.log('Hello from ${{ values.name }}!'); 7 | -------------------------------------------------------------------------------- /examples/template/content/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "${{ values.name }}", 3 | "private": true, 4 | "dependencies": {} 5 | } 6 | -------------------------------------------------------------------------------- /examples/template/template.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | apiVersion: scaffolder.backstage.io/v1beta3 5 | # https://backstage.io/docs/features/software-catalog/descriptor-format#kind-template 6 | kind: Template 7 | metadata: 8 | name: example-nodejs-template 9 | title: Example Node.js Template 10 | description: An example template for the scaffolder that creates a simple Node.js service 11 | spec: 12 | owner: user:guest 13 | type: service 14 | 15 | # These parameters are used to generate the input form in the frontend, and are 16 | # used to gather input data for the execution of the template. 17 | parameters: 18 | - title: Fill in some steps 19 | required: 20 | - name 21 | properties: 22 | name: 23 | title: Name 24 | type: string 25 | description: Unique name of the component 26 | ui:autofocus: true 27 | ui:options: 28 | rows: 5 29 | - title: Choose a location 30 | required: 31 | - repoUrl 32 | properties: 33 | repoUrl: 34 | title: Repository Location 35 | type: string 36 | ui:field: RepoUrlPicker 37 | ui:options: 38 | allowedHosts: 39 | - github.com 40 | 41 | # These steps are executed in the scaffolder backend, using data that we gathered 42 | # via the parameters above. 43 | steps: 44 | # Each step executes an action, in this case one templates files into the working directory. 45 | - id: fetch-base 46 | name: Fetch Base 47 | action: fetch:template 48 | input: 49 | url: ./content 50 | values: 51 | name: ${{ parameters.name }} 52 | 53 | # This step publishes the contents of the working directory to GitHub. 54 | - id: publish 55 | name: Publish 56 | action: publish:github 57 | input: 58 | allowedHosts: ['github.com'] 59 | description: This is ${{ parameters.name }} 60 | repoUrl: ${{ parameters.repoUrl }} 61 | 62 | # The final step is to register our new component in the catalog. 63 | - id: register 64 | name: Register 65 | action: catalog:register 66 | input: 67 | repoContentsUrl: ${{ steps['publish'].output.repoContentsUrl }} 68 | catalogInfoPath: '/catalog-info.yaml' 69 | 70 | # Outputs are displayed to the user after a successful execution of the template. 71 | output: 72 | links: 73 | - title: Repository 74 | url: ${{ steps['publish'].output.remoteUrl }} 75 | - title: Open in catalog 76 | icon: catalog 77 | entityRef: ${{ steps['register'].output.entityRef }} 78 | -------------------------------------------------------------------------------- /examples/terraform/content/README.md: -------------------------------------------------------------------------------- 1 | # ${{ values.name }}-infrastructure 2 | 3 | Creates infrastructure for ${{ values.name }} using Terraform. 4 | -------------------------------------------------------------------------------- /examples/terraform/content/catalog-info.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | apiVersion: backstage.io/v1alpha1 5 | kind: Component 6 | metadata: 7 | name: ${{ values.name | dump }} 8 | description: ${{values.description | dump}} 9 | annotations: 10 | terraform.io/organization: '${{ values.organization }}' 11 | terraform.io/project: '${{ values.project }}' 12 | terraform.io/workspace: '${{ values.name }}' 13 | spec: 14 | type: service 15 | owner: user:guest 16 | lifecycle: experimental 17 | -------------------------------------------------------------------------------- /examples/terraform/content/main.tf: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | 5 | variable "second_hello" { 6 | type = string 7 | description = "Second entity to greet" 8 | } 9 | 10 | variable "secret_key" { 11 | type = string 12 | description = "A secret key to pass to some_key" 13 | sensitive = true 14 | } 15 | 16 | module "infrastructure" { 17 | source = "joatmon08/hello/random" 18 | version = "6.0.0" 19 | 20 | hellos = { 21 | hello = "${{ values.name }}" 22 | second_hello = var.second_hello 23 | } 24 | 25 | some_key = var.secret_key 26 | } -------------------------------------------------------------------------------- /examples/terraform/template.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | apiVersion: scaffolder.backstage.io/v1beta3 5 | # https://backstage.io/docs/features/software-catalog/descriptor-format#kind-template 6 | kind: Template 7 | metadata: 8 | name: terraform-template 9 | title: Example Terraform Template for Platform Teams 10 | description: An example template for the scaffolder that creates infrastructure with Terraform 11 | spec: 12 | owner: user:guest 13 | type: service 14 | 15 | # These parameters are used to generate the input form in the frontend, and are 16 | # used to gather input data for the execution of the template. 17 | parameters: 18 | - title: Fill in some steps 19 | required: 20 | - name 21 | - description 22 | - organization 23 | - project 24 | properties: 25 | name: 26 | title: Name 27 | type: string 28 | description: Unique name of the component 29 | ui:autofocus: true 30 | ui:options: 31 | rows: 5 32 | description: 33 | title: Description 34 | type: string 35 | description: A description for the component 36 | organization: 37 | title: Organization 38 | type: string 39 | description: The name of an existing Terraform organization 40 | project: 41 | title: Project 42 | type: string 43 | description: The name of a new Terraform project you want to create 44 | - title: Choose a location 45 | required: 46 | - repoUrl 47 | properties: 48 | repoUrl: 49 | title: Repository Location 50 | type: string 51 | ui:field: RepoUrlPicker 52 | ui:options: 53 | requestUserCredentials: 54 | secretsKey: USER_OAUTH_TOKEN 55 | allowedHosts: 56 | - github.com 57 | - title: Define Terraform Cloud secrets engine for Vault 58 | properties: 59 | vaultMount: 60 | title: Vault Mount 61 | type: string 62 | description: A mount for the Terraform Cloud secrets engine in Vault 63 | default: terraform 64 | vaultRole: 65 | title: Vault Role 66 | type: string 67 | description: A role to use for accessing the Terraform Cloud token in Vault 68 | default: backstage 69 | - title: Define variables for Terraform module 70 | properties: 71 | secondHello: 72 | title: Second Hello 73 | type: string 74 | description: A variable to pass to the Terraform module, specifically a second greeting 75 | default: Backstage 76 | 77 | # These steps are executed in the scaffolder backend, using data that we gathered 78 | # via the parameters above. 79 | steps: 80 | - id: fetch-base 81 | name: Fetch Base 82 | action: fetch:template 83 | input: 84 | url: ./content 85 | values: 86 | name: ${{ parameters.name }} 87 | description: ${{ parameters.description }} 88 | organization: ${{ parameters.organization }} 89 | project: ${{ parameters.project }} 90 | secondHello: ${{ parameters.secondHello }} 91 | 92 | - id: publish 93 | name: Publish 94 | action: publish:github 95 | input: 96 | token: '${{ secrets.USER_OAUTH_TOKEN }}' 97 | allowedHosts: ['github.com'] 98 | description: This is ${{ parameters.name }} 99 | repoUrl: ${{ parameters.repoUrl }} 100 | 101 | - id: vault-auth 102 | name: Authenticate to Vault using GitHub Auth Method 103 | action: vault:authenticate:github 104 | input: 105 | token: '${{ secrets.USER_OAUTH_TOKEN }}' 106 | path: 'github' 107 | 108 | - id: vault-tfc-token 109 | name: Get Terraform token from Vault 110 | action: vault:secrets:terraform 111 | input: 112 | token: '${{ secrets.VAULT_TOKEN }}' 113 | mount: ${{ parameters.vaultMount }} 114 | role: ${{ parameters.vaultRole }} 115 | 116 | - id: terraform-project 117 | name: Create Terraform Project 118 | action: terraform:project:create 119 | input: 120 | token: '${{ secrets.TERRAFORM_CLOUD_TOKEN }}' 121 | organization: ${{ parameters.organization }} 122 | name: ${{ parameters.project }} 123 | 124 | - id: terraform-workspace 125 | name: Create Terraform Workspace 126 | action: terraform:workspace:create 127 | input: 128 | token: '${{ secrets.TERRAFORM_CLOUD_TOKEN }}' 129 | organization: ${{ parameters.organization }} 130 | project: ${{ steps['terraform-project'].output.name }} 131 | name: ${{ parameters.name }} 132 | vcsSourceProvider: 'github' 133 | vcsOwner: ${{ (parameters.repoUrl | parseRepoUrl).owner }} 134 | vcsRepo: ${{ (parameters.repoUrl | parseRepoUrl).repo }} 135 | 136 | - id: terraform-variables 137 | name: Create Terraform Variables 138 | action: terraform:variables:create 139 | input: 140 | token: '${{ secrets.TERRAFORM_CLOUD_TOKEN }}' 141 | workspaceID: ${{ steps['terraform-workspace'].output.id }} 142 | variables: 143 | - key: second_hello 144 | value: ${{ parameters.secondHello }} 145 | description: 'A second hello to add as a greeting' 146 | category: terraform 147 | - key: secret_key 148 | value: 'some-secret-key-from-output-on-another-step' 149 | description: 'An example of a sensitive variable passed to Terraform' 150 | category: terraform 151 | sensitive: true 152 | 153 | - id: terraform-run 154 | name: Create Terraform Run 155 | action: terraform:run:create 156 | input: 157 | token: '${{ secrets.TERRAFORM_CLOUD_TOKEN }}' 158 | workspaceID: ${{ steps['terraform-workspace'].output.id }} 159 | 160 | - id: register 161 | name: Register 162 | action: catalog:register 163 | input: 164 | repoContentsUrl: ${{ steps['publish'].output.repoContentsUrl }} 165 | catalogInfoPath: '/catalog-info.yaml' 166 | 167 | output: 168 | links: 169 | - title: Repository 170 | url: ${{ steps['publish'].output.remoteUrl }} 171 | - title: Terraform Workspace 172 | url: ${{ steps['terraform-workspace'].output.url }} 173 | - title: Open in catalog 174 | icon: catalog 175 | entityRef: ${{ steps['register'].output.entityRef }} 176 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": ["packages/*", "plugins/*"], 3 | "npmClient": "yarn", 4 | "useWorkspaces": true, 5 | "version": "0.1.0" 6 | } 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "version": "1.0.0", 4 | "private": true, 5 | "engines": { 6 | "node": "16 || 18" 7 | }, 8 | "scripts": { 9 | "dev": "concurrently \"yarn start\" \"yarn start-backend\"", 10 | "start": "yarn workspace app start", 11 | "start-backend": "yarn workspace backend start", 12 | "build:backend": "yarn workspace backend build", 13 | "build:all": "backstage-cli repo build --all", 14 | "build-image": "yarn workspace backend build-image", 15 | "tsc": "tsc", 16 | "tsc:full": "tsc --skipLibCheck false --incremental false", 17 | "clean": "backstage-cli repo clean", 18 | "test": "backstage-cli repo test", 19 | "test:all": "backstage-cli repo test --coverage", 20 | "lint": "backstage-cli repo lint --since origin/main", 21 | "lint:all": "backstage-cli repo lint", 22 | "prettier:check": "prettier --check .", 23 | "new": "backstage-cli new --scope internal" 24 | }, 25 | "workspaces": { 26 | "packages": [ 27 | "packages/*", 28 | "plugins/*" 29 | ] 30 | }, 31 | "devDependencies": { 32 | "@backstage/cli": "^0.22.8", 33 | "@spotify/prettier-config": "^12.0.0", 34 | "concurrently": "^6.0.0", 35 | "lerna": "^4.0.0", 36 | "node-gyp": "^9.0.0", 37 | "prettier": "^2.3.2", 38 | "typescript": "~5.0.0" 39 | }, 40 | "resolutions": { 41 | "@types/react": "^17", 42 | "@types/react-dom": "^17" 43 | }, 44 | "prettier": "@spotify/prettier-config", 45 | "lint-staged": { 46 | "*.{js,jsx,ts,tsx,mjs,cjs}": [ 47 | "eslint --fix", 48 | "prettier --write" 49 | ], 50 | "*.{json,md}": [ 51 | "prettier --write" 52 | ] 53 | }, 54 | "packageManager": "yarn@3.6.0" 55 | } 56 | -------------------------------------------------------------------------------- /packages/README.md: -------------------------------------------------------------------------------- 1 | # The Packages Folder 2 | 3 | This is where your own applications and centrally managed libraries live, each 4 | in a separate folder of its own. 5 | 6 | From the start there's an `app` folder (for the frontend) and a `backend` folder 7 | (for the Node backend), but you can also add more modules in here that house 8 | your core additions and adaptations, such as themes, common React component 9 | libraries, utilities, and similar. 10 | -------------------------------------------------------------------------------- /packages/app/.eslintignore: -------------------------------------------------------------------------------- 1 | public 2 | -------------------------------------------------------------------------------- /packages/app/.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); 7 | -------------------------------------------------------------------------------- /packages/app/cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "baseUrl": "http://localhost:3001", 3 | "fixturesFolder": false, 4 | "pluginsFile": false, 5 | "retries": 3 6 | } 7 | -------------------------------------------------------------------------------- /packages/app/cypress/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["cypress"], 3 | "extends": ["plugin:cypress/recommended"], 4 | "rules": { 5 | "jest/expect-expect": [ 6 | "error", 7 | { 8 | "assertFunctionNames": ["expect", "cy.contains"] 9 | } 10 | ] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/app/cypress/integration/app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | describe('App', () => { 7 | it('should render the catalog', () => { 8 | cy.visit('/'); 9 | cy.contains('My Company Catalog'); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /packages/app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app", 3 | "version": "0.0.0", 4 | "private": true, 5 | "bundled": true, 6 | "backstage": { 7 | "role": "frontend" 8 | }, 9 | "scripts": { 10 | "start": "backstage-cli package start", 11 | "build": "backstage-cli package build", 12 | "clean": "backstage-cli package clean", 13 | "test": "backstage-cli package test", 14 | "lint": "backstage-cli package lint", 15 | "test:e2e": "cross-env PORT=3001 start-server-and-test start http://localhost:3001 cy:dev", 16 | "test:e2e:ci": "cross-env PORT=3001 start-server-and-test start http://localhost:3001 cy:run", 17 | "cy:dev": "cypress open", 18 | "cy:run": "cypress run --browser chrome" 19 | }, 20 | "dependencies": { 21 | "@backstage/app-defaults": "^1.4.0", 22 | "@backstage/catalog-model": "^1.4.0", 23 | "@backstage/cli": "^0.22.8", 24 | "@backstage/core-app-api": "^1.8.1", 25 | "@backstage/core-components": "^0.13.2", 26 | "@backstage/core-plugin-api": "^1.5.2", 27 | "@backstage/integration-react": "^1.1.14", 28 | "@backstage/plugin-api-docs": "^0.9.5", 29 | "@backstage/plugin-catalog": "^1.11.2", 30 | "@backstage/plugin-catalog-common": "^1.0.14", 31 | "@backstage/plugin-catalog-graph": "^0.2.31", 32 | "@backstage/plugin-catalog-import": "^0.9.9", 33 | "@backstage/plugin-catalog-react": "^1.7.0", 34 | "@backstage/plugin-gcp-projects": "^0.3.38", 35 | "@backstage/plugin-github-actions": "^0.6.0", 36 | "@backstage/plugin-org": "^0.6.9", 37 | "@backstage/plugin-permission-react": "^0.4.13", 38 | "@backstage/plugin-scaffolder": "^1.14.0", 39 | "@backstage/plugin-search": "^1.3.2", 40 | "@backstage/plugin-search-react": "^1.6.2", 41 | "@backstage/plugin-tech-radar": "^0.6.5", 42 | "@backstage/plugin-techdocs": "^1.6.4", 43 | "@backstage/plugin-techdocs-module-addons-contrib": "^1.0.14", 44 | "@backstage/plugin-techdocs-react": "^1.1.7", 45 | "@backstage/plugin-user-settings": "^0.7.4", 46 | "@backstage/theme": "^0.4.0", 47 | "@internal/plugin-scaffolder-vault-backend": "^0.1.0", 48 | "@internal/plugin-terraform": "^0.1.0", 49 | "@material-ui/core": "^4.12.2", 50 | "@material-ui/icons": "^4.9.1", 51 | "@vippsno/plugin-azure-resources": "^1.0.8", 52 | "history": "^5.0.0", 53 | "react": "^17.0.2", 54 | "react-dom": "^17.0.2", 55 | "react-router": "^6.3.0", 56 | "react-router-dom": "^6.3.0", 57 | "react-use": "^17.2.4" 58 | }, 59 | "devDependencies": { 60 | "@backstage/test-utils": "^1.4.0", 61 | "@testing-library/dom": "^8.0.0", 62 | "@testing-library/jest-dom": "^5.10.1", 63 | "@testing-library/react": "^12.1.3", 64 | "@testing-library/user-event": "^14.0.0", 65 | "@types/node": "^16.11.26", 66 | "@types/react-dom": "*", 67 | "cross-env": "^7.0.0", 68 | "cypress": "^9.7.0", 69 | "eslint-plugin-cypress": "^2.10.3", 70 | "start-server-and-test": "^1.10.11" 71 | }, 72 | "browserslist": { 73 | "production": [ 74 | ">0.2%", 75 | "not dead", 76 | "not op_mini all" 77 | ], 78 | "development": [ 79 | "last 1 chrome version", 80 | "last 1 firefox version", 81 | "last 1 safari version" 82 | ] 83 | }, 84 | "files": [ 85 | "dist" 86 | ] 87 | } 88 | -------------------------------------------------------------------------------- /packages/app/public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joatmon08/backstage-plugin-terraform/c2177b4f1318fb9f67327f442f2af4834ea0d0b1/packages/app/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /packages/app/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joatmon08/backstage-plugin-terraform/c2177b4f1318fb9f67327f442f2af4834ea0d0b1/packages/app/public/apple-touch-icon.png -------------------------------------------------------------------------------- /packages/app/public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joatmon08/backstage-plugin-terraform/c2177b4f1318fb9f67327f442f2af4834ea0d0b1/packages/app/public/favicon-16x16.png -------------------------------------------------------------------------------- /packages/app/public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joatmon08/backstage-plugin-terraform/c2177b4f1318fb9f67327f442f2af4834ea0d0b1/packages/app/public/favicon-32x32.png -------------------------------------------------------------------------------- /packages/app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joatmon08/backstage-plugin-terraform/c2177b4f1318fb9f67327f442f2af4834ea0d0b1/packages/app/public/favicon.ico -------------------------------------------------------------------------------- /packages/app/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 16 | 17 | 21 | 26 | 27 | 28 | 33 | 39 | 45 | 50 | <%= config.getString('app.title') %> 51 | 52 | 53 | 54 |
55 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /packages/app/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Backstage", 3 | "name": "Backstage", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "48x48", 8 | "type": "image/png" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /packages/app/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /packages/app/public/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | Created by potrace 1.11, written by Peter Selinger 2001-2013 -------------------------------------------------------------------------------- /packages/app/src/App.test.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | import React from 'react'; 7 | import { renderWithEffects } from '@backstage/test-utils'; 8 | import App from './App'; 9 | 10 | describe('App', () => { 11 | it('should render', async () => { 12 | process.env = { 13 | NODE_ENV: 'test', 14 | APP_CONFIG: [ 15 | { 16 | data: { 17 | app: { title: 'Test' }, 18 | backend: { baseUrl: 'http://localhost:7007' }, 19 | techdocs: { 20 | storageUrl: 'http://localhost:7007/api/techdocs/static/docs', 21 | }, 22 | }, 23 | context: 'test', 24 | }, 25 | ] as any, 26 | }; 27 | 28 | const rendered = await renderWithEffects(); 29 | expect(rendered.baseElement).toBeInTheDocument(); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /packages/app/src/App.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | import React from 'react'; 7 | import { Navigate, Route } from 'react-router-dom'; 8 | import { apiDocsPlugin, ApiExplorerPage } from '@backstage/plugin-api-docs'; 9 | import { 10 | CatalogEntityPage, 11 | CatalogIndexPage, 12 | catalogPlugin, 13 | } from '@backstage/plugin-catalog'; 14 | import { 15 | CatalogImportPage, 16 | catalogImportPlugin, 17 | } from '@backstage/plugin-catalog-import'; 18 | import { ScaffolderPage, scaffolderPlugin } from '@backstage/plugin-scaffolder'; 19 | import { orgPlugin } from '@backstage/plugin-org'; 20 | import { SearchPage } from '@backstage/plugin-search'; 21 | import { TechRadarPage } from '@backstage/plugin-tech-radar'; 22 | import { 23 | TechDocsIndexPage, 24 | techdocsPlugin, 25 | TechDocsReaderPage, 26 | } from '@backstage/plugin-techdocs'; 27 | import { TechDocsAddons } from '@backstage/plugin-techdocs-react'; 28 | import { ReportIssue } from '@backstage/plugin-techdocs-module-addons-contrib'; 29 | import { UserSettingsPage } from '@backstage/plugin-user-settings'; 30 | import { apis } from './apis'; 31 | import { entityPage } from './components/catalog/EntityPage'; 32 | import { searchPage } from './components/search/SearchPage'; 33 | import { Root } from './components/Root'; 34 | 35 | import { AlertDisplay, OAuthRequestDialog } from '@backstage/core-components'; 36 | import { createApp } from '@backstage/app-defaults'; 37 | import { AppRouter, FlatRoutes } from '@backstage/core-app-api'; 38 | import { CatalogGraphPage } from '@backstage/plugin-catalog-graph'; 39 | import { RequirePermission } from '@backstage/plugin-permission-react'; 40 | import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common/alpha'; 41 | 42 | const app = createApp({ 43 | apis, 44 | bindRoutes({ bind }) { 45 | bind(catalogPlugin.externalRoutes, { 46 | createComponent: scaffolderPlugin.routes.root, 47 | viewTechDoc: techdocsPlugin.routes.docRoot, 48 | }); 49 | bind(apiDocsPlugin.externalRoutes, { 50 | registerApi: catalogImportPlugin.routes.importPage, 51 | }); 52 | bind(scaffolderPlugin.externalRoutes, { 53 | registerComponent: catalogImportPlugin.routes.importPage, 54 | }); 55 | bind(orgPlugin.externalRoutes, { 56 | catalogIndex: catalogPlugin.routes.catalogIndex, 57 | }); 58 | }, 59 | }); 60 | 61 | const routes = ( 62 | 63 | } /> 64 | } /> 65 | } 68 | > 69 | {entityPage} 70 | 71 | } /> 72 | } 75 | > 76 | 77 | 78 | 79 | 80 | } /> 81 | } /> 82 | } 85 | /> 86 | 90 | 91 | 92 | } 93 | /> 94 | }> 95 | {searchPage} 96 | 97 | } /> 98 | } /> 99 | 100 | ); 101 | 102 | export default app.createRoot( 103 | <> 104 | 105 | 106 | 107 | {routes} 108 | 109 | , 110 | ); 111 | -------------------------------------------------------------------------------- /packages/app/src/apis.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | import { 7 | ScmIntegrationsApi, 8 | scmIntegrationsApiRef, 9 | ScmAuth, 10 | } from '@backstage/integration-react'; 11 | import { 12 | AnyApiFactory, 13 | configApiRef, 14 | createApiFactory, 15 | } from '@backstage/core-plugin-api'; 16 | 17 | export const apis: AnyApiFactory[] = [ 18 | createApiFactory({ 19 | api: scmIntegrationsApiRef, 20 | deps: { configApi: configApiRef }, 21 | factory: ({ configApi }) => ScmIntegrationsApi.fromConfig(configApi), 22 | }), 23 | ScmAuth.createDefaultApiFactory(), 24 | ]; 25 | -------------------------------------------------------------------------------- /packages/app/src/components/Root/LogoFull.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | import React from 'react'; 7 | import { makeStyles } from '@material-ui/core'; 8 | 9 | const useStyles = makeStyles({ 10 | svg: { 11 | width: 'auto', 12 | height: 30, 13 | }, 14 | path: { 15 | fill: '#7df3e1', 16 | }, 17 | }); 18 | const LogoFull = () => { 19 | const classes = useStyles(); 20 | 21 | return ( 22 | 27 | 31 | 32 | ); 33 | }; 34 | 35 | export default LogoFull; 36 | -------------------------------------------------------------------------------- /packages/app/src/components/Root/LogoIcon.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | import React from 'react'; 7 | import { makeStyles } from '@material-ui/core'; 8 | 9 | const useStyles = makeStyles({ 10 | svg: { 11 | width: 'auto', 12 | height: 28, 13 | }, 14 | path: { 15 | fill: '#7df3e1', 16 | }, 17 | }); 18 | 19 | const LogoIcon = () => { 20 | const classes = useStyles(); 21 | 22 | return ( 23 | 28 | 32 | 33 | ); 34 | }; 35 | 36 | export default LogoIcon; 37 | -------------------------------------------------------------------------------- /packages/app/src/components/Root/Root.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | import React, { PropsWithChildren } from 'react'; 7 | import { makeStyles } from '@material-ui/core'; 8 | import HomeIcon from '@material-ui/icons/Home'; 9 | import ExtensionIcon from '@material-ui/icons/Extension'; 10 | import MapIcon from '@material-ui/icons/MyLocation'; 11 | import LibraryBooks from '@material-ui/icons/LibraryBooks'; 12 | import CreateComponentIcon from '@material-ui/icons/AddCircleOutline'; 13 | import LogoFull from './LogoFull'; 14 | import LogoIcon from './LogoIcon'; 15 | import { 16 | Settings as SidebarSettings, 17 | UserSettingsSignInAvatar, 18 | } from '@backstage/plugin-user-settings'; 19 | import { SidebarSearchModal } from '@backstage/plugin-search'; 20 | import { 21 | Sidebar, 22 | sidebarConfig, 23 | SidebarDivider, 24 | SidebarGroup, 25 | SidebarItem, 26 | SidebarPage, 27 | SidebarScrollWrapper, 28 | SidebarSpace, 29 | useSidebarOpenState, 30 | Link, 31 | } from '@backstage/core-components'; 32 | import MenuIcon from '@material-ui/icons/Menu'; 33 | import SearchIcon from '@material-ui/icons/Search'; 34 | 35 | const useSidebarLogoStyles = makeStyles({ 36 | root: { 37 | width: sidebarConfig.drawerWidthClosed, 38 | height: 3 * sidebarConfig.logoHeight, 39 | display: 'flex', 40 | flexFlow: 'row nowrap', 41 | alignItems: 'center', 42 | marginBottom: -14, 43 | }, 44 | link: { 45 | width: sidebarConfig.drawerWidthClosed, 46 | marginLeft: 24, 47 | }, 48 | }); 49 | 50 | const SidebarLogo = () => { 51 | const classes = useSidebarLogoStyles(); 52 | const { isOpen } = useSidebarOpenState(); 53 | 54 | return ( 55 |
56 | 57 | {isOpen ? : } 58 | 59 |
60 | ); 61 | }; 62 | 63 | export const Root = ({ children }: PropsWithChildren<{}>) => ( 64 | 65 | 66 | 67 | } to="/search"> 68 | 69 | 70 | 71 | }> 72 | {/* Global nav, not org-specific */} 73 | 74 | 75 | 76 | 77 | {/* End global nav */} 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | } 88 | to="/settings" 89 | > 90 | 91 | 92 | 93 | {children} 94 | 95 | ); 96 | -------------------------------------------------------------------------------- /packages/app/src/components/Root/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | export { Root } from './Root'; 7 | -------------------------------------------------------------------------------- /packages/app/src/components/catalog/EntityPage.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | import React from 'react'; 7 | import { Button, Grid } from '@material-ui/core'; 8 | import { 9 | EntityApiDefinitionCard, 10 | EntityConsumedApisCard, 11 | EntityConsumingComponentsCard, 12 | EntityHasApisCard, 13 | EntityProvidedApisCard, 14 | EntityProvidingComponentsCard, 15 | } from '@backstage/plugin-api-docs'; 16 | import { 17 | EntityAboutCard, 18 | EntityDependsOnComponentsCard, 19 | EntityDependsOnResourcesCard, 20 | EntityHasComponentsCard, 21 | EntityHasResourcesCard, 22 | EntityHasSubcomponentsCard, 23 | EntityHasSystemsCard, 24 | EntityLayout, 25 | EntityLinksCard, 26 | EntitySwitch, 27 | EntityOrphanWarning, 28 | EntityProcessingErrorsPanel, 29 | isComponentType, 30 | isKind, 31 | hasCatalogProcessingErrors, 32 | isOrphan, 33 | } from '@backstage/plugin-catalog'; 34 | import { 35 | isGithubActionsAvailable, 36 | EntityGithubActionsContent, 37 | } from '@backstage/plugin-github-actions'; 38 | import { 39 | EntityUserProfileCard, 40 | EntityGroupProfileCard, 41 | EntityMembersListCard, 42 | EntityOwnershipCard, 43 | } from '@backstage/plugin-org'; 44 | import { EntityTechdocsContent } from '@backstage/plugin-techdocs'; 45 | import { EmptyState } from '@backstage/core-components'; 46 | import { 47 | Direction, 48 | EntityCatalogGraphCard, 49 | } from '@backstage/plugin-catalog-graph'; 50 | import { 51 | RELATION_API_CONSUMED_BY, 52 | RELATION_API_PROVIDED_BY, 53 | RELATION_CONSUMES_API, 54 | RELATION_DEPENDENCY_OF, 55 | RELATION_DEPENDS_ON, 56 | RELATION_HAS_PART, 57 | RELATION_PART_OF, 58 | RELATION_PROVIDES_API, 59 | } from '@backstage/catalog-model'; 60 | 61 | import { TechDocsAddons } from '@backstage/plugin-techdocs-react'; 62 | import { ReportIssue } from '@backstage/plugin-techdocs-module-addons-contrib'; 63 | import { 64 | TerraformOutputTable, 65 | TerraformRunTable, 66 | TerraformResourceTable, 67 | } from '@internal/plugin-terraform'; 68 | 69 | const techdocsContent = ( 70 | 71 | 72 | 73 | 74 | 75 | ); 76 | 77 | const cicdContent = ( 78 | // This is an example of how you can implement your company's logic in entity page. 79 | // You can for example enforce that all components of type 'service' should use GitHubActions 80 | 81 | 82 | 83 | 84 | 85 | 86 | 96 | Read more 97 | 98 | } 99 | /> 100 | 101 | 102 | ); 103 | 104 | const entityWarningContent = ( 105 | <> 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | ); 123 | 124 | const overviewContent = ( 125 | 126 | {entityWarningContent} 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | ); 142 | 143 | const serviceEntityPage = ( 144 | 145 | 146 | {overviewContent} 147 | 148 | 149 | 150 | {cicdContent} 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | {techdocsContent} 193 | 194 | 195 | ); 196 | 197 | const websiteEntityPage = ( 198 | 199 | 200 | {overviewContent} 201 | 202 | 203 | 204 | {cicdContent} 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | {techdocsContent} 236 | 237 | 238 | ); 239 | 240 | /** 241 | * NOTE: This page is designed to work on small screens such as mobile devices. 242 | * This is based on Material UI Grid. If breakpoints are used, each grid item must set the `xs` prop to a column size or to `true`, 243 | * since this does not default. If no breakpoints are used, the items will equitably share the available space. 244 | * https://material-ui.com/components/grid/#basic-grid. 245 | */ 246 | 247 | const defaultEntityPage = ( 248 | 249 | 250 | {overviewContent} 251 | 252 | 253 | 254 | {techdocsContent} 255 | 256 | 257 | ); 258 | 259 | const componentPage = ( 260 | 261 | 262 | {serviceEntityPage} 263 | 264 | 265 | 266 | {websiteEntityPage} 267 | 268 | 269 | {defaultEntityPage} 270 | 271 | ); 272 | 273 | const apiPage = ( 274 | 275 | 276 | 277 | {entityWarningContent} 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | ); 307 | 308 | const userPage = ( 309 | 310 | 311 | 312 | {entityWarningContent} 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | ); 323 | 324 | const groupPage = ( 325 | 326 | 327 | 328 | {entityWarningContent} 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | ); 342 | 343 | const systemPage = ( 344 | 345 | 346 | 347 | {entityWarningContent} 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 386 | 387 | 388 | ); 389 | 390 | const domainPage = ( 391 | 392 | 393 | 394 | {entityWarningContent} 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | ); 408 | 409 | export const entityPage = ( 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | {defaultEntityPage} 419 | 420 | ); 421 | -------------------------------------------------------------------------------- /packages/app/src/components/search/SearchPage.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | import React from 'react'; 7 | import { makeStyles, Theme, Grid, Paper } from '@material-ui/core'; 8 | 9 | import { CatalogSearchResultListItem } from '@backstage/plugin-catalog'; 10 | import { 11 | catalogApiRef, 12 | CATALOG_FILTER_EXISTS, 13 | } from '@backstage/plugin-catalog-react'; 14 | import { TechDocsSearchResultListItem } from '@backstage/plugin-techdocs'; 15 | 16 | import { SearchType } from '@backstage/plugin-search'; 17 | import { 18 | SearchBar, 19 | SearchFilter, 20 | SearchResult, 21 | SearchPagination, 22 | useSearch, 23 | } from '@backstage/plugin-search-react'; 24 | import { 25 | CatalogIcon, 26 | Content, 27 | DocsIcon, 28 | Header, 29 | Page, 30 | } from '@backstage/core-components'; 31 | import { useApi } from '@backstage/core-plugin-api'; 32 | 33 | const useStyles = makeStyles((theme: Theme) => ({ 34 | bar: { 35 | padding: theme.spacing(1, 0), 36 | }, 37 | filters: { 38 | padding: theme.spacing(2), 39 | marginTop: theme.spacing(2), 40 | }, 41 | filter: { 42 | '& + &': { 43 | marginTop: theme.spacing(2.5), 44 | }, 45 | }, 46 | })); 47 | 48 | const SearchPage = () => { 49 | const classes = useStyles(); 50 | const { types } = useSearch(); 51 | const catalogApi = useApi(catalogApiRef); 52 | 53 | return ( 54 | 55 |
56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | , 72 | }, 73 | { 74 | value: 'techdocs', 75 | name: 'Documentation', 76 | icon: , 77 | }, 78 | ]} 79 | /> 80 | 81 | {types.includes('techdocs') && ( 82 | { 87 | // Return a list of entities which are documented. 88 | const { items } = await catalogApi.getEntities({ 89 | fields: ['metadata.name'], 90 | filter: { 91 | 'metadata.annotations.backstage.io/techdocs-ref': 92 | CATALOG_FILTER_EXISTS, 93 | }, 94 | }); 95 | 96 | const names = items.map(entity => entity.metadata.name); 97 | names.sort(); 98 | return names; 99 | }} 100 | /> 101 | )} 102 | 108 | 114 | 115 | 116 | 117 | 118 | 119 | } /> 120 | } /> 121 | 122 | 123 | 124 | 125 | 126 | ); 127 | }; 128 | 129 | export const searchPage = ; 130 | -------------------------------------------------------------------------------- /packages/app/src/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | import '@backstage/cli/asset-types'; 7 | import React from 'react'; 8 | import ReactDOM from 'react-dom'; 9 | import App from './App'; 10 | 11 | ReactDOM.render(, document.getElementById('root')); 12 | -------------------------------------------------------------------------------- /packages/app/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | import '@testing-library/jest-dom'; 7 | -------------------------------------------------------------------------------- /packages/backend/.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); 7 | -------------------------------------------------------------------------------- /packages/backend/Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | # This dockerfile builds an image for the backend package. 5 | # It should be executed with the root of the repo as docker context. 6 | # 7 | # Before building this image, be sure to have run the following commands in the repo root: 8 | # 9 | # yarn install 10 | # yarn tsc 11 | # yarn build:backend 12 | # 13 | # Once the commands have been run, you can build the image using `yarn build-image` 14 | 15 | FROM node:16-bullseye-slim 16 | 17 | # Install sqlite3 dependencies. You can skip this if you don't use sqlite3 in the image, 18 | # in which case you should also move better-sqlite3 to "devDependencies" in package.json. 19 | RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ 20 | --mount=type=cache,target=/var/lib/apt,sharing=locked \ 21 | apt-get update && \ 22 | apt-get install -y --no-install-recommends libsqlite3-dev python3 build-essential && \ 23 | yarn config set python /usr/bin/python3 24 | 25 | # From here on we use the least-privileged `node` user to run the backend. 26 | USER node 27 | 28 | # This should create the app dir as `node`. 29 | # If it is instead created as `root` then the `tar` command below will fail: `can't create directory 'packages/': Permission denied`. 30 | # If this occurs, then ensure BuildKit is enabled (`DOCKER_BUILDKIT=1`) so the app dir is correctly created as `node`. 31 | WORKDIR /app 32 | 33 | # This switches many Node.js dependencies to production mode. 34 | ENV NODE_ENV production 35 | 36 | # Copy repo skeleton first, to avoid unnecessary docker cache invalidation. 37 | # The skeleton contains the package.json of each package in the monorepo, 38 | # and along with yarn.lock and the root package.json, that's enough to run yarn install. 39 | COPY --chown=node:node yarn.lock package.json packages/backend/dist/skeleton.tar.gz ./ 40 | RUN tar xzf skeleton.tar.gz && rm skeleton.tar.gz 41 | 42 | RUN --mount=type=cache,target=/home/node/.cache/yarn,sharing=locked,uid=1000,gid=1000 \ 43 | yarn install --frozen-lockfile --production --network-timeout 300000 44 | 45 | # Then copy the rest of the backend bundle, along with any other files we might want. 46 | COPY --chown=node:node packages/backend/dist/bundle.tar.gz app-config*.yaml ./ 47 | RUN tar xzf bundle.tar.gz && rm bundle.tar.gz 48 | 49 | CMD ["node", "packages/backend", "--config", "app-config.yaml", "--config", "app-config.production.yaml"] 50 | -------------------------------------------------------------------------------- /packages/backend/README.md: -------------------------------------------------------------------------------- 1 | # example-backend 2 | 3 | This package is an EXAMPLE of a Backstage backend. 4 | 5 | The main purpose of this package is to provide a test bed for Backstage plugins 6 | that have a backend part. Feel free to experiment locally or within your fork by 7 | adding dependencies and routes to this backend, to try things out. 8 | 9 | Our goal is to eventually amend the create-app flow of the CLI, such that a 10 | production ready version of a backend skeleton is made alongside the frontend 11 | app. Until then, feel free to experiment here! 12 | 13 | ## Development 14 | 15 | To run the example backend, first go to the project root and run 16 | 17 | ```bash 18 | yarn install 19 | ``` 20 | 21 | You should only need to do this once. 22 | 23 | After that, go to the `packages/backend` directory and run 24 | 25 | ```bash 26 | yarn start 27 | ``` 28 | 29 | If you want to override any configuration locally, for example adding any secrets, 30 | you can do so in `app-config.local.yaml`. 31 | 32 | The backend starts up on port 7007 per default. 33 | 34 | ## Populating The Catalog 35 | 36 | If you want to use the catalog functionality, you need to add so called 37 | locations to the backend. These are places where the backend can find some 38 | entity descriptor data to consume and serve. For more information, see 39 | [Software Catalog Overview - Adding Components to the Catalog](https://backstage.io/docs/features/software-catalog/#adding-components-to-the-catalog). 40 | 41 | To get started quickly, this template already includes some statically configured example locations 42 | in `app-config.yaml` under `catalog.locations`. You can remove and replace these locations as you 43 | like, and also override them for local development in `app-config.local.yaml`. 44 | 45 | ## Authentication 46 | 47 | We chose [Passport](http://www.passportjs.org/) as authentication platform due 48 | to its comprehensive set of supported authentication 49 | [strategies](http://www.passportjs.org/packages/). 50 | 51 | Read more about the 52 | [auth-backend](https://github.com/backstage/backstage/blob/master/plugins/auth-backend/README.md) 53 | and 54 | [how to add a new provider](https://github.com/backstage/backstage/blob/master/docs/auth/add-auth-provider.md) 55 | 56 | ## Documentation 57 | 58 | - [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md) 59 | - [Backstage Documentation](https://backstage.io/docs) 60 | -------------------------------------------------------------------------------- /packages/backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backend", 3 | "version": "0.0.0", 4 | "main": "dist/index.cjs.js", 5 | "types": "src/index.ts", 6 | "private": true, 7 | "backstage": { 8 | "role": "backend" 9 | }, 10 | "scripts": { 11 | "start": "backstage-cli package start", 12 | "build": "backstage-cli package build", 13 | "lint": "backstage-cli package lint", 14 | "test": "backstage-cli package test", 15 | "clean": "backstage-cli package clean", 16 | "build-image": "docker build ../.. -f Dockerfile --tag backstage" 17 | }, 18 | "dependencies": { 19 | "@backstage/backend-common": "^0.19.0", 20 | "@backstage/backend-tasks": "^0.5.3", 21 | "@backstage/catalog-client": "^1.4.2", 22 | "@backstage/catalog-model": "^1.4.0", 23 | "@backstage/config": "^1.0.8", 24 | "@backstage/integration": "^1.5.0", 25 | "@backstage/plugin-app-backend": "^0.3.46", 26 | "@backstage/plugin-auth-backend": "^0.18.4", 27 | "@backstage/plugin-auth-node": "^0.2.15", 28 | "@backstage/plugin-catalog-backend": "^1.10.0", 29 | "@backstage/plugin-permission-common": "^0.7.6", 30 | "@backstage/plugin-permission-node": "^0.7.9", 31 | "@backstage/plugin-proxy-backend": "^0.2.40", 32 | "@backstage/plugin-scaffolder-backend": "^1.15.0", 33 | "@backstage/plugin-search-backend": "^1.3.2", 34 | "@backstage/plugin-search-backend-module-pg": "^0.5.7", 35 | "@backstage/plugin-search-backend-node": "^1.2.2", 36 | "@backstage/plugin-techdocs-backend": "^1.6.3", 37 | "@internal/plugin-scaffolder-terraform-backend": "^0.1.0", 38 | "app": "link:../app", 39 | "backstage-aws-cloudformation-plugin": "^2.0.7", 40 | "better-sqlite3": "^8.0.0", 41 | "dockerode": "^3.3.1", 42 | "express": "^4.17.1", 43 | "express-promise-router": "^4.1.0", 44 | "pg": "^8.3.0", 45 | "winston": "^3.2.1" 46 | }, 47 | "devDependencies": { 48 | "@backstage/cli": "^0.22.8", 49 | "@types/dockerode": "^3.3.0", 50 | "@types/express": "^4.17.6", 51 | "@types/express-serve-static-core": "^4.17.5", 52 | "@types/luxon": "^2.0.4" 53 | }, 54 | "files": [ 55 | "dist" 56 | ] 57 | } 58 | -------------------------------------------------------------------------------- /packages/backend/src/index.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | import { PluginEnvironment } from './types'; 7 | 8 | describe('test', () => { 9 | it('unbreaks the test runner', () => { 10 | const unbreaker = {} as PluginEnvironment; 11 | expect(unbreaker).toBeTruthy(); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /packages/backend/src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | /* 7 | * Hi! 8 | * 9 | * Note that this is an EXAMPLE Backstage backend. Please check the README. 10 | * 11 | * Happy hacking! 12 | */ 13 | 14 | import Router from 'express-promise-router'; 15 | import { 16 | createServiceBuilder, 17 | loadBackendConfig, 18 | getRootLogger, 19 | useHotMemoize, 20 | notFoundHandler, 21 | CacheManager, 22 | DatabaseManager, 23 | SingleHostDiscovery, 24 | UrlReaders, 25 | ServerTokenManager, 26 | } from '@backstage/backend-common'; 27 | import { TaskScheduler } from '@backstage/backend-tasks'; 28 | import { Config } from '@backstage/config'; 29 | import app from './plugins/app'; 30 | import auth from './plugins/auth'; 31 | import catalog from './plugins/catalog'; 32 | import scaffolder from './plugins/scaffolder'; 33 | import proxy from './plugins/proxy'; 34 | import techdocs from './plugins/techdocs'; 35 | import search from './plugins/search'; 36 | import { PluginEnvironment } from './types'; 37 | import { ServerPermissionClient } from '@backstage/plugin-permission-node'; 38 | import { DefaultIdentityClient } from '@backstage/plugin-auth-node'; 39 | 40 | function makeCreateEnv(config: Config) { 41 | const root = getRootLogger(); 42 | const reader = UrlReaders.default({ logger: root, config }); 43 | const discovery = SingleHostDiscovery.fromConfig(config); 44 | const cacheManager = CacheManager.fromConfig(config); 45 | const databaseManager = DatabaseManager.fromConfig(config, { logger: root }); 46 | const tokenManager = ServerTokenManager.noop(); 47 | const taskScheduler = TaskScheduler.fromConfig(config); 48 | 49 | const identity = DefaultIdentityClient.create({ 50 | discovery, 51 | }); 52 | const permissions = ServerPermissionClient.fromConfig(config, { 53 | discovery, 54 | tokenManager, 55 | }); 56 | 57 | root.info(`Created UrlReader ${reader}`); 58 | 59 | return (plugin: string): PluginEnvironment => { 60 | const logger = root.child({ type: 'plugin', plugin }); 61 | const database = databaseManager.forPlugin(plugin); 62 | const cache = cacheManager.forPlugin(plugin); 63 | const scheduler = taskScheduler.forPlugin(plugin); 64 | return { 65 | logger, 66 | database, 67 | cache, 68 | config, 69 | reader, 70 | discovery, 71 | tokenManager, 72 | scheduler, 73 | permissions, 74 | identity, 75 | }; 76 | }; 77 | } 78 | 79 | async function main() { 80 | const config = await loadBackendConfig({ 81 | argv: process.argv, 82 | logger: getRootLogger(), 83 | }); 84 | const createEnv = makeCreateEnv(config); 85 | 86 | const catalogEnv = useHotMemoize(module, () => createEnv('catalog')); 87 | const scaffolderEnv = useHotMemoize(module, () => createEnv('scaffolder')); 88 | const authEnv = useHotMemoize(module, () => createEnv('auth')); 89 | const proxyEnv = useHotMemoize(module, () => createEnv('proxy')); 90 | const techdocsEnv = useHotMemoize(module, () => createEnv('techdocs')); 91 | const searchEnv = useHotMemoize(module, () => createEnv('search')); 92 | const appEnv = useHotMemoize(module, () => createEnv('app')); 93 | 94 | const apiRouter = Router(); 95 | apiRouter.use('/catalog', await catalog(catalogEnv)); 96 | apiRouter.use('/scaffolder', await scaffolder(scaffolderEnv)); 97 | apiRouter.use('/auth', await auth(authEnv)); 98 | apiRouter.use('/techdocs', await techdocs(techdocsEnv)); 99 | apiRouter.use('/proxy', await proxy(proxyEnv)); 100 | apiRouter.use('/search', await search(searchEnv)); 101 | 102 | // Add backends ABOVE this line; this 404 handler is the catch-all fallback 103 | apiRouter.use(notFoundHandler()); 104 | 105 | const service = createServiceBuilder(module) 106 | .loadConfig(config) 107 | .addRouter('/api', apiRouter) 108 | .addRouter('', await app(appEnv)); 109 | 110 | await service.start().catch(err => { 111 | console.log(err); 112 | process.exit(1); 113 | }); 114 | } 115 | 116 | module.hot?.accept(); 117 | main().catch(error => { 118 | console.error('Backend failed to start up', error); 119 | process.exit(1); 120 | }); 121 | -------------------------------------------------------------------------------- /packages/backend/src/plugins/app.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | import { createRouter } from '@backstage/plugin-app-backend'; 7 | import { Router } from 'express'; 8 | import { PluginEnvironment } from '../types'; 9 | 10 | export default async function createPlugin( 11 | env: PluginEnvironment, 12 | ): Promise { 13 | return await createRouter({ 14 | logger: env.logger, 15 | config: env.config, 16 | database: env.database, 17 | appPackageName: 'app', 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /packages/backend/src/plugins/auth.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | import { 7 | createRouter, 8 | providers, 9 | defaultAuthProviderFactories, 10 | } from '@backstage/plugin-auth-backend'; 11 | import { Router } from 'express'; 12 | import { PluginEnvironment } from '../types'; 13 | 14 | export default async function createPlugin( 15 | env: PluginEnvironment, 16 | ): Promise { 17 | return await createRouter({ 18 | logger: env.logger, 19 | config: env.config, 20 | database: env.database, 21 | discovery: env.discovery, 22 | tokenManager: env.tokenManager, 23 | providerFactories: { 24 | ...defaultAuthProviderFactories, 25 | 26 | // This replaces the default GitHub auth provider with a customized one. 27 | // The `signIn` option enables sign-in for this provider, using the 28 | // identity resolution logic that's provided in the `resolver` callback. 29 | // 30 | // This particular resolver makes all users share a single "guest" identity. 31 | // It should only be used for testing and trying out Backstage. 32 | // 33 | // If you want to use a production ready resolver you can switch to 34 | // the one that is commented out below, it looks up a user entity in the 35 | // catalog using the GitHub username of the authenticated user. 36 | // That resolver requires you to have user entities populated in the catalog, 37 | // for example using https://backstage.io/docs/integrations/github/org 38 | // 39 | // There are other resolvers to choose from, and you can also create 40 | // your own, see the auth documentation for more details: 41 | // 42 | // https://backstage.io/docs/auth/identity-resolver 43 | github: providers.github.create({ 44 | signIn: { 45 | resolver(_, ctx) { 46 | const userRef = 'user:default/guest'; // Must be a full entity reference 47 | return ctx.issueToken({ 48 | claims: { 49 | sub: userRef, // The user's own identity 50 | ent: [userRef], // A list of identities that the user claims ownership through 51 | }, 52 | }); 53 | }, 54 | // resolver: providers.github.resolvers.usernameMatchingUserEntityName(), 55 | }, 56 | }), 57 | }, 58 | }); 59 | } 60 | -------------------------------------------------------------------------------- /packages/backend/src/plugins/catalog.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | import { CatalogBuilder } from '@backstage/plugin-catalog-backend'; 7 | import { ScaffolderEntitiesProcessor } from '@backstage/plugin-scaffolder-backend'; 8 | import { Router } from 'express'; 9 | import { PluginEnvironment } from '../types'; 10 | 11 | import { 12 | CloudFormationRegionProcessor, 13 | CloudFormationStackProcessor, 14 | } from 'backstage-aws-cloudformation-plugin'; 15 | 16 | export default async function createPlugin( 17 | env: PluginEnvironment, 18 | ): Promise { 19 | const builder = await CatalogBuilder.create(env); 20 | builder.addProcessor(new ScaffolderEntitiesProcessor()); 21 | builder.addProcessor(new CloudFormationStackProcessor(env.config)); 22 | builder.addProcessor(new CloudFormationRegionProcessor(env.config)); 23 | const { processingEngine, router } = await builder.build(); 24 | await processingEngine.start(); 25 | return router; 26 | } 27 | -------------------------------------------------------------------------------- /packages/backend/src/plugins/proxy.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | import { createRouter } from '@backstage/plugin-proxy-backend'; 7 | import { Router } from 'express'; 8 | import { PluginEnvironment } from '../types'; 9 | 10 | export default async function createPlugin( 11 | env: PluginEnvironment, 12 | ): Promise { 13 | return await createRouter({ 14 | logger: env.logger, 15 | config: env.config, 16 | discovery: env.discovery, 17 | }); 18 | } 19 | -------------------------------------------------------------------------------- /packages/backend/src/plugins/scaffolder.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | import { CatalogClient } from '@backstage/catalog-client'; 7 | import { 8 | createBuiltinActions, 9 | createRouter, 10 | } from '@backstage/plugin-scaffolder-backend'; 11 | import { ScmIntegrations } from '@backstage/integration'; 12 | import { Router } from 'express'; 13 | import type { PluginEnvironment } from '../types'; 14 | import { 15 | createTerraformProjectAction, 16 | createTerraformWorkspaceAction, 17 | createTerraformRunAction, 18 | createTerraformVariablesAction, 19 | } from '@internal/plugin-scaffolder-terraform-backend'; 20 | 21 | import { authenticateToVaultWithGitHubAction, getTerraformTokenFromVaultAction } from '@internal/plugin-scaffolder-vault-backend'; 22 | 23 | export default async function createPlugin( 24 | env: PluginEnvironment, 25 | ): Promise { 26 | const catalogClient = new CatalogClient({ 27 | discoveryApi: env.discovery, 28 | }); 29 | const integrations = ScmIntegrations.fromConfig(env.config); 30 | 31 | const builtInActions = createBuiltinActions({ 32 | integrations, 33 | catalogClient, 34 | config: env.config, 35 | reader: env.reader, 36 | }); 37 | 38 | const actions = [ 39 | ...builtInActions, 40 | createTerraformProjectAction({ 41 | configApi: env.config, 42 | discoveryApi: env.discovery, 43 | }), 44 | createTerraformWorkspaceAction({ 45 | configApi: env.config, 46 | discoveryApi: env.discovery, 47 | }), 48 | createTerraformRunAction({ 49 | configApi: env.config, 50 | discoveryApi: env.discovery, 51 | }), 52 | createTerraformVariablesAction({ 53 | configApi: env.config, 54 | discoveryApi: env.discovery, 55 | }), 56 | authenticateToVaultWithGitHubAction({ 57 | configApi: env.config, 58 | discoveryApi: env.discovery, 59 | }), 60 | getTerraformTokenFromVaultAction({ 61 | configApi: env.config, 62 | discoveryApi: env.discovery, 63 | }), 64 | ]; 65 | 66 | return await createRouter({ 67 | actions, 68 | logger: env.logger, 69 | config: env.config, 70 | database: env.database, 71 | reader: env.reader, 72 | catalogClient, 73 | identity: env.identity, 74 | permissions: env.permissions, 75 | }); 76 | } 77 | -------------------------------------------------------------------------------- /packages/backend/src/plugins/search.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | import { useHotCleanup } from '@backstage/backend-common'; 7 | import { createRouter } from '@backstage/plugin-search-backend'; 8 | import { 9 | IndexBuilder, 10 | LunrSearchEngine, 11 | } from '@backstage/plugin-search-backend-node'; 12 | import { PluginEnvironment } from '../types'; 13 | import { DefaultCatalogCollatorFactory } from '@backstage/plugin-catalog-backend'; 14 | import { DefaultTechDocsCollatorFactory } from '@backstage/plugin-techdocs-backend'; 15 | import { Router } from 'express'; 16 | 17 | export default async function createPlugin( 18 | env: PluginEnvironment, 19 | ): Promise { 20 | // Initialize a connection to a search engine. 21 | const searchEngine = new LunrSearchEngine({ 22 | logger: env.logger, 23 | }); 24 | const indexBuilder = new IndexBuilder({ 25 | logger: env.logger, 26 | searchEngine, 27 | }); 28 | 29 | const schedule = env.scheduler.createScheduledTaskRunner({ 30 | frequency: { minutes: 10 }, 31 | timeout: { minutes: 15 }, 32 | // A 3 second delay gives the backend server a chance to initialize before 33 | // any collators are executed, which may attempt requests against the API. 34 | initialDelay: { seconds: 3 }, 35 | }); 36 | 37 | // Collators are responsible for gathering documents known to plugins. This 38 | // collator gathers entities from the software catalog. 39 | indexBuilder.addCollator({ 40 | schedule, 41 | factory: DefaultCatalogCollatorFactory.fromConfig(env.config, { 42 | discovery: env.discovery, 43 | tokenManager: env.tokenManager, 44 | }), 45 | }); 46 | 47 | // collator gathers entities from techdocs. 48 | indexBuilder.addCollator({ 49 | schedule, 50 | factory: DefaultTechDocsCollatorFactory.fromConfig(env.config, { 51 | discovery: env.discovery, 52 | logger: env.logger, 53 | tokenManager: env.tokenManager, 54 | }), 55 | }); 56 | 57 | // The scheduler controls when documents are gathered from collators and sent 58 | // to the search engine for indexing. 59 | const { scheduler } = await indexBuilder.build(); 60 | scheduler.start(); 61 | 62 | useHotCleanup(module, () => scheduler.stop()); 63 | 64 | return await createRouter({ 65 | engine: indexBuilder.getSearchEngine(), 66 | types: indexBuilder.getDocumentTypes(), 67 | permissions: env.permissions, 68 | config: env.config, 69 | logger: env.logger, 70 | }); 71 | } 72 | -------------------------------------------------------------------------------- /packages/backend/src/plugins/techdocs.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | import { DockerContainerRunner } from '@backstage/backend-common'; 7 | import { 8 | createRouter, 9 | Generators, 10 | Preparers, 11 | Publisher, 12 | } from '@backstage/plugin-techdocs-backend'; 13 | import Docker from 'dockerode'; 14 | import { Router } from 'express'; 15 | import { PluginEnvironment } from '../types'; 16 | 17 | export default async function createPlugin( 18 | env: PluginEnvironment, 19 | ): Promise { 20 | // Preparers are responsible for fetching source files for documentation. 21 | const preparers = await Preparers.fromConfig(env.config, { 22 | logger: env.logger, 23 | reader: env.reader, 24 | }); 25 | 26 | // Docker client (conditionally) used by the generators, based on techdocs.generators config. 27 | const dockerClient = new Docker(); 28 | const containerRunner = new DockerContainerRunner({ dockerClient }); 29 | 30 | // Generators are used for generating documentation sites. 31 | const generators = await Generators.fromConfig(env.config, { 32 | logger: env.logger, 33 | containerRunner, 34 | }); 35 | 36 | // Publisher is used for 37 | // 1. Publishing generated files to storage 38 | // 2. Fetching files from storage and passing them to TechDocs frontend. 39 | const publisher = await Publisher.fromConfig(env.config, { 40 | logger: env.logger, 41 | discovery: env.discovery, 42 | }); 43 | 44 | // checks if the publisher is working and logs the result 45 | await publisher.getReadiness(); 46 | 47 | return await createRouter({ 48 | preparers, 49 | generators, 50 | publisher, 51 | logger: env.logger, 52 | config: env.config, 53 | discovery: env.discovery, 54 | cache: env.cache, 55 | }); 56 | } 57 | -------------------------------------------------------------------------------- /packages/backend/src/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | import { Logger } from 'winston'; 7 | import { Config } from '@backstage/config'; 8 | import { 9 | PluginCacheManager, 10 | PluginDatabaseManager, 11 | PluginEndpointDiscovery, 12 | TokenManager, 13 | UrlReader, 14 | } from '@backstage/backend-common'; 15 | import { PluginTaskScheduler } from '@backstage/backend-tasks'; 16 | import { PermissionEvaluator } from '@backstage/plugin-permission-common'; 17 | import { IdentityApi } from '@backstage/plugin-auth-node'; 18 | 19 | export type PluginEnvironment = { 20 | logger: Logger; 21 | database: PluginDatabaseManager; 22 | cache: PluginCacheManager; 23 | config: Config; 24 | reader: UrlReader; 25 | discovery: PluginEndpointDiscovery; 26 | tokenManager: TokenManager; 27 | scheduler: PluginTaskScheduler; 28 | permissions: PermissionEvaluator; 29 | identity: IdentityApi; 30 | }; 31 | -------------------------------------------------------------------------------- /plugins/README.md: -------------------------------------------------------------------------------- 1 | # The Plugins Folder 2 | 3 | This is where your own plugins and their associated modules live, each in a 4 | separate folder of its own. 5 | 6 | If you want to create a new plugin here, go to your project root directory, run 7 | the command `yarn backstage-cli create`, and follow the on-screen instructions. 8 | 9 | You can also check out existing plugins on [the plugin marketplace](https://backstage.io/plugins)! 10 | -------------------------------------------------------------------------------- /plugins/scaffolder-terraform-backend/.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); 7 | -------------------------------------------------------------------------------- /plugins/scaffolder-terraform-backend/README.md: -------------------------------------------------------------------------------- 1 | # scaffolder-terraform 2 | 3 | Welcome to the scaffolder-terraform backend plugin! 4 | 5 | _This plugin was created through the Backstage CLI_ 6 | 7 | ## Getting started 8 | 9 | Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn 10 | start` in the root directory, and then navigating to [/scaffolder-terraform](http://localhost:3000/scaffolder-terraform). 11 | 12 | You can also serve the plugin in isolation by running `yarn start` in the plugin directory. 13 | This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads. 14 | It is only meant for local development, and the setup for it can be found inside the [/dev](/dev) directory. 15 | 16 | ## Install 17 | 18 | Go to `packages/backend/src/plugins/scaffolder.ts` and add the following: 19 | 20 | ```typescript 21 | import { CatalogClient } from '@backstage/catalog-client'; 22 | import { 23 | createBuiltinActions, 24 | createRouter, 25 | } from '@backstage/plugin-scaffolder-backend'; 26 | import { ScmIntegrations } from '@backstage/integration'; 27 | import { Router } from 'express'; 28 | import type { PluginEnvironment } from '../types'; 29 | import { createTerraformWorkspaceAction } from '@internal/plugin-scaffolder-terraform-backend'; 30 | 31 | export default async function createPlugin( 32 | env: PluginEnvironment, 33 | ): Promise { 34 | const catalogClient = new CatalogClient({ 35 | discoveryApi: env.discovery, 36 | }); 37 | const integrations = ScmIntegrations.fromConfig(env.config); 38 | 39 | const builtInActions = createBuiltinActions({ 40 | integrations, 41 | catalogClient, 42 | config: env.config, 43 | reader: env.reader, 44 | }); 45 | 46 | const actions = [ 47 | ...builtInActions, 48 | createTerraformWorkspaceAction({ 49 | configApi: env.config, 50 | discoveryApi: env.discovery, 51 | }), 52 | ]; 53 | 54 | return await createRouter({ 55 | actions, 56 | logger: env.logger, 57 | config: env.config, 58 | database: env.database, 59 | reader: env.reader, 60 | catalogClient, 61 | identity: env.identity, 62 | permissions: env.permissions, 63 | }); 64 | } 65 | ``` 66 | 67 | This adds the custom action for creating a Terraform workspace. 68 | 69 | In your `app-config.yaml`, add the base URL for Terraform Cloud/Enterprise 70 | and set up the proxy to point to Terraform Cloud/Enterprise. 71 | 72 | Note that if you want to limit the scope of Backstage accesses in 73 | TFC/E, use a Terraform token that has read-only access to an 74 | organization and configure your Scaffolder template to 75 | use custom actions to authenticate to Vault and get a Terraform 76 | team token. 77 | 78 | Otherwise, you'll need to use a `TF_TOKEN` with write access 79 | to your Terraform organization. 80 | 81 | ```yaml 82 | scaffolder: 83 | terraform: 84 | baseUrl: https://app.terraform.io 85 | 86 | proxy: 87 | '/terraform': 88 | target: https://app.terraform.io 89 | headers: 90 | ### If you have Vault, you can limit 91 | ### this TF_TOKEN to read-only access 92 | ### to the organization so frontend 93 | ### components can pull information 94 | ### about Terraform Cloud runs. 95 | Authorization: Bearer ${TF_TOKEN} 96 | Accept: 'application/vnd.api+json' 97 | allowedHeaders: ['Authorization'] 98 | ``` 99 | 100 | Check out an example template using the action in `examples/terraform/template.yaml`. 101 | -------------------------------------------------------------------------------- /plugins/scaffolder-terraform-backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@internal/plugin-scaffolder-terraform-backend", 3 | "version": "0.1.0", 4 | "main": "src/index.ts", 5 | "types": "src/index.ts", 6 | "license": "Apache-2.0", 7 | "private": true, 8 | "publishConfig": { 9 | "access": "public", 10 | "main": "dist/index.cjs.js", 11 | "types": "dist/index.d.ts" 12 | }, 13 | "backstage": { 14 | "role": "backend-plugin" 15 | }, 16 | "scripts": { 17 | "start": "backstage-cli package start", 18 | "build": "backstage-cli package build", 19 | "lint": "backstage-cli package lint", 20 | "test": "backstage-cli package test", 21 | "clean": "backstage-cli package clean", 22 | "prepack": "backstage-cli package prepack", 23 | "postpack": "backstage-cli package postpack" 24 | }, 25 | "dependencies": { 26 | "@backstage/backend-common": "^0.19.0", 27 | "@backstage/config": "^1.0.8", 28 | "@backstage/core-plugin-api": "^1.5.2", 29 | "@backstage/plugin-scaffolder-node": "^0.1.4", 30 | "@types/express": "*", 31 | "express": "^4.17.1", 32 | "express-promise-router": "^4.1.0", 33 | "node-fetch": "^2.6.7", 34 | "winston": "^3.2.1", 35 | "yn": "^4.0.0" 36 | }, 37 | "devDependencies": { 38 | "@backstage/cli": "^0.22.8", 39 | "@types/supertest": "^2.0.12", 40 | "msw": "^1.0.0", 41 | "supertest": "^6.2.4" 42 | }, 43 | "files": [ 44 | "dist" 45 | ] 46 | } 47 | -------------------------------------------------------------------------------- /plugins/scaffolder-terraform-backend/src/actions/createProject.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; 7 | import { ConfigApi, DiscoveryApi } from '@backstage/core-plugin-api'; 8 | import { TerraformClient } from '../api'; 9 | import { ProjectRequest } from '../api/types'; 10 | 11 | export const createTerraformProjectAction = (options: { 12 | configApi: ConfigApi; 13 | discoveryApi: DiscoveryApi; 14 | }) => { 15 | return createTemplateAction<{ 16 | organization: string; 17 | name: string; 18 | token: string; 19 | }>({ 20 | id: 'terraform:project:create', 21 | schema: { 22 | input: { 23 | required: ['organization', 'name'], 24 | type: 'object', 25 | properties: { 26 | organization: { 27 | type: 'string', 28 | title: 'Terraform Organization', 29 | description: 'The Terraform organization to create project', 30 | }, 31 | name: { 32 | type: 'string', 33 | title: 'Name', 34 | description: 'The name of the Terraform project', 35 | }, 36 | token: { 37 | type: 'string', 38 | title: 'Terraform Token', 39 | description: 'Terraform token', 40 | }, 41 | }, 42 | }, 43 | }, 44 | async handler(ctx) { 45 | const { organization, name, token } = ctx.input; 46 | 47 | const terraformApi = new TerraformClient(options, token); 48 | 49 | const projectRequest: ProjectRequest = { 50 | data: { 51 | type: 'projects', 52 | attributes: { 53 | name: name, 54 | }, 55 | }, 56 | }; 57 | ctx.logger.info(JSON.stringify(projectRequest)); 58 | 59 | const project = await terraformApi.createProject( 60 | organization, 61 | projectRequest, 62 | ); 63 | 64 | ctx.logger.info(`Created project ${name} with id ${project.id}`); 65 | ctx.output('name', name); 66 | ctx.output('id', project.id); 67 | }, 68 | }); 69 | }; 70 | -------------------------------------------------------------------------------- /plugins/scaffolder-terraform-backend/src/actions/createRun.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; 7 | import { ConfigApi, DiscoveryApi } from '@backstage/core-plugin-api'; 8 | import { TerraformClient } from '../api'; 9 | 10 | export const createTerraformRunAction = (options: { 11 | configApi: ConfigApi; 12 | discoveryApi: DiscoveryApi; 13 | }) => { 14 | return createTemplateAction<{ 15 | workspaceID: string; 16 | token: string; 17 | }>({ 18 | id: 'terraform:run:create', 19 | schema: { 20 | input: { 21 | required: ['workspaceID'], 22 | type: 'object', 23 | properties: { 24 | workspaceID: { 25 | type: 'string', 26 | title: 'Terraform Workspace ID', 27 | description: 'The Terraform workspace ID to queue a run', 28 | }, 29 | token: { 30 | type: 'string', 31 | title: 'Terraform Token', 32 | description: 'Terraform token', 33 | }, 34 | }, 35 | }, 36 | }, 37 | async handler(ctx) { 38 | const { workspaceID, token } = ctx.input; 39 | 40 | const message = 'Started by Backstage scaffolder task'; 41 | 42 | const terraformApi = new TerraformClient(options, token); 43 | 44 | const run = await terraformApi.createRun(workspaceID, message); 45 | 46 | ctx.logger.info(`Started run with id ${run.data.id}`); 47 | ctx.output('id', run.data.id); 48 | }, 49 | }); 50 | }; 51 | -------------------------------------------------------------------------------- /plugins/scaffolder-terraform-backend/src/actions/createVariables.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; 7 | import { ConfigApi, DiscoveryApi } from '@backstage/core-plugin-api'; 8 | import { TerraformClient } from '../api'; 9 | import { VariableRequest } from '../api/types'; 10 | 11 | export const createTerraformVariablesAction = (options: { 12 | configApi: ConfigApi; 13 | discoveryApi: DiscoveryApi; 14 | }) => { 15 | return createTemplateAction<{ 16 | workspaceID: string; 17 | variables: { 18 | key: string; 19 | value: string; 20 | description: string; 21 | category: string; 22 | hcl?: boolean; 23 | sensitive?: boolean; 24 | }[]; 25 | token: string; 26 | }>({ 27 | id: 'terraform:variables:create', 28 | schema: { 29 | input: { 30 | required: ['workspaceID', 'variables'], 31 | type: 'object', 32 | properties: { 33 | workspaceID: { 34 | type: 'string', 35 | title: 'Terraform Workspace ID', 36 | description: 'The Terraform workspace ID to create variables', 37 | }, 38 | variables: { 39 | type: 'array', 40 | items: { 41 | type: 'object', 42 | required: ['key', 'value', 'description', 'category'], 43 | properties: { 44 | key: { 45 | title: 'Variable Key', 46 | description: 'The key for Terraform variable', 47 | type: 'string', 48 | }, 49 | value: { 50 | title: 'Variable Value', 51 | description: 'The value for Terraform variable', 52 | type: 'string', 53 | }, 54 | description: { 55 | title: 'Description', 56 | description: 'The description for Terraform variable', 57 | type: 'string', 58 | }, 59 | category: { 60 | title: 'Category', 61 | description: 'Set to "terraform" or "env"', 62 | type: 'string', 63 | }, 64 | isHCLObject: { 65 | title: 'HCL Object', 66 | description: 'Set if string should be an HCL object', 67 | type: 'boolean', 68 | }, 69 | sensitive: { 70 | title: 'Sensitive', 71 | description: 'Set if variable is sensitive', 72 | type: 'boolean', 73 | }, 74 | }, 75 | }, 76 | }, 77 | token: { 78 | type: 'string', 79 | title: 'Terraform Token', 80 | description: 'Terraform token', 81 | }, 82 | }, 83 | }, 84 | }, 85 | async handler(ctx) { 86 | const { workspaceID, variables, token } = ctx.input; 87 | 88 | const variableIDs = []; 89 | 90 | const terraformApi = new TerraformClient(options, token); 91 | 92 | for (const v of variables) { 93 | const request: VariableRequest = { 94 | data: { 95 | type: 'vars', 96 | attributes: v, 97 | }, 98 | }; 99 | const variable = await terraformApi.createVariable( 100 | workspaceID, 101 | request, 102 | ); 103 | 104 | ctx.logger.info( 105 | `Created variable for ${v.key} with id ${variable.data.id}`, 106 | ); 107 | variableIDs.push(variable.data.id); 108 | } 109 | 110 | ctx.output('ids', variableIDs); 111 | }, 112 | }); 113 | }; 114 | -------------------------------------------------------------------------------- /plugins/scaffolder-terraform-backend/src/actions/createWorkspace.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; 7 | import { ConfigApi, DiscoveryApi } from '@backstage/core-plugin-api'; 8 | import { TerraformClient } from '../api'; 9 | import { WorkspaceRequest } from '../api/types'; 10 | 11 | const DEFAULT_TERRAFORM_URL = 'https://app.terraform.io'; 12 | 13 | export const createTerraformWorkspaceAction = (options: { 14 | configApi: ConfigApi; 15 | discoveryApi: DiscoveryApi; 16 | }) => { 17 | return createTemplateAction<{ 18 | organization: string; 19 | name: string; 20 | vcsSourceProvider: string; 21 | vcsOwner: string; 22 | vcsRepo: string; 23 | vcsAuthUser: string; 24 | workingDirectory: string; 25 | agentPoolId?: string; 26 | autoApply?: boolean; 27 | project?: string; 28 | queueRuns?: boolean; 29 | token: string; 30 | }>({ 31 | id: 'terraform:workspace:create', 32 | schema: { 33 | input: { 34 | required: [ 35 | 'organization', 36 | 'name', 37 | 'vcsSourceProvider', 38 | 'vcsOwner', 39 | 'vcsRepo', 40 | ], 41 | type: 'object', 42 | properties: { 43 | token: { 44 | type: 'string', 45 | title: 'Terraform Token', 46 | description: 'Terraform token', 47 | }, 48 | organization: { 49 | type: 'string', 50 | title: 'Terraform Organization', 51 | description: 'The Terraform organization to create workspace', 52 | }, 53 | name: { 54 | type: 'string', 55 | title: 'Name', 56 | description: 'The name of the Terraform workspace', 57 | }, 58 | vcsSourceProvider: { 59 | type: 'string', 60 | title: 'VCS Source Provider', 61 | description: 62 | 'The source provider for version control. Must be "github", "github_enterprise", "gitlab_hosted", "gitlab_community_edition", "gitlab_enterprise_edition", or "ado_server"', 63 | }, 64 | vcsOwner: { 65 | type: 'string', 66 | title: 'VCS Owner Identifer', 67 | description: 'The owner identifier for version control repository', 68 | }, 69 | vcsRepo: { 70 | type: 'string', 71 | title: 'VCS Repository Identifer', 72 | description: 'The repo identifier for version control repository', 73 | }, 74 | vcsAuthUser: { 75 | type: 'string', 76 | title: 'VCS User for Authentication', 77 | description: 78 | 'The VCS user in Terraform workspace for authentication', 79 | }, 80 | workingDirectory: { 81 | type: 'string', 82 | title: 'Working Directory', 83 | description: 'Working directory of Terraform configuration.', 84 | }, 85 | agentPoolId: { 86 | type: 'string', 87 | title: 'Terraform Agent Pool ID', 88 | description: 'The identifier for Terraform agent pool', 89 | }, 90 | autoApply: { 91 | type: 'boolean', 92 | title: 'Auto-Approve Applies', 93 | description: 'Enable auto-approval for applies in workspace.', 94 | }, 95 | project: { 96 | type: 'string', 97 | title: 'Name of Project in Workspace', 98 | description: 'The name of the project in the workspace', 99 | }, 100 | queueRuns: { 101 | type: 'boolean', 102 | title: 'Queue Runs', 103 | description: 'Queue a run after workspace creation', 104 | }, 105 | }, 106 | }, 107 | }, 108 | async handler(ctx) { 109 | const { 110 | token, 111 | organization, 112 | name, 113 | vcsSourceProvider, 114 | vcsOwner, 115 | vcsRepo, 116 | vcsAuthUser, 117 | workingDirectory, 118 | agentPoolId, 119 | autoApply, 120 | project, 121 | queueRuns, 122 | } = ctx.input; 123 | const baseUrl = 124 | options.configApi.getOptionalString('scaffolder.terraform.baseUrl') || 125 | DEFAULT_TERRAFORM_URL; 126 | 127 | const terraformApi = new TerraformClient(options, token); 128 | 129 | const oauthClient = await terraformApi.getOAuthClients( 130 | organization, 131 | vcsSourceProvider, 132 | ); 133 | if (oauthClient.id === undefined) { 134 | throw new Error(`oauth client not found for ${vcsSourceProvider}`); 135 | } else { 136 | ctx.logger.info( 137 | `Found OAuth client for ${vcsSourceProvider} with id ${oauthClient.id}`, 138 | ); 139 | } 140 | 141 | const oauthToken = await terraformApi.getOAuthToken( 142 | oauthClient.id, 143 | vcsAuthUser, 144 | ); 145 | 146 | ctx.logger.info(`Found OAuth Token with id ${oauthToken.id}`); 147 | 148 | const terraformProject = 149 | project !== undefined 150 | ? await terraformApi.getProject(organization, project).then(p => { 151 | return p.id; 152 | }) 153 | : ''; 154 | ctx.logger.info(`Found project with id ${terraformProject}`); 155 | 156 | const workspaceRequest: WorkspaceRequest = { 157 | data: { 158 | type: 'workspaces', 159 | attributes: { 160 | name: name, 161 | description: 'Generated by Backstage', 162 | 'agent-pool-id': agentPoolId, 163 | 'auto-apply': autoApply, 164 | 'vcs-repo': { 165 | identifier: `${vcsOwner}/${vcsRepo}`, 166 | 'oauth-token-id': oauthToken.id, 167 | }, 168 | 'working-directory': workingDirectory, 169 | 'source-name': 'Backstage', 170 | 'queue-all-runs': queueRuns, 171 | }, 172 | relationships: { 173 | project: { 174 | data: { 175 | type: 'projects', 176 | id: terraformProject, 177 | }, 178 | }, 179 | }, 180 | }, 181 | }; 182 | 183 | ctx.logger.info(JSON.stringify(workspaceRequest)); 184 | 185 | const workspace = await terraformApi.createWorkspace( 186 | organization, 187 | workspaceRequest, 188 | ); 189 | ctx.logger.info(`Created workspace with id ${workspace.data.id}`); 190 | ctx.output('url', `${baseUrl}/app/${organization}/workspaces/${name}`); 191 | ctx.output('id', workspace.data.id); 192 | }, 193 | }); 194 | }; 195 | -------------------------------------------------------------------------------- /plugins/scaffolder-terraform-backend/src/actions/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | export { createTerraformProjectAction } from './createProject'; 7 | export { createTerraformWorkspaceAction } from './createWorkspace'; 8 | export { createTerraformRunAction } from './createRun'; 9 | export { createTerraformVariablesAction } from './createVariables'; 10 | -------------------------------------------------------------------------------- /plugins/scaffolder-terraform-backend/src/api/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | import { ConfigApi, DiscoveryApi } from '@backstage/core-plugin-api'; 7 | import { 8 | OAuthClient, 9 | OAuthToken, 10 | Project, 11 | ProjectRequest, 12 | Run, 13 | RunRequest, 14 | TerraformApi, 15 | Variable, 16 | VariableRequest, 17 | Workspace, 18 | WorkspaceRequest, 19 | } from './types'; 20 | 21 | const DEFAULT_PROXY_PATH = '/terraform/api'; 22 | 23 | type Options = { 24 | discoveryApi: DiscoveryApi; 25 | configApi: ConfigApi; 26 | }; 27 | 28 | export class TerraformClient implements TerraformApi { 29 | private readonly discoveryApi: DiscoveryApi; 30 | private readonly proxyPath: string; 31 | private readonly headers: Record; 32 | 33 | constructor(options: Options, token?: string) { 34 | this.discoveryApi = options.discoveryApi; 35 | 36 | const proxyPath = options.configApi.getOptionalString( 37 | 'terraformCloud.proxyPath', 38 | ); 39 | this.proxyPath = proxyPath ?? DEFAULT_PROXY_PATH; 40 | this.headers = this.getHeaders(token); 41 | } 42 | 43 | private async getUrls() { 44 | const proxyUrl = await this.discoveryApi.getBaseUrl('proxy'); 45 | return { 46 | apiUrl: `${proxyUrl}${this.proxyPath}/v2`, 47 | baseUrl: `${proxyUrl}${this.proxyPath}`, 48 | }; 49 | } 50 | 51 | private getHeaders(token?: string): Record { 52 | const headers = { 53 | 'Content-Type': 'application/vnd.api+json' 54 | }; 55 | 56 | if (token != undefined) { 57 | headers['Authorization'] = `Bearer ${token}`; 58 | } 59 | 60 | return headers; 61 | } 62 | 63 | async getOAuthClients( 64 | organization: string, 65 | serviceProvider: string, 66 | ): Promise { 67 | const { apiUrl } = await this.getUrls(); 68 | 69 | const response = await fetch( 70 | `${apiUrl}/organizations/${organization}/oauth-clients`, 71 | { 72 | method: 'GET', 73 | headers: this.headers, 74 | }, 75 | ); 76 | 77 | if (!response.ok) { 78 | throw new Error( 79 | `failed to fetch oauth clients, status ${response.status}: ${response.statusText}`, 80 | ); 81 | } 82 | 83 | return response.json().then(clients => { 84 | return clients.data.filter( 85 | (client: OAuthClient) => 86 | client.attributes['service-provider'] === serviceProvider, 87 | )[0]; 88 | }); 89 | } 90 | 91 | async getOAuthToken(clientID: string, user: string, token?: string): Promise { 92 | const { apiUrl } = await this.getUrls(); 93 | 94 | const response = await fetch( 95 | `${apiUrl}/oauth-clients/${clientID}/oauth-tokens`, 96 | { 97 | method: 'GET', 98 | headers: this.headers, 99 | }, 100 | ); 101 | 102 | if (!response.ok) { 103 | throw new Error( 104 | `failed to fetch oauth client token, status ${response.status}: ${response.statusText}`, 105 | ); 106 | } 107 | 108 | return response.json().then(tokens => { 109 | if (user !== undefined) { 110 | return tokens.data.filter( 111 | (token: OAuthToken) => 112 | token.attributes['service-provider-user'] === user, 113 | )[0]; 114 | } 115 | return tokens.data[0]; 116 | }); 117 | } 118 | 119 | async getProject(organization: string, project: string, token?: string): Promise { 120 | const { apiUrl } = await this.getUrls(); 121 | 122 | const urlSearchParams = new URLSearchParams({ 123 | 'filter[names]': project, 124 | }); 125 | 126 | const response = await fetch( 127 | `${apiUrl}/organizations/${organization}/projects?${urlSearchParams}`, 128 | { 129 | method: 'GET', 130 | headers: this.headers, 131 | }, 132 | ); 133 | 134 | if (!response.ok) { 135 | throw new Error( 136 | `failed to get project, status ${response.status}: ${response.statusText}`, 137 | ); 138 | } 139 | 140 | return response.json().then(projects => { 141 | return projects.data[0]; 142 | }); 143 | } 144 | 145 | async createProject( 146 | organization: string, 147 | project: ProjectRequest, 148 | ): Promise { 149 | const { apiUrl } = await this.getUrls(); 150 | 151 | const response = await fetch( 152 | `${apiUrl}/organizations/${organization}/projects`, 153 | { 154 | method: 'POST', 155 | body: JSON.stringify(project), 156 | headers: this.headers, 157 | }, 158 | ); 159 | 160 | if (!response.ok) { 161 | throw new Error( 162 | `failed to create project, status ${response.status}: ${response.statusText}`, 163 | ); 164 | } 165 | 166 | const terraformProject = await response.json(); 167 | return terraformProject.data; 168 | } 169 | 170 | async createWorkspace( 171 | organization: string, 172 | workspace: WorkspaceRequest, 173 | ): Promise { 174 | const { apiUrl } = await this.getUrls(); 175 | 176 | const response = await fetch( 177 | `${apiUrl}/organizations/${organization}/workspaces`, 178 | { 179 | method: 'POST', 180 | body: JSON.stringify(workspace), 181 | headers: this.headers, 182 | }, 183 | ); 184 | 185 | if (!response.ok) { 186 | throw new Error( 187 | `failed to create workspace, status ${response.status}: ${response.statusText}`, 188 | ); 189 | } 190 | 191 | return response.json(); 192 | } 193 | 194 | async createVariable( 195 | workspace: string, 196 | variable: VariableRequest, 197 | ): Promise { 198 | const { apiUrl } = await this.getUrls(); 199 | 200 | const response = await fetch(`${apiUrl}/workspaces/${workspace}/vars`, { 201 | method: 'POST', 202 | body: JSON.stringify(variable), 203 | headers: this.headers, 204 | }); 205 | 206 | if (!response.ok) { 207 | throw new Error( 208 | `failed to create variable, status ${response.status}: ${response.statusText}`, 209 | ); 210 | } 211 | 212 | return response.json(); 213 | } 214 | 215 | async createRun(workspaceID: string, message: string, token?: string): Promise { 216 | const { apiUrl } = await this.getUrls(); 217 | 218 | const runRequest: RunRequest = { 219 | data: { 220 | attributes: { 221 | message: message, 222 | }, 223 | type: 'runs', 224 | relationships: { 225 | workspace: { 226 | data: { 227 | type: 'workspaces', 228 | id: workspaceID, 229 | }, 230 | }, 231 | }, 232 | }, 233 | }; 234 | 235 | const response = await fetch(`${apiUrl}/runs`, { 236 | method: 'POST', 237 | body: JSON.stringify(runRequest), 238 | headers: this.headers, 239 | }); 240 | 241 | if (!response.ok) { 242 | throw new Error( 243 | `failed to create run, status ${response.status}: ${response.statusText}`, 244 | ); 245 | } 246 | 247 | return response.json(); 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /plugins/scaffolder-terraform-backend/src/api/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | export interface TerraformApi { 7 | getProject(organization: string, project: string): Promise; 8 | createProject( 9 | organization: string, 10 | project: ProjectRequest, 11 | ): Promise; 12 | getOAuthClients( 13 | organization: string, 14 | serviceProvider: string, 15 | ): Promise; 16 | getOAuthToken(clientID: string, user?: string): Promise; 17 | createWorkspace( 18 | organization: string, 19 | workspace: WorkspaceRequest, 20 | ): Promise; 21 | createRun(workspaceID: string, message: string): Promise; 22 | } 23 | 24 | export interface Run { 25 | data: { 26 | id: string; 27 | }; 28 | } 29 | 30 | export interface RunRequest { 31 | data: { 32 | attributes: { 33 | message: string; 34 | }; 35 | type: string; 36 | relationships: { 37 | workspace: { 38 | data: { 39 | type: string; 40 | id: string; 41 | }; 42 | }; 43 | }; 44 | }; 45 | } 46 | 47 | export interface OAuthClient { 48 | id: string; 49 | attributes: { 50 | 'service-provider'?: string; 51 | 'http-url'?: string; 52 | }; 53 | } 54 | 55 | export interface OAuthTokens { 56 | data: OAuthToken[]; 57 | } 58 | 59 | export interface OAuthToken { 60 | id: string; 61 | attributes: { 62 | 'service-provider-user'?: string; 63 | }; 64 | } 65 | 66 | export interface Project { 67 | id: string; 68 | } 69 | 70 | export interface ProjectRequest { 71 | data: { 72 | type: 'projects'; 73 | attributes: { 74 | name: string; 75 | }; 76 | }; 77 | } 78 | 79 | export interface Workspace { 80 | data: { 81 | id: string; 82 | }; 83 | links: { 84 | 'self-html': string; 85 | }; 86 | } 87 | 88 | export interface VCS { 89 | branch?: string; 90 | identifier?: string; 91 | 'ingress-submodules'?: boolean; 92 | 'oauth-token-id'?: string; 93 | 'tags-regex'?: string; 94 | } 95 | 96 | export interface WorkspaceRequest { 97 | data: { 98 | type: string; 99 | attributes: { 100 | name: string; 101 | 'agent-pool-id'?: string; 102 | 'allow-destroy-plan'?: boolean; 103 | 'assessments-enabled'?: boolean; 104 | 'auto-apply'?: boolean; 105 | description?: string; 106 | 'execution-mode'?: string; 107 | 'file-triggers-enabled'?: boolean; 108 | 'global-remote-state'?: boolean; 109 | 'queue-all-runs'?: boolean; 110 | 'source-name'?: string; 111 | 'source-url'?: string; 112 | 'speculative-enabled'?: boolean; 113 | 'terraform-version'?: string; 114 | 'trigger-patterns'?: string[]; 115 | 'trigger-prefixes'?: string[]; 116 | 'vcs-repo'?: VCS; 117 | 'working-directory'?: string; 118 | }; 119 | relationships?: { 120 | project: { 121 | data: { 122 | type: string; 123 | id: string; 124 | }; 125 | }; 126 | }; 127 | }; 128 | } 129 | 130 | export interface VariableRequest { 131 | data: { 132 | readonly type: 'vars'; 133 | attributes: { 134 | key: string; 135 | value: string; 136 | description: string; 137 | category: string; 138 | hcl?: boolean; 139 | sensitive?: boolean; 140 | }; 141 | }; 142 | } 143 | 144 | export interface Variable { 145 | data: { 146 | id: string; 147 | }; 148 | } 149 | -------------------------------------------------------------------------------- /plugins/scaffolder-terraform-backend/src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | export * from './actions'; 7 | -------------------------------------------------------------------------------- /plugins/scaffolder-vault-backend/.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); 7 | -------------------------------------------------------------------------------- /plugins/scaffolder-vault-backend/README.md: -------------------------------------------------------------------------------- 1 | # scaffolder-vault 2 | 3 | Welcome to the scaffolder-vault backend plugin! 4 | 5 | _This plugin was created through the Backstage CLI_ 6 | 7 | ## Getting started 8 | 9 | Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn 10 | start` in the root directory, and then navigating to [/scaffolder-vault](http://localhost:3000/scaffolder-vault). 11 | 12 | You can also serve the plugin in isolation by running `yarn start` in the plugin directory. 13 | This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads. 14 | It is only meant for local development, and the setup for it can be found inside the [/dev](/dev) directory. 15 | 16 | ## Install 17 | 18 | Go to `packages/backend/src/plugins/scaffolder.ts` and add the following: 19 | 20 | ```typescript 21 | import { CatalogClient } from '@backstage/catalog-client'; 22 | import { 23 | createBuiltinActions, 24 | createRouter, 25 | } from '@backstage/plugin-scaffolder-backend'; 26 | import { ScmIntegrations } from '@backstage/integration'; 27 | import { Router } from 'express'; 28 | import type { PluginEnvironment } from '../types'; 29 | import { createvaultWorkspaceAction } from '@internal/plugin-scaffolder-vault-backend'; 30 | 31 | export default async function createPlugin( 32 | env: PluginEnvironment, 33 | ): Promise { 34 | const catalogClient = new CatalogClient({ 35 | discoveryApi: env.discovery, 36 | }); 37 | const integrations = ScmIntegrations.fromConfig(env.config); 38 | 39 | const builtInActions = createBuiltinActions({ 40 | integrations, 41 | catalogClient, 42 | config: env.config, 43 | reader: env.reader, 44 | }); 45 | 46 | const actions = [ 47 | ...builtInActions, 48 | authenticateToVaultWithGitHubAction({ 49 | configApi: env.config, 50 | discoveryApi: env.discovery, 51 | }), 52 | getTerraformTokenFromVaultAction({ 53 | configApi: env.config, 54 | discoveryApi: env.discovery, 55 | }), 56 | ]; 57 | 58 | return await createRouter({ 59 | actions, 60 | logger: env.logger, 61 | config: env.config, 62 | database: env.database, 63 | reader: env.reader, 64 | catalogClient, 65 | identity: env.identity, 66 | permissions: env.permissions, 67 | }); 68 | } 69 | ``` 70 | 71 | This adds the custom action for creating a vault workspace. 72 | 73 | In your `app-config.yaml`, set up the proxy to point to Vault. 74 | 75 | ```yaml 76 | proxy: 77 | '/vault': 78 | target: http://127.0.0.1:8200 79 | allowedHeaders: ['X-Vault-Token'] 80 | ``` 81 | 82 | Check out an example template using the action in `examples/vault/template.yaml`. 83 | -------------------------------------------------------------------------------- /plugins/scaffolder-vault-backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@internal/plugin-scaffolder-vault-backend", 3 | "version": "0.1.0", 4 | "main": "src/index.ts", 5 | "types": "src/index.ts", 6 | "license": "Apache-2.0", 7 | "private": true, 8 | "publishConfig": { 9 | "access": "public", 10 | "main": "dist/index.cjs.js", 11 | "types": "dist/index.d.ts" 12 | }, 13 | "backstage": { 14 | "role": "backend-plugin" 15 | }, 16 | "scripts": { 17 | "start": "backstage-cli package start", 18 | "build": "backstage-cli package build", 19 | "lint": "backstage-cli package lint", 20 | "test": "backstage-cli package test", 21 | "clean": "backstage-cli package clean", 22 | "prepack": "backstage-cli package prepack", 23 | "postpack": "backstage-cli package postpack" 24 | }, 25 | "dependencies": { 26 | "@backstage/backend-common": "^0.19.0", 27 | "@backstage/config": "^1.0.8", 28 | "@backstage/core-plugin-api": "^1.5.2", 29 | "@backstage/plugin-scaffolder-node": "^0.1.4", 30 | "@types/express": "*", 31 | "express": "^4.17.1", 32 | "express-promise-router": "^4.1.0", 33 | "node-fetch": "^2.6.7", 34 | "winston": "^3.2.1", 35 | "yn": "^4.0.0" 36 | }, 37 | "devDependencies": { 38 | "@backstage/cli": "^0.22.8", 39 | "@types/supertest": "^2.0.12", 40 | "msw": "^1.0.0", 41 | "supertest": "^6.2.4" 42 | }, 43 | "files": [ 44 | "dist" 45 | ] 46 | } 47 | -------------------------------------------------------------------------------- /plugins/scaffolder-vault-backend/src/actions/authenticate.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; 7 | import { ConfigApi, DiscoveryApi } from '@backstage/core-plugin-api'; 8 | import { VaultClient } from '../api'; 9 | 10 | export const authenticateToVaultWithGitHubAction = (options: { 11 | configApi: ConfigApi; 12 | discoveryApi: DiscoveryApi; 13 | }) => { 14 | return createTemplateAction<{ 15 | token: string; 16 | path: string; 17 | }>({ 18 | id: 'vault:authenticate:github', 19 | schema: { 20 | input: { 21 | required: [ 22 | 'token', 23 | 'path', 24 | ], 25 | type: 'object', 26 | properties: { 27 | token: { 28 | type: 'string', 29 | title: 'GitHub Token', 30 | description: 'The GitHub OAuth2 user token to authenticate to Vault', 31 | }, 32 | path: { 33 | type: 'string', 34 | title: 'GitHub Auth Method Path', 35 | description: 'The auth method path for GitHub', 36 | }, 37 | }, 38 | }, 39 | }, 40 | async handler(ctx) { 41 | const { 42 | token, 43 | path, 44 | } = ctx.input; 45 | 46 | const vaultApi = new VaultClient(options); 47 | 48 | ctx.logger.info(`Using GitHub OAuth user token to log into Vault`); 49 | 50 | const payload = JSON.stringify({token: `${token}`}); 51 | const vaultToken = await vaultApi.authenticate(`${path}/login`, payload); 52 | 53 | ctx.logger.info(`Got Vault token, adding to secrets.VAULT_TOKEN`); 54 | ctx.secrets["VAULT_TOKEN"] = vaultToken; 55 | }, 56 | }); 57 | }; 58 | -------------------------------------------------------------------------------- /plugins/scaffolder-vault-backend/src/actions/getTerraformToken.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; 7 | import { ConfigApi, DiscoveryApi } from '@backstage/core-plugin-api'; 8 | import { VaultClient } from '../api'; 9 | 10 | export const getTerraformTokenFromVaultAction = (options: { 11 | configApi: ConfigApi; 12 | discoveryApi: DiscoveryApi; 13 | }) => { 14 | return createTemplateAction<{ 15 | token: string; 16 | mount: string; 17 | role: string; 18 | }>({ 19 | id: 'vault:secrets:terraform', 20 | schema: { 21 | input: { 22 | required: [ 23 | 'token', 24 | 'mount', 25 | 'role', 26 | ], 27 | type: 'object', 28 | properties: { 29 | token: { 30 | type: 'string', 31 | title: 'Vault Token', 32 | description: 'The Vault token to access Terraform secrets engine', 33 | }, 34 | mount: { 35 | type: 'string', 36 | title: 'Vault Mount Path', 37 | description: 'The Vault mount path for Terraform secrets engine' 38 | }, 39 | role: { 40 | type: 'string', 41 | title: 'Vault Role', 42 | description: 'The Vault role to access the Terraform secrets engine', 43 | }, 44 | }, 45 | }, 46 | }, 47 | async handler(ctx) { 48 | const { 49 | token, 50 | mount, 51 | role, 52 | } = ctx.input; 53 | 54 | const vaultApi = new VaultClient(options); 55 | 56 | ctx.logger.info(`Using Vault token to get Terraform token at ${mount}/creds/${role}`); 57 | 58 | const terraformCloudToken = await vaultApi.getTerraformToken(token, mount, role); 59 | 60 | ctx.logger.info(`Got Terraform Cloud token ${terraformCloudToken.data.token_id}, adding to secrets.TERRAFORM_CLOUD_TOKEN`); 61 | ctx.secrets["TERRAFORM_CLOUD_TOKEN"] = terraformCloudToken.data.token; 62 | }, 63 | }); 64 | }; 65 | -------------------------------------------------------------------------------- /plugins/scaffolder-vault-backend/src/actions/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | export { authenticateToVaultWithGitHubAction } from './authenticate'; 7 | export { getTerraformTokenFromVaultAction } from './getTerraformToken'; -------------------------------------------------------------------------------- /plugins/scaffolder-vault-backend/src/api/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | import { ConfigApi, DiscoveryApi } from '@backstage/core-plugin-api'; 7 | import { 8 | AuthResponse, 9 | TerraformSecretResponse, 10 | VaultApi 11 | } from './types'; 12 | 13 | const DEFAULT_PROXY_PATH = '/vault'; 14 | 15 | type Options = { 16 | discoveryApi: DiscoveryApi; 17 | configApi: ConfigApi; 18 | }; 19 | 20 | export class VaultClient implements VaultApi { 21 | private readonly discoveryApi: DiscoveryApi; 22 | private readonly proxyPath: string; 23 | 24 | constructor(options: Options) { 25 | this.discoveryApi = options.discoveryApi; 26 | 27 | const proxyPath = options.configApi.getOptionalString( 28 | 'vault.proxyPath', 29 | ); 30 | this.proxyPath = proxyPath ?? DEFAULT_PROXY_PATH; 31 | } 32 | 33 | private async getUrls() { 34 | const proxyUrl = await this.discoveryApi.getBaseUrl('proxy'); 35 | return { 36 | apiUrl: `${proxyUrl}${this.proxyPath}/v1`, 37 | baseUrl: `${proxyUrl}${this.proxyPath}`, 38 | }; 39 | } 40 | 41 | async authenticate( 42 | path: string, 43 | payload: string, 44 | ): Promise { 45 | const { apiUrl } = await this.getUrls(); 46 | 47 | const response = await fetch(`${apiUrl}/auth/${path}`, { 48 | method: 'POST', 49 | body: payload 50 | }); 51 | 52 | if (!response.ok) { 53 | throw new Error( 54 | `failed to authenticate to Vault, status ${response.status}: ${response.statusText}`, 55 | ); 56 | } 57 | 58 | return response.json().then((resp: AuthResponse) => { 59 | return resp.auth.client_token; 60 | }); 61 | } 62 | 63 | async getTerraformToken(token: string, mount: string, role: string): Promise { 64 | const { apiUrl } = await this.getUrls(); 65 | const vaultHeaders = new Headers(); 66 | vaultHeaders.append("X-Vault-Token", token); 67 | 68 | const response = await fetch(`${apiUrl}/${mount}/creds/${role}`, { 69 | method: 'GET', 70 | headers: vaultHeaders, 71 | }); 72 | 73 | if (!response.ok) { 74 | throw new Error( 75 | `failed to get Terraform token from Vault, status ${response.status}: ${response.statusText}`, 76 | ); 77 | } 78 | 79 | return response.json(); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /plugins/scaffolder-vault-backend/src/api/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | export interface VaultApi { 7 | authenticate(path: string, payload: string): Promise; 8 | getTerraformToken(token: string, mount: string, role: string): Promise; 9 | } 10 | 11 | export interface AuthResponse { 12 | auth: { 13 | renewable: boolean; 14 | lease_duration: number; 15 | metadata: Map; 16 | policies: string[]; 17 | accessor: string; 18 | client_token: string; 19 | }; 20 | } 21 | 22 | export interface TerraformSecretResponse { 23 | request_id: string; 24 | lease_id: string; 25 | lease_duration: number; 26 | renewable: true; 27 | data: { 28 | token: string; 29 | token_id: string; 30 | }; 31 | } -------------------------------------------------------------------------------- /plugins/scaffolder-vault-backend/src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | export * from './actions'; 7 | -------------------------------------------------------------------------------- /plugins/terraform/.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); 7 | -------------------------------------------------------------------------------- /plugins/terraform/README.md: -------------------------------------------------------------------------------- 1 | # terraform 2 | 3 | Welcome to the terraform plugin! 4 | 5 | _This plugin was created through the Backstage CLI_ 6 | 7 | ## Getting started 8 | 9 | Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn start` in the root directory, and then navigating to [/terraform](http://localhost:3000/terraform). 10 | 11 | You can also serve the plugin in isolation by running `yarn start` in the plugin directory. 12 | This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads. 13 | It is only meant for local development, and the setup for it can be found inside the [/dev](./dev) directory. 14 | -------------------------------------------------------------------------------- /plugins/terraform/dev/fixtures/outputs.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "wsout-ofgzLGNPVS9R9fyk", 4 | "type": "state-version-outputs", 5 | "attributes": { 6 | "name": "hcp_consul_public_address", 7 | "sensitive": false, 8 | "type": "string", 9 | "value": "https://hound.consul.public.aws.hashicorp.cloud", 10 | "detailed-type": "string" 11 | }, 12 | "links": { 13 | "self": "/api/v2/state-version-outputs/wsout-ofgzLGNPVS9R9fyk" 14 | } 15 | }, 16 | { 17 | "id": "wsout-ofgzLGNPVS9R9fyk", 18 | "type": "state-version-outputs", 19 | "attributes": { 20 | "name": "hcp_consul_private_address", 21 | "sensitive": false, 22 | "type": "string", 23 | "value": "https://hound.consul.private.aws.hashicorp.cloud", 24 | "detailed-type": "string" 25 | }, 26 | "links": { 27 | "self": "/api/v2/state-version-outputs/wsout-ofgzLGNPVS9R9fyk" 28 | } 29 | }, 30 | { 31 | "id": "wsout-cUF1YUGaperXFGFQ", 32 | "type": "state-version-outputs", 33 | "attributes": { 34 | "name": "hcp_vault_token", 35 | "sensitive": true, 36 | "type": "string", 37 | "value": "example.token", 38 | "detailed-type": "string" 39 | }, 40 | "links": { 41 | "self": "/api/v2/state-version-outputs/wsout-cUF1YUGaperXFGFQ" 42 | } 43 | } 44 | ] 45 | -------------------------------------------------------------------------------- /plugins/terraform/dev/fixtures/runs.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "run-4KGb9N6MFk4ZMKB3", 4 | "attributes": { 5 | "created-at": "2021-08-25T21:15:10.368Z", 6 | "status": "planned" 7 | } 8 | }, 9 | { 10 | "id": "run-9wsAeCBCC8ZYuHq1", 11 | "attributes": { 12 | "created-at": "2021-08-25T21:11:17.364Z", 13 | "status": "applied" 14 | } 15 | }, 16 | { 17 | "id": "run-DqwnRbvww8c7D5kb", 18 | "attributes": { 19 | "created-at": "2021-08-25T20:31:29.862Z", 20 | "status": "canceled" 21 | } 22 | }, 23 | { 24 | "id": "run-bi6ade6zN3iRfWQy", 25 | "attributes": { 26 | "created-at": "2021-03-03T19:26:19.320Z", 27 | "status": "applied" 28 | } 29 | }, 30 | { 31 | "id": "run-mrg22zbwq4mDRNLX", 32 | "attributes": { 33 | "created-at": "2021-03-03T19:23:26.048Z", 34 | "status": "planned_and_finished" 35 | } 36 | }, 37 | { 38 | "id": "run-hzC4YMwBMd2PQrRu", 39 | "attributes": { 40 | "created-at": "2021-03-03T18:53:14.614Z", 41 | "status": "errored" 42 | } 43 | }, 44 | { 45 | "id": "run-yKDReA3DtTdsFFoo", 46 | "attributes": { 47 | "created-at": "2021-03-03T18:50:17.417Z", 48 | "status": "applied" 49 | } 50 | } 51 | ] 52 | -------------------------------------------------------------------------------- /plugins/terraform/dev/fixtures/workspaces.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "id": "ws-H8yvfX7iB2ni9wkk", 4 | "type": "workspaces", 5 | "attributes": { 6 | "allow-destroy-plan": true, 7 | "auto-apply": false, 8 | "auto-destroy-at": null, 9 | "created-at": "2021-01-20T14:27:07.413Z", 10 | "environment": "default", 11 | "locked": false, 12 | "name": "rosemary-terraform-aws-listener-rule-unit-tests", 13 | "queue-all-runs": false, 14 | "speculative-enabled": true, 15 | "structured-run-output-enabled": false, 16 | "terraform-version": "1.0.5", 17 | "working-directory": "test", 18 | "global-remote-state": true, 19 | "updated-at": "2021-08-25T21:15:55.867Z", 20 | "resource-count": 0, 21 | "apply-duration-average": 62000, 22 | "plan-duration-average": 20000, 23 | "policy-check-failures": 0, 24 | "run-failures": 7, 25 | "workspace-kpis-runs-count": 30, 26 | "latest-change-at": "2021-08-25T21:15:54.414Z", 27 | "operations": true, 28 | "execution-mode": "remote", 29 | "vcs-repo": null, 30 | "vcs-repo-identifier": null, 31 | "permissions": { 32 | "can-update": true, 33 | "can-destroy": true, 34 | "can-queue-run": true, 35 | "can-read-variable": true, 36 | "can-update-variable": true, 37 | "can-read-state-versions": true, 38 | "can-read-state-outputs": true, 39 | "can-create-state-versions": true, 40 | "can-queue-apply": true, 41 | "can-lock": true, 42 | "can-unlock": true, 43 | "can-force-unlock": true, 44 | "can-read-settings": true, 45 | "can-manage-tags": true, 46 | "can-manage-run-tasks": true, 47 | "can-force-delete": true, 48 | "can-manage-assessments": true, 49 | "can-read-assessment-results": true, 50 | "can-queue-destroy": true 51 | }, 52 | "actions": { 53 | "is-destroyable": true 54 | }, 55 | "description": "This workspace includes a perpetual Sentinel demonstration for customer reference. It runs unit tests against a Terraform module.", 56 | "file-triggers-enabled": true, 57 | "trigger-prefixes": [], 58 | "trigger-patterns": [], 59 | "assessments-enabled": false, 60 | "last-assessment-result-at": null, 61 | "source": "tfe-ui", 62 | "source-name": null, 63 | "source-url": null, 64 | "tag-names": ["nia"] 65 | }, 66 | "relationships": { 67 | "organization": { 68 | "data": { 69 | "id": "hashicorp-team-da-beta", 70 | "type": "organizations" 71 | } 72 | }, 73 | "current-run": { 74 | "data": { 75 | "id": "run-4KGb9N6MFk4ZMKB3", 76 | "type": "runs" 77 | }, 78 | "links": { 79 | "related": "/api/v2/runs/run-4KGb9N6MFk4ZMKB3" 80 | } 81 | }, 82 | "latest-run": { 83 | "data": { 84 | "id": "run-4KGb9N6MFk4ZMKB3", 85 | "type": "runs" 86 | }, 87 | "links": { 88 | "related": "/api/v2/runs/run-4KGb9N6MFk4ZMKB3" 89 | } 90 | }, 91 | "outputs": { 92 | "data": [], 93 | "links": { 94 | "related": "/api/v2/workspaces/ws-H8yvfX7iB2ni9wkk/current-state-version-outputs" 95 | } 96 | }, 97 | "remote-state-consumers": { 98 | "links": { 99 | "related": "/api/v2/workspaces/ws-H8yvfX7iB2ni9wkk/relationships/remote-state-consumers" 100 | } 101 | }, 102 | "current-state-version": { 103 | "data": { 104 | "id": "sv-PDK8MLHas3uueSCd", 105 | "type": "state-versions" 106 | }, 107 | "links": { 108 | "related": "/api/v2/workspaces/ws-H8yvfX7iB2ni9wkk/current-state-version" 109 | } 110 | }, 111 | "current-configuration-version": { 112 | "data": { 113 | "id": "cv-XwLmAVXDM5xnzAii", 114 | "type": "configuration-versions" 115 | }, 116 | "links": { 117 | "related": "/api/v2/configuration-versions/cv-XwLmAVXDM5xnzAii" 118 | } 119 | }, 120 | "agent-pool": { 121 | "data": null 122 | }, 123 | "readme": { 124 | "data": { 125 | "id": "1690", 126 | "type": "workspace-readme" 127 | } 128 | }, 129 | "project": { 130 | "data": { 131 | "id": "prj-M8nFRmD5QQBUmNJF", 132 | "type": "projects" 133 | } 134 | }, 135 | "current-assessment-result": { 136 | "data": null 137 | }, 138 | "vars": { 139 | "data": [ 140 | { 141 | "id": "var-eLdVp6Q8vNssGMRx", 142 | "type": "vars" 143 | }, 144 | { 145 | "id": "var-5k58oiEjW2LLaTeZ", 146 | "type": "vars" 147 | }, 148 | { 149 | "id": "var-yTt5QXtX3VdsU2N6", 150 | "type": "vars" 151 | } 152 | ] 153 | } 154 | }, 155 | "links": { 156 | "self": "/api/v2/organizations/hashicorp-team-da-beta/workspaces/rosemary-terraform-aws-listener-rule-unit-tests", 157 | "self-html": "/app/hashicorp-team-da-beta/workspaces/rosemary-terraform-aws-listener-rule-unit-tests" 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /plugins/terraform/dev/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | import React from 'react'; 7 | import { Entity } from '@backstage/catalog-model'; 8 | import { createDevApp } from '@backstage/dev-utils'; 9 | import { 10 | terraformPlugin, 11 | TerraformRunTable, 12 | TerraformOutputTable, 13 | } from '../src/plugin'; 14 | import { 15 | TERRAFORM_ORGANIZATION_ANNOTATION, 16 | TERRAFORM_WORKSPACE_ANNOTATION, 17 | } from '../src/annotations'; 18 | import { EntityProvider } from '@backstage/plugin-catalog-react'; 19 | import { TestApiProvider } from '@backstage/test-utils'; 20 | import { Grid } from '@material-ui/core'; 21 | import { 22 | Header, 23 | Page, 24 | Content, 25 | ContentHeader, 26 | HeaderLabel, 27 | SupportButton, 28 | } from '@backstage/core-components'; 29 | import workspaceJson from './fixtures/workspaces.json'; 30 | import runJson from './fixtures/runs.json'; 31 | import outputJson from './fixtures/outputs.json'; 32 | import { 33 | TerraformApi, 34 | TerraformOutput, 35 | TerraformRun, 36 | Workspace, 37 | } from '../src/api/types'; 38 | import { terraformApiRef } from '../src/api'; 39 | 40 | const entity: Entity = { 41 | apiVersion: 'backstage.io/v1alpha1', 42 | kind: 'Component', 43 | metadata: { 44 | name: 'backstage', 45 | annotations: { 46 | [TERRAFORM_ORGANIZATION_ANNOTATION]: 'test', 47 | [TERRAFORM_WORKSPACE_ANNOTATION]: 'test', 48 | }, 49 | }, 50 | spec: { 51 | type: 'service', 52 | }, 53 | }; 54 | 55 | const mockedApi: TerraformApi = { 56 | getWorkspace: async (): Promise => workspaceJson, 57 | listRuns: async (): Promise => runJson, 58 | getOutputs: async (): Promise => outputJson, 59 | }; 60 | 61 | createDevApp() 62 | .registerPlugin(terraformPlugin) 63 | .addPage({ 64 | element: ( 65 | 66 | 67 | 68 |
72 | 73 | 74 |
75 | 76 | 77 | 78 | Go to Terraform for additional debug information. 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 |
93 |
94 |
95 | ), 96 | title: 'Root Page', 97 | path: '/terraform', 98 | }) 99 | .render(); 100 | -------------------------------------------------------------------------------- /plugins/terraform/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@internal/plugin-terraform", 3 | "version": "0.1.0", 4 | "main": "src/index.ts", 5 | "types": "src/index.ts", 6 | "license": "Apache-2.0", 7 | "private": true, 8 | "publishConfig": { 9 | "access": "public", 10 | "main": "dist/index.esm.js", 11 | "types": "dist/index.d.ts" 12 | }, 13 | "backstage": { 14 | "role": "frontend-plugin" 15 | }, 16 | "scripts": { 17 | "start": "backstage-cli package start", 18 | "build": "backstage-cli package build", 19 | "lint": "backstage-cli package lint", 20 | "test": "backstage-cli package test", 21 | "clean": "backstage-cli package clean", 22 | "prepack": "backstage-cli package prepack", 23 | "postpack": "backstage-cli package postpack" 24 | }, 25 | "dependencies": { 26 | "@backstage/catalog-model": "^1.4.0", 27 | "@backstage/core-components": "^0.13.2", 28 | "@backstage/core-plugin-api": "^1.5.2", 29 | "@backstage/plugin-catalog-react": "^1.7.0", 30 | "@backstage/theme": "^0.4.0", 31 | "@material-ui/core": "^4.9.13", 32 | "@material-ui/icons": "^4.9.1", 33 | "@material-ui/lab": "4.0.0-alpha.61", 34 | "@mui/icons-material": "^5.11.16", 35 | "react-use": "^17.2.4" 36 | }, 37 | "peerDependencies": { 38 | "react": "^16.13.1 || ^17.0.0" 39 | }, 40 | "devDependencies": { 41 | "@backstage/catalog-model": "^1.4.0", 42 | "@backstage/cli": "^0.22.8", 43 | "@backstage/core-app-api": "^1.8.1", 44 | "@backstage/dev-utils": "^1.0.16", 45 | "@backstage/plugin-catalog-react": "^1.7.0", 46 | "@backstage/test-utils": "^1.4.0", 47 | "@testing-library/jest-dom": "^5.10.1", 48 | "@testing-library/react": "^12.1.3", 49 | "@testing-library/user-event": "^14.0.0", 50 | "@types/node": "*", 51 | "cross-fetch": "^3.1.5", 52 | "msw": "^1.0.0" 53 | }, 54 | "files": [ 55 | "dist" 56 | ] 57 | } 58 | -------------------------------------------------------------------------------- /plugins/terraform/src/annotations.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | import { Entity } from '@backstage/catalog-model'; 7 | 8 | export const TERRAFORM_ORGANIZATION_ANNOTATION = 'terraform.io/organization'; 9 | export const TERRAFORM_PROJECT_ANNOTATION = 'terraform.io/project'; 10 | export const TERRAFORM_WORKSPACE_ANNOTATION = 'terraform.io/workspace'; 11 | 12 | export const isTerraformAvailable = (entity: Entity) => 13 | Boolean( 14 | entity?.metadata.annotations?.hasOwnProperty( 15 | TERRAFORM_ORGANIZATION_ANNOTATION, 16 | ) || 17 | entity?.metadata.annotations?.hasOwnProperty( 18 | TERRAFORM_PROJECT_ANNOTATION, 19 | ) || 20 | entity?.metadata.annotations?.hasOwnProperty( 21 | TERRAFORM_WORKSPACE_ANNOTATION, 22 | ), 23 | ); 24 | -------------------------------------------------------------------------------- /plugins/terraform/src/api/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | import { 7 | ConfigApi, 8 | DiscoveryApi, 9 | createApiRef, 10 | } from '@backstage/core-plugin-api'; 11 | import { 12 | Resource, 13 | StateVersion, 14 | TerraformApi, 15 | TerraformOutput, 16 | TerraformRun, 17 | Workspace, 18 | } from './types'; 19 | 20 | const DEFAULT_PROXY_PATH = '/terraform/api'; 21 | const DEFAULT_TERRAFORM_URL = 'https://app.terraform.io'; 22 | 23 | /** 24 | * @public 25 | */ 26 | export const terraformApiRef = createApiRef({ 27 | id: 'plugin.terraform.service', 28 | }); 29 | 30 | type Options = { 31 | discoveryApi: DiscoveryApi; 32 | configApi: ConfigApi; 33 | }; 34 | 35 | export class TerraformClient implements TerraformApi { 36 | private readonly discoveryApi: DiscoveryApi; 37 | private readonly proxyPath: string; 38 | private readonly targetUrl: string; 39 | 40 | constructor(options: Options) { 41 | this.discoveryApi = options.discoveryApi; 42 | 43 | const proxyPath = options.configApi.getOptionalString( 44 | 'terraform.proxyPath', 45 | ); 46 | this.proxyPath = proxyPath ?? DEFAULT_PROXY_PATH; 47 | 48 | const targetUrl = 49 | options.configApi.getOptionalString('terraform.baseUrl') || 50 | DEFAULT_TERRAFORM_URL; 51 | this.targetUrl = targetUrl; 52 | } 53 | 54 | private async getUrls() { 55 | const proxyUrl = await this.discoveryApi.getBaseUrl('proxy'); 56 | return { 57 | apiUrl: `${proxyUrl}${this.proxyPath}/v2`, 58 | baseUrl: `${proxyUrl}${this.proxyPath}`, 59 | }; 60 | } 61 | 62 | async getTargetUrl() { 63 | return this.targetUrl; 64 | } 65 | 66 | async getWorkspace( 67 | organization: string, 68 | workspaceName: string, 69 | ): Promise { 70 | const { apiUrl } = await this.getUrls(); 71 | 72 | const response = await fetch( 73 | `${apiUrl}/organizations/${organization}/workspaces/${workspaceName}`, 74 | { 75 | method: 'GET', 76 | }, 77 | ); 78 | 79 | if (!response.ok) { 80 | throw new Error( 81 | `failed to fetch data, status ${response.status}: ${response.statusText}`, 82 | ); 83 | } 84 | 85 | return await response.json(); 86 | } 87 | 88 | async listRuns(workspaceID: string): Promise { 89 | const { apiUrl } = await this.getUrls(); 90 | 91 | const response = await fetch(`${apiUrl}/workspaces/${workspaceID}/runs`, { 92 | method: 'GET', 93 | }); 94 | 95 | if (!response.ok) { 96 | throw new Error( 97 | `failed to fetch data, status ${response.status}: ${response.statusText}`, 98 | ); 99 | } 100 | 101 | return response.json().then(runs => { 102 | return runs.data; 103 | }); 104 | } 105 | 106 | private async getCurrentStateVersion( 107 | workspaceID: string, 108 | ): Promise { 109 | const { apiUrl } = await this.getUrls(); 110 | 111 | const response = await fetch( 112 | `${apiUrl}/workspaces/${workspaceID}/current-state-version`, 113 | { 114 | method: 'GET', 115 | }, 116 | ); 117 | 118 | if (!response.ok) { 119 | throw new Error( 120 | `failed to fetch data, status ${response.status}: ${response.statusText}`, 121 | ); 122 | } 123 | 124 | return response.json(); 125 | } 126 | 127 | private async getStateVersionOutput( 128 | outputID: string, 129 | ): Promise { 130 | const { apiUrl } = await this.getUrls(); 131 | 132 | const response = await fetch( 133 | `${apiUrl}/state-version-outputs/${outputID}`, 134 | { 135 | method: 'GET', 136 | }, 137 | ); 138 | 139 | if (!response.ok) { 140 | throw new Error( 141 | `failed to fetch data, status ${response.status}: ${response.statusText}`, 142 | ); 143 | } 144 | 145 | return response.json().then(output => { 146 | return output.data; 147 | }); 148 | } 149 | 150 | async getResources(workspaceID: string): Promise { 151 | const stateVersion = this.getCurrentStateVersion(workspaceID); 152 | return (await stateVersion).data.attributes.resources; 153 | } 154 | 155 | async getOutputs(workspaceID: string): Promise { 156 | const stateVersion = this.getCurrentStateVersion(workspaceID); 157 | const stateVersionOutputs = (await stateVersion).data.relationships.outputs 158 | .data; 159 | 160 | return Promise.all( 161 | stateVersionOutputs.map(async output => { 162 | const out = await this.getStateVersionOutput(output.id); 163 | return out; 164 | }), 165 | ); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /plugins/terraform/src/api/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | export interface TerraformApi { 7 | getTargetUrl(): Promise; 8 | listRuns(workspaceId: string): Promise; 9 | getWorkspace(organization: string, workspace: string): Promise; 10 | getOutputs(workspaceID: string): Promise; 11 | getResources(workspaceID: string): Promise; 12 | } 13 | 14 | export interface Run { 15 | data: TerraformRun; 16 | } 17 | 18 | export interface Runs { 19 | data: TerraformRun[]; 20 | } 21 | 22 | export interface TerraformRun { 23 | id: string; 24 | attributes: { 25 | status: string; 26 | 'created-at': string; 27 | }; 28 | } 29 | 30 | export interface Workspace { 31 | data: { 32 | id: string; 33 | attributes: { 34 | name: string; 35 | }; 36 | relationships: { 37 | organization: { 38 | data: { 39 | id: string; 40 | }; 41 | }; 42 | project: { 43 | data: { 44 | id: string; 45 | }; 46 | }; 47 | }; 48 | links: { 49 | self: string; 50 | 'self-html': string; 51 | }; 52 | }; 53 | } 54 | 55 | export interface StateVersion { 56 | data: TerraformState; 57 | } 58 | 59 | export interface TerraformState { 60 | id: string; 61 | attributes: { 62 | resources: Resource[]; 63 | }; 64 | relationships: { 65 | outputs: { 66 | data: [ 67 | { 68 | id: string; 69 | }, 70 | ]; 71 | }; 72 | }; 73 | } 74 | 75 | export interface Resource { 76 | name: string; 77 | type: string; 78 | count: number; 79 | module: string; 80 | } 81 | 82 | export interface Output { 83 | data: TerraformOutput; 84 | } 85 | 86 | export interface TerraformOutput { 87 | id: string; 88 | attributes: { 89 | name: string; 90 | sensitive: boolean; 91 | type: string; 92 | value: string; 93 | }; 94 | } 95 | -------------------------------------------------------------------------------- /plugins/terraform/src/components/TerraformOutputFetchComponent/TerraformOutputFetchComponent.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | import React from 'react'; 7 | import Lock from '@material-ui/icons/Lock'; 8 | import { Entity } from '@backstage/catalog-model'; 9 | import { 10 | Table, 11 | TableColumn, 12 | Progress, 13 | ResponseErrorPanel, 14 | } from '@backstage/core-components'; 15 | import { useApi } from '@backstage/core-plugin-api'; 16 | import useAsync from 'react-use/lib/useAsync'; 17 | import { 18 | TERRAFORM_ORGANIZATION_ANNOTATION, 19 | TERRAFORM_WORKSPACE_ANNOTATION, 20 | } from '../../annotations'; 21 | import { TerraformOutput } from '../../api/types'; 22 | import { terraformApiRef } from '../../api'; 23 | 24 | export const terraformWorkspace = (entity: Entity) => { 25 | const organization = 26 | entity.metadata.annotations?.[TERRAFORM_ORGANIZATION_ANNOTATION]; 27 | const workspace = 28 | entity.metadata.annotations?.[TERRAFORM_WORKSPACE_ANNOTATION]; 29 | return { organization, workspace }; 30 | }; 31 | 32 | type BasicTableProps = { 33 | organization: string; 34 | workspace: string; 35 | outputs: TerraformOutput[]; 36 | }; 37 | 38 | export const BasicTable = (props: BasicTableProps) => { 39 | const columns: TableColumn[] = [ 40 | { title: 'Name', field: 'name' }, 41 | { 42 | title: 'Value', 43 | field: 'value', 44 | render: rowData => { 45 | return rowData.sensitive ? ( 46 | { 48 | navigator.clipboard.writeText(JSON.stringify(rowData.value)); 49 | }} 50 | /> 51 | ) : ( 52 | String(JSON.stringify(rowData.value)) 53 | ); 54 | }, 55 | }, 56 | ]; 57 | 58 | const data = props.outputs.map((run: TerraformOutput) => { 59 | return { 60 | name: run.attributes.name, 61 | value: run.attributes.value, 62 | type: run.attributes.type, 63 | sensitive: run.attributes.sensitive, 64 | }; 65 | }); 66 | 67 | return ( 68 | 74 | ); 75 | }; 76 | 77 | export const TerraformOutputFetchComponent = ({ 78 | entity, 79 | }: { 80 | entity: Entity; 81 | }) => { 82 | const terraformApi = useApi(terraformApiRef); 83 | const { organization, workspace } = terraformWorkspace(entity); 84 | 85 | if (!organization) { 86 | throw Error( 87 | `The Terraform organizaton is undefined. Please, define the annotation ${TERRAFORM_ORGANIZATION_ANNOTATION}`, 88 | ); 89 | } 90 | 91 | if (!workspace) { 92 | throw Error( 93 | `The Terraform workspace is undefined. Please, define the annotation ${TERRAFORM_WORKSPACE_ANNOTATION}`, 94 | ); 95 | } 96 | 97 | const { value, loading, error } = useAsync(async (): Promise< 98 | TerraformOutput[] 99 | > => { 100 | const tfWorkspace = await terraformApi.getWorkspace( 101 | organization, 102 | workspace, 103 | ); 104 | return terraformApi.getOutputs(tfWorkspace.data.id); 105 | }, []); 106 | 107 | if (loading) { 108 | return ; 109 | } else if (error) { 110 | return ; 111 | } 112 | 113 | return ( 114 | 119 | ); 120 | }; 121 | -------------------------------------------------------------------------------- /plugins/terraform/src/components/TerraformOutputFetchComponent/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | export { TerraformOutputFetchComponent } from './TerraformOutputFetchComponent'; 7 | -------------------------------------------------------------------------------- /plugins/terraform/src/components/TerraformOutputTable/TerraformOutputTable.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | import React from 'react'; 7 | import { useEntity } from '@backstage/plugin-catalog-react'; 8 | import { MissingAnnotationEmptyState } from '@backstage/core-components'; 9 | import { 10 | TERRAFORM_WORKSPACE_ANNOTATION, 11 | isTerraformAvailable, 12 | } from '../../annotations'; 13 | import { TerraformOutputFetchComponent } from '../TerraformOutputFetchComponent'; 14 | 15 | export const TerraformOutputTable = () => { 16 | const { entity } = useEntity(); 17 | return !isTerraformAvailable(entity) ? ( 18 | 19 | ) : ( 20 | 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /plugins/terraform/src/components/TerraformOutputTable/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | export { TerraformOutputTable } from './TerraformOutputTable'; 7 | -------------------------------------------------------------------------------- /plugins/terraform/src/components/TerraformResourceFetchComponent/TerraformResourceFetchComponent.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | import React from 'react'; 7 | import { Entity } from '@backstage/catalog-model'; 8 | import { 9 | Table, 10 | TableColumn, 11 | Progress, 12 | ResponseErrorPanel, 13 | } from '@backstage/core-components'; 14 | import { useApi } from '@backstage/core-plugin-api'; 15 | import useAsync from 'react-use/lib/useAsync'; 16 | import { 17 | TERRAFORM_ORGANIZATION_ANNOTATION, 18 | TERRAFORM_WORKSPACE_ANNOTATION, 19 | } from '../../annotations'; 20 | import { Resource } from '../../api/types'; 21 | import { terraformApiRef } from '../../api'; 22 | 23 | export const terraformWorkspace = (entity: Entity) => { 24 | const organization = 25 | entity.metadata.annotations?.[TERRAFORM_ORGANIZATION_ANNOTATION]; 26 | const workspace = 27 | entity.metadata.annotations?.[TERRAFORM_WORKSPACE_ANNOTATION]; 28 | return { organization, workspace }; 29 | }; 30 | 31 | type BasicTableProps = { 32 | organization: string; 33 | workspace: string; 34 | resources: Resource[]; 35 | }; 36 | 37 | export const BasicTable = (props: BasicTableProps) => { 38 | const columns: TableColumn[] = [ 39 | { title: 'Name', field: 'name' }, 40 | { title: 'Type', field: 'type' }, 41 | { title: 'Count', field: 'count' }, 42 | ]; 43 | 44 | const data = props.resources.map((run: Resource) => { 45 | return { 46 | name: run.name, 47 | type: run.type, 48 | count: run.count, 49 | }; 50 | }); 51 | 52 | return ( 53 |
59 | ); 60 | }; 61 | 62 | export const TerraformResourceFetchComponent = ({ 63 | entity, 64 | }: { 65 | entity: Entity; 66 | }) => { 67 | const terraformApi = useApi(terraformApiRef); 68 | const { organization, workspace } = terraformWorkspace(entity); 69 | 70 | if (!organization) { 71 | throw Error( 72 | `The Terraform organizaton is undefined. Please, define the annotation ${TERRAFORM_ORGANIZATION_ANNOTATION}`, 73 | ); 74 | } 75 | 76 | if (!workspace) { 77 | throw Error( 78 | `The Terraform workspace is undefined. Please, define the annotation ${TERRAFORM_WORKSPACE_ANNOTATION}`, 79 | ); 80 | } 81 | 82 | const { value, loading, error } = useAsync(async (): Promise => { 83 | const tfWorkspace = await terraformApi.getWorkspace( 84 | organization, 85 | workspace, 86 | ); 87 | return terraformApi.getResources(tfWorkspace.data.id); 88 | }, []); 89 | 90 | if (loading) { 91 | return ; 92 | } else if (error) { 93 | return ; 94 | } 95 | 96 | return ( 97 | 102 | ); 103 | }; 104 | -------------------------------------------------------------------------------- /plugins/terraform/src/components/TerraformResourceFetchComponent/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | export { TerraformResourceFetchComponent } from './TerraformResourceFetchComponent'; 7 | -------------------------------------------------------------------------------- /plugins/terraform/src/components/TerraformResourceTable/TerraformResourceTable.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | import React from 'react'; 7 | import { useEntity } from '@backstage/plugin-catalog-react'; 8 | import { MissingAnnotationEmptyState } from '@backstage/core-components'; 9 | import { 10 | TERRAFORM_WORKSPACE_ANNOTATION, 11 | isTerraformAvailable, 12 | } from '../../annotations'; 13 | import { TerraformResourceFetchComponent } from '../TerraformResourceFetchComponent'; 14 | 15 | export const TerraformResourceTable = () => { 16 | const { entity } = useEntity(); 17 | return !isTerraformAvailable(entity) ? ( 18 | 19 | ) : ( 20 | 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /plugins/terraform/src/components/TerraformResourceTable/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | export { TerraformResourceTable } from './TerraformResourceTable'; 7 | -------------------------------------------------------------------------------- /plugins/terraform/src/components/TerraformRunFetchComponent/TerraformRunFetchComponent.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | import React from 'react'; 7 | import { Entity } from '@backstage/catalog-model'; 8 | import { 9 | Table, 10 | TableColumn, 11 | Progress, 12 | ResponseErrorPanel, 13 | } from '@backstage/core-components'; 14 | import { useApi } from '@backstage/core-plugin-api'; 15 | import useAsync from 'react-use/lib/useAsync'; 16 | import { 17 | TERRAFORM_ORGANIZATION_ANNOTATION, 18 | TERRAFORM_WORKSPACE_ANNOTATION, 19 | } from '../../annotations'; 20 | import { TerraformRun, Workspace } from '../../api/types'; 21 | import { terraformApiRef } from '../../api'; 22 | import { Box, Chip, Link } from '@material-ui/core'; 23 | 24 | export const terraformWorkspace = (entity: Entity) => { 25 | const organization = 26 | entity.metadata.annotations?.[TERRAFORM_ORGANIZATION_ANNOTATION]; 27 | const workspace = 28 | entity.metadata.annotations?.[TERRAFORM_WORKSPACE_ANNOTATION]; 29 | return { organization, workspace }; 30 | }; 31 | 32 | type DenseTableProps = { 33 | organization: string; 34 | workspace: Workspace; 35 | targetUrl: string; 36 | runs: TerraformRun[]; 37 | }; 38 | 39 | export const DenseTable = (props: DenseTableProps) => { 40 | const colorMap: Record = { 41 | applied: '#60DEA9', 42 | planned: '#63D0FF', 43 | planned_and_finished: '#CEFCF2', 44 | errored: '#F24C53', 45 | }; 46 | 47 | const columns: TableColumn[] = [ 48 | { title: 'Run ID', field: 'id' }, 49 | { 50 | title: 'Status', 51 | field: 'status', 52 | render: rowData => { 53 | return ( 54 | 57 | {rowData.status} 58 | 59 | } 60 | variant="outlined" 61 | /> 62 | ); 63 | }, 64 | }, 65 | { title: 'Created At', field: 'createdAt' }, 66 | ]; 67 | 68 | const data = props.runs.map((run: TerraformRun) => { 69 | return { 70 | id: run.id, 71 | status: run.attributes.status, 72 | createdAt: run.attributes['created-at'], 73 | }; 74 | }); 75 | 76 | const url = `${props.targetUrl}${props.workspace.data.links['self-html']}`; 77 | 78 | return ( 79 |
Runs for {props.workspace.data.attributes.name} 82 | } 83 | options={{ search: true, paging: true }} 84 | columns={columns} 85 | data={data} 86 | /> 87 | ); 88 | }; 89 | 90 | export const TerraformRunFetchComponent = ({ entity }: { entity: Entity }) => { 91 | const terraformApi = useApi(terraformApiRef); 92 | const { organization, workspace } = terraformWorkspace(entity); 93 | 94 | if (!organization) { 95 | throw Error( 96 | `The Terraform organizaton is undefined. Please, define the annotation ${TERRAFORM_ORGANIZATION_ANNOTATION}`, 97 | ); 98 | } 99 | 100 | if (!workspace) { 101 | throw Error( 102 | `The Terraform workspace is undefined. Please, define the annotation ${TERRAFORM_WORKSPACE_ANNOTATION}`, 103 | ); 104 | } 105 | 106 | const { value, loading, error } = useAsync(async (): Promise<{ 107 | workspace: Workspace; 108 | runs: TerraformRun[]; 109 | targetUrl: string; 110 | }> => { 111 | const w = await terraformApi.getWorkspace(organization, workspace); 112 | const r = await terraformApi.listRuns(w.data.id); 113 | const t = await terraformApi.getTargetUrl(); 114 | return { workspace: w, runs: r, targetUrl: t }; 115 | }, []); 116 | 117 | if (loading) { 118 | return ; 119 | } else if (error) { 120 | return ; 121 | } 122 | 123 | const emptyWorkspace = { 124 | data: { 125 | id: '', 126 | relationships: { 127 | organization: { data: { id: '' } }, 128 | project: { data: { id: '' } }, 129 | }, 130 | attributes: { name: workspace }, 131 | links: { self: '', 'self-html': '' }, 132 | }, 133 | }; 134 | 135 | return ( 136 | 142 | ); 143 | }; 144 | -------------------------------------------------------------------------------- /plugins/terraform/src/components/TerraformRunFetchComponent/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | export { TerraformRunFetchComponent } from './TerraformRunFetchComponent'; 7 | -------------------------------------------------------------------------------- /plugins/terraform/src/components/TerraformRunTable/TerraformRunTable.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | import React from 'react'; 7 | import { useEntity } from '@backstage/plugin-catalog-react'; 8 | import { MissingAnnotationEmptyState } from '@backstage/core-components'; 9 | import { TerraformRunFetchComponent } from '../TerraformRunFetchComponent'; 10 | import { 11 | TERRAFORM_WORKSPACE_ANNOTATION, 12 | isTerraformAvailable, 13 | } from '../../annotations'; 14 | 15 | export const TerraformRunTable = () => { 16 | const { entity } = useEntity(); 17 | return !isTerraformAvailable(entity) ? ( 18 | 19 | ) : ( 20 | 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /plugins/terraform/src/components/TerraformRunTable/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | export { TerraformRunTable } from './TerraformRunTable'; 7 | -------------------------------------------------------------------------------- /plugins/terraform/src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | export { 7 | terraformPlugin, 8 | TerraformRunTable, 9 | TerraformOutputTable, 10 | TerraformResourceTable, 11 | } from './plugin'; 12 | -------------------------------------------------------------------------------- /plugins/terraform/src/plugin.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | import { terraformPlugin } from './plugin'; 7 | 8 | describe('terraform', () => { 9 | it('should export plugin', () => { 10 | expect(terraformPlugin).toBeDefined(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /plugins/terraform/src/plugin.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | import { 7 | configApiRef, 8 | createApiFactory, 9 | createComponentExtension, 10 | createPlugin, 11 | discoveryApiRef, 12 | } from '@backstage/core-plugin-api'; 13 | 14 | import { rootRouteRef } from './routes'; 15 | import { TerraformClient, terraformApiRef } from './api'; 16 | 17 | export const terraformPlugin = createPlugin({ 18 | id: 'terraform', 19 | routes: { 20 | root: rootRouteRef, 21 | }, 22 | apis: [ 23 | createApiFactory({ 24 | api: terraformApiRef, 25 | deps: { 26 | discoveryApi: discoveryApiRef, 27 | configApi: configApiRef, 28 | }, 29 | factory: ({ discoveryApi, configApi }) => { 30 | return new TerraformClient({ 31 | discoveryApi, 32 | configApi, 33 | }); 34 | }, 35 | }), 36 | ], 37 | }); 38 | 39 | export const TerraformRunTable = terraformPlugin.provide( 40 | createComponentExtension({ 41 | name: 'EntityTerraformRuns', 42 | component: { 43 | lazy: () => 44 | import('./components/TerraformRunTable').then(m => m.TerraformRunTable), 45 | }, 46 | }), 47 | ); 48 | 49 | export const TerraformOutputTable = terraformPlugin.provide( 50 | createComponentExtension({ 51 | name: 'EntityTerraformOutputs', 52 | component: { 53 | lazy: () => 54 | import('./components/TerraformOutputTable').then( 55 | m => m.TerraformOutputTable, 56 | ), 57 | }, 58 | }), 59 | ); 60 | 61 | export const TerraformResourceTable = terraformPlugin.provide( 62 | createComponentExtension({ 63 | name: 'EntityTerraformResources', 64 | component: { 65 | lazy: () => 66 | import('./components/TerraformResourceTable').then( 67 | m => m.TerraformResourceTable, 68 | ), 69 | }, 70 | }), 71 | ); 72 | -------------------------------------------------------------------------------- /plugins/terraform/src/routes.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | import { createRouteRef } from '@backstage/core-plugin-api'; 7 | 8 | export const rootRouteRef = createRouteRef({ 9 | id: 'terraform', 10 | }); 11 | -------------------------------------------------------------------------------- /plugins/terraform/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) HashiCorp, Inc. 3 | * SPDX-License-Identifier: MPL-2.0 4 | */ 5 | 6 | import '@testing-library/jest-dom'; 7 | import 'cross-fetch/polyfill'; 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@backstage/cli/config/tsconfig.json", 3 | "include": [ 4 | "packages/*/src", 5 | "plugins/*/src", 6 | "plugins/*/dev", 7 | "plugins/*/migrations", 8 | "plugins/terraform/config.d.ts" 9 | ], 10 | "exclude": ["node_modules"], 11 | "compilerOptions": { 12 | "outDir": "dist-types", 13 | "rootDir": "." 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /vault/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.3" 2 | services: 3 | 4 | vault: 5 | image: vault:1.13.2 6 | restart: always 7 | command: [ 'vault', 'server', '-dev', '-dev-listen-address=0.0.0.0:8200'] 8 | environment: 9 | VAULT_DEV_ROOT_TOKEN_ID: "some-root-token" 10 | ports: 11 | - "8200:8200" 12 | cap_add: 13 | - IPC_LOCK -------------------------------------------------------------------------------- /vault/policy.hcl: -------------------------------------------------------------------------------- 1 | path "terraform/creds/backstage" { 2 | capabilities = [ "read" ] 3 | } 4 | -------------------------------------------------------------------------------- /vault/setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | export VAULT_ADDR="http://127.0.0.1:8200" 6 | export VAULT_TOKEN="some-root-token" 7 | 8 | 9 | ## Enable GitHub Auth Method 10 | vault auth enable github 11 | vault write auth/github/config organization=${VAULT_GITHUB_ORG} organization_id=${VAULT_GITHUB_ORG_ID} 12 | vault policy write backstage vault/policy.hcl 13 | vault write auth/github/map/users/${VAULT_GITHUB_USER} value=backstage 14 | 15 | ## Enable Terraform Cloud secrets engine 16 | vault secrets enable terraform 17 | vault write terraform/config token=${TERRAFORM_CLOUD_ORGANIZATION_TOKEN} 18 | vault write terraform/role/backstage team_id=${TERRAFORM_CLOUD_TEAM_ID} 19 | vault read terraform/creds/backstage --------------------------------------------------------------------------------