├── .gitignore ├── LICENSE ├── README.md ├── go.mod ├── go.sum ├── main.go └── tfmerge ├── testdata ├── module_conflict │ ├── state1 │ └── state2 ├── module_conflict_same_id │ ├── expect │ ├── state1 │ └── state2 ├── module_cross │ ├── expect │ ├── state1 │ └── state2 ├── module_instance │ ├── expect │ ├── state1 │ └── state2 ├── module_no_cross │ ├── expect │ ├── state1 │ └── state2 ├── resource_conflict │ ├── state1 │ └── state2 ├── resource_conflict_same_id │ ├── expect │ ├── state1 │ └── state2 └── resource_only │ ├── expect │ ├── state1 │ └── state2 ├── tfmerge.go └── tfmerge_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tfmerge 2 | 3 | A tool to merge multiple Terrafrom state files into one. 4 | 5 | ## Usage 6 | 7 | Given you have an initialized Terraform working directory (abbr. *wd*) (create one and run `terraform init` in it if not existed yet). The state file in *wd* will be used as the *base state file* (i.e. the `lineage` will be reserved, and the `serial` will be incremented). Meanwhile, you have other three state files to be merged: `state1`, `state2`, `state3`, where the module and resource addresses among these state files together with the *base state file* have no overlaps. 8 | 9 | `tfmerge` helps you merging these state files into the *base state file* by simply running `tfmerge -o terraform.tfstate state1 state2 state3` within the *wd*. 10 | 11 | If your *wd* is using [a non-local backend](https://www.terraform.io/language/settings/backends/configuration), you'll need to manually upload the merged state file via `terraform state push`. 12 | 13 | ## How 14 | 15 | *The process is inspired by https://support.hashicorp.com/hc/en-us/articles/4418624552339-How-to-Merge-State-Files* 16 | 17 | `tfmerge` will simply do followings: 18 | 19 | - Run `terraform state pull` to retrieve the *base state file*, works for both local and non-local backends. Especially, the output can be an empty string if there is no state file in the working directory, in this case a new state file will be created with a new lineage. 20 | - Run `terraform state list` on the *base state file* and the to-be-merged state files, to list all the items to be moved. Meanwhile, ensure there is no resource/module address overlap. 21 | - Copy all the state files to a temporary directory, to avoid mutation on existing state files. 22 | - Repeatedly run `terraform state mv -state-out= -state= ` 23 | - Return the merged base state file 24 | 25 | ## Reference 26 | 27 | - https://discuss.hashicorp.com/t/will-state-and-state-out-be-removed-for-terraform-state-mv-subcommand/44630 28 | - https://support.hashicorp.com/hc/en-us/articles/4418624552339-How-to-Merge-State-Files 29 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/magodo/tfmerge 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/hashicorp/go-multierror v1.1.1 7 | github.com/hashicorp/go-version v1.6.0 8 | github.com/hashicorp/hc-install v0.4.0 9 | github.com/hashicorp/terraform-exec v0.17.2 10 | github.com/stretchr/testify v1.8.0 11 | github.com/urfave/cli/v2 v2.11.2 12 | ) 13 | 14 | require ( 15 | github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect 16 | github.com/davecgh/go-spew v1.1.1 // indirect 17 | github.com/hashicorp/errwrap v1.0.0 // indirect 18 | github.com/hashicorp/terraform-json v0.14.0 // indirect 19 | github.com/pmezard/go-difflib v1.0.0 // indirect 20 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 21 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect 22 | github.com/zclconf/go-cty v1.10.0 // indirect 23 | golang.org/x/text v0.3.7 // indirect 24 | gopkg.in/yaml.v3 v3.0.1 // indirect 25 | ) 26 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= 2 | github.com/Microsoft/go-winio v0.4.16 h1:FtSW/jqD+l4ba5iPBj9CODVtgfYAD8w2wS923g/cFDk= 3 | github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= 4 | github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7 h1:YoJbenK9C67SkzkDfmQuVln04ygHj3vjZfd9FL+GmQQ= 5 | github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo= 6 | github.com/acomagu/bufpipe v1.0.3 h1:fxAGrHZTgQ9w5QqVItgzwj235/uYZYgbXitB+dLupOk= 7 | github.com/acomagu/bufpipe v1.0.3/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4= 8 | github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= 9 | github.com/apparentlymart/go-textseg v1.0.0/go.mod h1:z96Txxhf3xSFMPmb5X/1W05FF/Nj9VFpLOpjS5yuumk= 10 | github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= 11 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= 12 | github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= 13 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 14 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 15 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 16 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 17 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 18 | github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg= 19 | github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= 20 | github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= 21 | github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= 22 | github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4= 23 | github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E= 24 | github.com/go-git/go-billy/v5 v5.2.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0= 25 | github.com/go-git/go-billy/v5 v5.3.1 h1:CPiOUAzKtMRvolEKw+bG1PLRpT7D3LIs3/3ey4Aiu34= 26 | github.com/go-git/go-billy/v5 v5.3.1/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0= 27 | github.com/go-git/go-git-fixtures/v4 v4.2.1/go.mod h1:K8zd3kDUAykwTdDCr+I0per6Y6vMiRR/nnVTBtavnB0= 28 | github.com/go-git/go-git/v5 v5.4.2 h1:BXyZu9t0VkbiHtqrsvdq39UDhGJTl1h55VW6CSC4aY4= 29 | github.com/go-git/go-git/v5 v5.4.2/go.mod h1:gQ1kArt6d+n+BGd+/B/I74HwRTLhth2+zti4ihgckDc= 30 | github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 31 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 32 | github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 33 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 34 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 35 | github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= 36 | github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 37 | github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= 38 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 39 | github.com/hashicorp/go-checkpoint v0.5.0/go.mod h1:7nfLNL10NsxqO4iWuW6tWW0HjZuDrwkBuEQsVcpCOgg= 40 | github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= 41 | github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= 42 | github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= 43 | github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= 44 | github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= 45 | github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 46 | github.com/hashicorp/go-version v1.5.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= 47 | github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= 48 | github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= 49 | github.com/hashicorp/hc-install v0.4.0 h1:cZkRFr1WVa0Ty6x5fTvL1TuO1flul231rWkGH92oYYk= 50 | github.com/hashicorp/hc-install v0.4.0/go.mod h1:5d155H8EC5ewegao9A4PUTMNPZaq+TbOzkJJZ4vrXeI= 51 | github.com/hashicorp/terraform-exec v0.17.2 h1:EU7i3Fh7vDUI9nNRdMATCEfnm9axzTnad8zszYZ73Go= 52 | github.com/hashicorp/terraform-exec v0.17.2/go.mod h1:tuIbsL2l4MlwwIZx9HPM+LOV9vVyEfBYu2GsO1uH3/8= 53 | github.com/hashicorp/terraform-json v0.14.0 h1:sh9iZ1Y8IFJLx+xQiKHGud6/TSUCM0N8e17dKDpqV7s= 54 | github.com/hashicorp/terraform-json v0.14.0/go.mod h1:5A9HIWPkk4e5aeeXIBbkcOvaZbIYnAIkEyqP2pNSckM= 55 | github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= 56 | github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= 57 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= 58 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= 59 | github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= 60 | github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 h1:DowS9hvgyYSX4TO5NpyC606/Z4SxnNYbT+WX27or6Ck= 61 | github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= 62 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 63 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 64 | github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= 65 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 66 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 67 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 68 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 69 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 70 | github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= 71 | github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA= 72 | github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= 73 | github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= 74 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 75 | github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 76 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 77 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 78 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 79 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 80 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 81 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 82 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 83 | github.com/sebdah/goldie v1.0.0/go.mod h1:jXP4hmWywNEwZzhMuv2ccnqTSFpuq8iyQhtQdkkZBH4= 84 | github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= 85 | github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= 86 | github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= 87 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 88 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 89 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 90 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 91 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 92 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 93 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 94 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 95 | github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= 96 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 97 | github.com/urfave/cli/v2 v2.11.2 h1:FVfNg4m3vbjbBpLYxW//WjxUoHvJ9TlppXcqY9Q9ZfA= 98 | github.com/urfave/cli/v2 v2.11.2/go.mod h1:f8iq5LtQ/bLxafbdBSLPPNsgaW0l/2fYYEHhAyPlwvo= 99 | github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= 100 | github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4= 101 | github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= 102 | github.com/xanzy/ssh-agent v0.3.0 h1:wUMzuKtKilRgBAD1sUb8gOwwRr2FGoBVumcjoOACClI= 103 | github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6eAcqrDh0= 104 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= 105 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= 106 | github.com/zclconf/go-cty v1.2.0/go.mod h1:hOPWgoHbaTUnI5k4D2ld+GRpFJSCe6bCM7m1q/N4PQ8= 107 | github.com/zclconf/go-cty v1.10.0 h1:mp9ZXQeIcN8kAwuqorjH+Q+njbJKjLrvB2yIh4q7U+0= 108 | github.com/zclconf/go-cty v1.10.0/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk= 109 | github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b/go.mod h1:ZRKQfBXbGkpdV6QMzT3rU1kSTAnfu1dO8dPKjYprgj8= 110 | golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 111 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 112 | golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= 113 | golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= 114 | golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e h1:gsTQYXdTw2Gq7RBsWvlQ91b+aEQ6bXFUngBGuR8sPpI= 115 | golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 116 | golang.org/x/net v0.0.0-20180811021610-c39426892332/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 117 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 118 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 119 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 120 | golang.org/x/net v0.0.0-20210326060303-6b1517762897 h1:KrsHThm5nFk34YtATK1LsThyGhGbGe1olrte/HInHvs= 121 | golang.org/x/net v0.0.0-20210326060303-6b1517762897/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k= 122 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 123 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 124 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 125 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 126 | golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 127 | golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 128 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 129 | golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 130 | golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 131 | golang.org/x/sys v0.0.0-20210502180810-71e4cd670f79/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 132 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4= 133 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 134 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 135 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 136 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 137 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 138 | golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 139 | golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= 140 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 141 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 142 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 143 | google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 144 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 145 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 146 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 147 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 148 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 149 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 150 | gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= 151 | gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= 152 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 153 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 154 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 155 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 156 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 157 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 158 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "log" 8 | "os" 9 | 10 | "github.com/hashicorp/go-version" 11 | install "github.com/hashicorp/hc-install" 12 | "github.com/hashicorp/hc-install/fs" 13 | "github.com/hashicorp/hc-install/product" 14 | "github.com/hashicorp/hc-install/src" 15 | "github.com/hashicorp/terraform-exec/tfexec" 16 | "github.com/magodo/tfmerge/tfmerge" 17 | "github.com/urfave/cli/v2" 18 | ) 19 | 20 | func main() { 21 | app := &cli.App{ 22 | Name: "tfmerge", 23 | Usage: `Merge Terraform state files into the state file of the current working directory`, 24 | UsageText: "tfmerge [option] statefile ...", 25 | Flags: []cli.Flag{ 26 | &cli.StringFlag{ 27 | Name: "output", 28 | EnvVars: []string{"TFMERGE_OUTPUT"}, 29 | Aliases: []string{"o"}, 30 | Usage: "The output merged state file name", 31 | }, 32 | &cli.BoolFlag{ 33 | Name: "debug", 34 | EnvVars: []string{"TFMERGE_DEBUG"}, 35 | Aliases: []string{"d"}, 36 | Usage: "Show debug log", 37 | }, 38 | &cli.StringFlag{ 39 | Name: "chdir", 40 | EnvVars: []string{"TFMERGE_CHDIR"}, 41 | Usage: "Switch to a different working directory before executing", 42 | }, 43 | }, 44 | Action: func(ctx *cli.Context) error { 45 | log.SetOutput(io.Discard) 46 | if ctx.Bool("debug") { 47 | log.SetPrefix("[tfmerge] ") 48 | log.SetOutput(os.Stderr) 49 | } 50 | cwd, err := os.Getwd() 51 | if err != nil { 52 | return err 53 | } 54 | 55 | if v := ctx.String("chdir"); v != "" { 56 | cwd = v 57 | } 58 | 59 | tf, err := initTerraform(context.Background(), cwd) 60 | if err != nil { 61 | return err 62 | } 63 | 64 | baseState, err := tf.StatePull(ctx.Context) 65 | if err != nil { 66 | return fmt.Errorf("pulling state file of the working directory: %v", err) 67 | } 68 | 69 | b, err := tfmerge.Merge(ctx.Context, tf, []byte(baseState), ctx.Args().Slice()...) 70 | if err != nil { 71 | return err 72 | } 73 | 74 | if v := ctx.String("output"); v != "" { 75 | return os.WriteFile(v, b, 0644) 76 | } 77 | fmt.Println(string(b)) 78 | return nil 79 | }, 80 | } 81 | 82 | if err := app.Run(os.Args); err != nil { 83 | fmt.Fprintln(os.Stderr, err) 84 | os.Exit(1) 85 | } 86 | } 87 | 88 | func initTerraform(ctx context.Context, tfwd string) (*tfexec.Terraform, error) { 89 | i := install.NewInstaller() 90 | tfpath, err := i.Ensure(ctx, []src.Source{ 91 | &fs.Version{ 92 | Product: product.Terraform, 93 | // `terraform stat mv` is introducd since v1.1.0: https://github.com/hashicorp/terraform/releases/tag/v1.1.0 94 | Constraints: version.MustConstraints(version.NewConstraint(">=1.1.0")), 95 | }, 96 | }) 97 | if err != nil { 98 | return nil, fmt.Errorf("finding a terraform executable: %v", err) 99 | } 100 | 101 | tf, err := tfexec.NewTerraform(tfwd, tfpath) 102 | if err != nil { 103 | return nil, fmt.Errorf("error running NewTerraform: %w", err) 104 | } 105 | if v, ok := os.LookupEnv("TF_LOG_PATH"); ok { 106 | tf.SetLogPath(v) 107 | } 108 | if v, ok := os.LookupEnv("TF_LOG"); ok { 109 | tf.SetLog(v) 110 | } 111 | return tf, nil 112 | } 113 | -------------------------------------------------------------------------------- /tfmerge/testdata/module_conflict/state1: -------------------------------------------------------------------------------- 1 | { 2 | "version": 4, 3 | "terraform_version": "1.2.8", 4 | "serial": 1, 5 | "lineage": "97673df8-a96c-1ebb-e82b-fa1a6281a979", 6 | "outputs": {}, 7 | "resources": [ 8 | { 9 | "module": "module.mod1", 10 | "mode": "managed", 11 | "type": "null_resource", 12 | "name": "test", 13 | "provider": "provider[\"registry.terraform.io/hashicorp/null\"]", 14 | "instances": [ 15 | { 16 | "schema_version": 0, 17 | "attributes": { 18 | "id": "4256987146005369787", 19 | "triggers": null 20 | }, 21 | "sensitive_attributes": [], 22 | "private": "bnVsbA==" 23 | } 24 | ] 25 | } 26 | ], 27 | "check_results": null 28 | } 29 | -------------------------------------------------------------------------------- /tfmerge/testdata/module_conflict/state2: -------------------------------------------------------------------------------- 1 | { 2 | "version": 4, 3 | "terraform_version": "1.2.8", 4 | "serial": 1, 5 | "lineage": "a43c2c4e-d361-9815-d1d7-3f8a1589b50a", 6 | "outputs": {}, 7 | "resources": [ 8 | { 9 | "module": "module.mod1", 10 | "mode": "managed", 11 | "type": "null_resource", 12 | "name": "test", 13 | "provider": "provider[\"registry.terraform.io/hashicorp/null\"]", 14 | "instances": [ 15 | { 16 | "schema_version": 0, 17 | "attributes": { 18 | "id": "6013074630852056609", 19 | "triggers": null 20 | }, 21 | "sensitive_attributes": [], 22 | "private": "bnVsbA==" 23 | } 24 | ] 25 | } 26 | ], 27 | "check_results": null 28 | } 29 | -------------------------------------------------------------------------------- /tfmerge/testdata/module_conflict_same_id/expect: -------------------------------------------------------------------------------- 1 | { 2 | "version": 4, 3 | "terraform_version": "1.2.8", 4 | "serial": 1, 5 | "lineage": "97673df8-a96c-1ebb-e82b-fa1a6281a979", 6 | "outputs": {}, 7 | "resources": [ 8 | { 9 | "module": "module.mod1", 10 | "mode": "managed", 11 | "type": "null_resource", 12 | "name": "test", 13 | "provider": "provider[\"registry.terraform.io/hashicorp/null\"]", 14 | "instances": [ 15 | { 16 | "schema_version": 0, 17 | "attributes": { 18 | "id": "4256987146005369787", 19 | "triggers": null 20 | }, 21 | "sensitive_attributes": [], 22 | "private": "bnVsbA==" 23 | } 24 | ] 25 | }, 26 | { 27 | "module": "module.mod1", 28 | "mode": "managed", 29 | "type": "null_resource", 30 | "name": "test2", 31 | "provider": "provider[\"registry.terraform.io/hashicorp/null\"]", 32 | "instances": [ 33 | { 34 | "schema_version": 0, 35 | "attributes": { 36 | "id": "aaaa", 37 | "triggers": null 38 | }, 39 | "sensitive_attributes": [], 40 | "private": "bnVsbA==" 41 | } 42 | ] 43 | } 44 | ], 45 | "check_results": null 46 | } 47 | -------------------------------------------------------------------------------- /tfmerge/testdata/module_conflict_same_id/state1: -------------------------------------------------------------------------------- 1 | { 2 | "version": 4, 3 | "terraform_version": "1.2.8", 4 | "serial": 1, 5 | "lineage": "97673df8-a96c-1ebb-e82b-fa1a6281a979", 6 | "outputs": {}, 7 | "resources": [ 8 | { 9 | "module": "module.mod1", 10 | "mode": "managed", 11 | "type": "null_resource", 12 | "name": "test", 13 | "provider": "provider[\"registry.terraform.io/hashicorp/null\"]", 14 | "instances": [ 15 | { 16 | "schema_version": 0, 17 | "attributes": { 18 | "id": "4256987146005369787", 19 | "triggers": null 20 | }, 21 | "sensitive_attributes": [], 22 | "private": "bnVsbA==" 23 | } 24 | ] 25 | }, 26 | { 27 | "module": "module.mod1", 28 | "mode": "managed", 29 | "type": "null_resource", 30 | "name": "test2", 31 | "provider": "provider[\"registry.terraform.io/hashicorp/null\"]", 32 | "instances": [ 33 | { 34 | "schema_version": 0, 35 | "attributes": { 36 | "id": "aaaa", 37 | "triggers": null 38 | }, 39 | "sensitive_attributes": [], 40 | "private": "bnVsbA==" 41 | } 42 | ] 43 | } 44 | ], 45 | "check_results": null 46 | } 47 | -------------------------------------------------------------------------------- /tfmerge/testdata/module_conflict_same_id/state2: -------------------------------------------------------------------------------- 1 | { 2 | "version": 4, 3 | "terraform_version": "1.2.8", 4 | "serial": 1, 5 | "lineage": "a43c2c4e-d361-9815-d1d7-3f8a1589b50a", 6 | "outputs": {}, 7 | "resources": [ 8 | { 9 | "module": "module.mod1", 10 | "mode": "managed", 11 | "type": "null_resource", 12 | "name": "test", 13 | "provider": "provider[\"registry.terraform.io/hashicorp/null\"]", 14 | "instances": [ 15 | { 16 | "schema_version": 0, 17 | "attributes": { 18 | "id": "4256987146005369787", 19 | "triggers": null 20 | }, 21 | "sensitive_attributes": [], 22 | "private": "bnVsbA==" 23 | } 24 | ] 25 | } 26 | ], 27 | "check_results": null 28 | } 29 | -------------------------------------------------------------------------------- /tfmerge/testdata/module_cross/expect: -------------------------------------------------------------------------------- 1 | { 2 | "version": 4, 3 | "terraform_version": "1.2.8", 4 | "serial": 0, 5 | "lineage": "00000000-0000-0000-0000-000000000000", 6 | "outputs": {}, 7 | "resources": [ 8 | { 9 | "module": "module.mod1", 10 | "mode": "managed", 11 | "type": "null_resource", 12 | "name": "test", 13 | "provider": "provider[\"registry.terraform.io/hashicorp/null\"]", 14 | "instances": [ 15 | { 16 | "schema_version": 0, 17 | "attributes": { 18 | "id": "4256987146005369787", 19 | "triggers": null 20 | }, 21 | "sensitive_attributes": [], 22 | "private": "bnVsbA==" 23 | } 24 | ] 25 | }, 26 | { 27 | "module": "module.mod1", 28 | "mode": "managed", 29 | "type": "null_resource", 30 | "name": "test2", 31 | "provider": "provider[\"registry.terraform.io/hashicorp/null\"]", 32 | "instances": [ 33 | { 34 | "schema_version": 0, 35 | "attributes": { 36 | "id": "6013074630852056609", 37 | "triggers": null 38 | }, 39 | "sensitive_attributes": [], 40 | "private": "bnVsbA==" 41 | } 42 | ] 43 | } 44 | ], 45 | "check_results": null 46 | } 47 | -------------------------------------------------------------------------------- /tfmerge/testdata/module_cross/state1: -------------------------------------------------------------------------------- 1 | { 2 | "version": 4, 3 | "terraform_version": "1.2.8", 4 | "serial": 1, 5 | "lineage": "97673df8-a96c-1ebb-e82b-fa1a6281a979", 6 | "outputs": {}, 7 | "resources": [ 8 | { 9 | "module": "module.mod1", 10 | "mode": "managed", 11 | "type": "null_resource", 12 | "name": "test", 13 | "provider": "provider[\"registry.terraform.io/hashicorp/null\"]", 14 | "instances": [ 15 | { 16 | "schema_version": 0, 17 | "attributes": { 18 | "id": "4256987146005369787", 19 | "triggers": null 20 | }, 21 | "sensitive_attributes": [], 22 | "private": "bnVsbA==" 23 | } 24 | ] 25 | } 26 | ], 27 | "check_results": null 28 | } 29 | -------------------------------------------------------------------------------- /tfmerge/testdata/module_cross/state2: -------------------------------------------------------------------------------- 1 | { 2 | "version": 4, 3 | "terraform_version": "1.2.8", 4 | "serial": 1, 5 | "lineage": "a43c2c4e-d361-9815-d1d7-3f8a1589b50a", 6 | "outputs": {}, 7 | "resources": [ 8 | { 9 | "module": "module.mod1", 10 | "mode": "managed", 11 | "type": "null_resource", 12 | "name": "test2", 13 | "provider": "provider[\"registry.terraform.io/hashicorp/null\"]", 14 | "instances": [ 15 | { 16 | "schema_version": 0, 17 | "attributes": { 18 | "id": "6013074630852056609", 19 | "triggers": null 20 | }, 21 | "sensitive_attributes": [], 22 | "private": "bnVsbA==" 23 | } 24 | ] 25 | } 26 | ], 27 | "check_results": null 28 | } 29 | -------------------------------------------------------------------------------- /tfmerge/testdata/module_instance/expect: -------------------------------------------------------------------------------- 1 | { 2 | "version": 4, 3 | "terraform_version": "1.3.6", 4 | "serial": 3, 5 | "lineage": "00000000-0000-0000-0000-000000000000", 6 | "outputs": {}, 7 | "resources": [ 8 | { 9 | "module": "module.mod1[0]", 10 | "mode": "managed", 11 | "type": "null_resource", 12 | "name": "test", 13 | "provider": "provider[\"registry.terraform.io/hashicorp/null\"]", 14 | "instances": [ 15 | { 16 | "schema_version": 0, 17 | "attributes": { 18 | "id": "5839070286178946060", 19 | "triggers": null 20 | }, 21 | "sensitive_attributes": [] 22 | } 23 | ] 24 | }, 25 | { 26 | "module": "module.mod1[1]", 27 | "mode": "managed", 28 | "type": "null_resource", 29 | "name": "test", 30 | "provider": "provider[\"registry.terraform.io/hashicorp/null\"]", 31 | "instances": [ 32 | { 33 | "schema_version": 0, 34 | "attributes": { 35 | "id": "769310592933334532", 36 | "triggers": null 37 | }, 38 | "sensitive_attributes": [] 39 | } 40 | ] 41 | } 42 | ], 43 | "check_results": null 44 | } 45 | -------------------------------------------------------------------------------- /tfmerge/testdata/module_instance/state1: -------------------------------------------------------------------------------- 1 | { 2 | "version": 4, 3 | "terraform_version": "1.3.6", 4 | "serial": 3, 5 | "lineage": "823291d5-315a-be8c-3294-2a65641cfbe0", 6 | "outputs": {}, 7 | "resources": [ 8 | { 9 | "module": "module.mod1[0]", 10 | "mode": "managed", 11 | "type": "null_resource", 12 | "name": "test", 13 | "provider": "provider[\"registry.terraform.io/hashicorp/null\"]", 14 | "instances": [ 15 | { 16 | "schema_version": 0, 17 | "attributes": { 18 | "id": "5839070286178946060", 19 | "triggers": null 20 | }, 21 | "sensitive_attributes": [] 22 | } 23 | ] 24 | } 25 | ], 26 | "check_results": null 27 | } 28 | -------------------------------------------------------------------------------- /tfmerge/testdata/module_instance/state2: -------------------------------------------------------------------------------- 1 | { 2 | "version": 4, 3 | "terraform_version": "1.3.6", 4 | "serial": 3, 5 | "lineage": "823291d5-315a-be8c-3294-2a65641cfbe0", 6 | "outputs": {}, 7 | "resources": [ 8 | { 9 | "module": "module.mod1[1]", 10 | "mode": "managed", 11 | "type": "null_resource", 12 | "name": "test", 13 | "provider": "provider[\"registry.terraform.io/hashicorp/null\"]", 14 | "instances": [ 15 | { 16 | "schema_version": 0, 17 | "attributes": { 18 | "id": "769310592933334532", 19 | "triggers": null 20 | }, 21 | "sensitive_attributes": [] 22 | } 23 | ] 24 | } 25 | ], 26 | "check_results": null 27 | } 28 | -------------------------------------------------------------------------------- /tfmerge/testdata/module_no_cross/expect: -------------------------------------------------------------------------------- 1 | { 2 | "version": 4, 3 | "terraform_version": "1.2.8", 4 | "serial": 0, 5 | "lineage": "00000000-0000-0000-0000-000000000000", 6 | "outputs": {}, 7 | "resources": [ 8 | { 9 | "module": "module.mod1", 10 | "mode": "managed", 11 | "type": "null_resource", 12 | "name": "test", 13 | "provider": "provider[\"registry.terraform.io/hashicorp/null\"]", 14 | "instances": [ 15 | { 16 | "schema_version": 0, 17 | "attributes": { 18 | "id": "4256987146005369787", 19 | "triggers": null 20 | }, 21 | "sensitive_attributes": [], 22 | "private": "bnVsbA==" 23 | } 24 | ] 25 | }, 26 | { 27 | "module": "module.mod2", 28 | "mode": "managed", 29 | "type": "null_resource", 30 | "name": "test", 31 | "provider": "provider[\"registry.terraform.io/hashicorp/null\"]", 32 | "instances": [ 33 | { 34 | "schema_version": 0, 35 | "attributes": { 36 | "id": "6013074630852056609", 37 | "triggers": null 38 | }, 39 | "sensitive_attributes": [], 40 | "private": "bnVsbA==" 41 | } 42 | ] 43 | } 44 | ], 45 | "check_results": null 46 | } 47 | -------------------------------------------------------------------------------- /tfmerge/testdata/module_no_cross/state1: -------------------------------------------------------------------------------- 1 | { 2 | "version": 4, 3 | "terraform_version": "1.2.8", 4 | "serial": 1, 5 | "lineage": "97673df8-a96c-1ebb-e82b-fa1a6281a979", 6 | "outputs": {}, 7 | "resources": [ 8 | { 9 | "module": "module.mod1", 10 | "mode": "managed", 11 | "type": "null_resource", 12 | "name": "test", 13 | "provider": "provider[\"registry.terraform.io/hashicorp/null\"]", 14 | "instances": [ 15 | { 16 | "schema_version": 0, 17 | "attributes": { 18 | "id": "4256987146005369787", 19 | "triggers": null 20 | }, 21 | "sensitive_attributes": [], 22 | "private": "bnVsbA==" 23 | } 24 | ] 25 | } 26 | ], 27 | "check_results": null 28 | } 29 | -------------------------------------------------------------------------------- /tfmerge/testdata/module_no_cross/state2: -------------------------------------------------------------------------------- 1 | { 2 | "version": 4, 3 | "terraform_version": "1.2.8", 4 | "serial": 1, 5 | "lineage": "a43c2c4e-d361-9815-d1d7-3f8a1589b50a", 6 | "outputs": {}, 7 | "resources": [ 8 | { 9 | "module": "module.mod2", 10 | "mode": "managed", 11 | "type": "null_resource", 12 | "name": "test", 13 | "provider": "provider[\"registry.terraform.io/hashicorp/null\"]", 14 | "instances": [ 15 | { 16 | "schema_version": 0, 17 | "attributes": { 18 | "id": "6013074630852056609", 19 | "triggers": null 20 | }, 21 | "sensitive_attributes": [], 22 | "private": "bnVsbA==" 23 | } 24 | ] 25 | } 26 | ], 27 | "check_results": null 28 | } 29 | -------------------------------------------------------------------------------- /tfmerge/testdata/resource_conflict/state1: -------------------------------------------------------------------------------- 1 | { 2 | "version": 4, 3 | "terraform_version": "1.2.7", 4 | "serial": 1, 5 | "lineage": "01cbbc6d-7e75-ebce-ff78-f5d9c03501ab", 6 | "outputs": {}, 7 | "resources": [ 8 | { 9 | "mode": "managed", 10 | "type": "null_resource", 11 | "name": "test1", 12 | "provider": "provider[\"registry.terraform.io/hashicorp/null\"]", 13 | "instances": [ 14 | { 15 | "schema_version": 0, 16 | "attributes": {}, 17 | "sensitive_attributes": [], 18 | "private": "bnVsbA==" 19 | } 20 | ] 21 | } 22 | ], 23 | "check_results": null 24 | } 25 | -------------------------------------------------------------------------------- /tfmerge/testdata/resource_conflict/state2: -------------------------------------------------------------------------------- 1 | { 2 | "version": 4, 3 | "terraform_version": "1.2.7", 4 | "serial": 1, 5 | "lineage": "76bfda81-f1f9-70c6-d7d4-df7d8c23de5a", 6 | "outputs": {}, 7 | "resources": [ 8 | { 9 | "mode": "managed", 10 | "type": "null_resource", 11 | "name": "test1", 12 | "provider": "provider[\"registry.terraform.io/hashicorp/null\"]", 13 | "instances": [ 14 | { 15 | "schema_version": 0, 16 | "attributes": {}, 17 | "sensitive_attributes": [], 18 | "private": "bnVsbA==" 19 | } 20 | ] 21 | } 22 | ], 23 | "check_results": null 24 | } 25 | -------------------------------------------------------------------------------- /tfmerge/testdata/resource_conflict_same_id/expect: -------------------------------------------------------------------------------- 1 | { 2 | "version": 4, 3 | "terraform_version": "1.2.7", 4 | "serial": 1, 5 | "lineage": "01cbbc6d-7e75-ebce-ff78-f5d9c03501ab", 6 | "outputs": {}, 7 | "resources": [ 8 | { 9 | "mode": "managed", 10 | "type": "null_resource", 11 | "name": "test1", 12 | "provider": "provider[\"registry.terraform.io/hashicorp/null\"]", 13 | "instances": [ 14 | { 15 | "schema_version": 0, 16 | "attributes": { 17 | "id": "5839070286178946060" 18 | }, 19 | "sensitive_attributes": [], 20 | "private": "bnVsbA==" 21 | } 22 | ] 23 | }, 24 | { 25 | "mode": "managed", 26 | "type": "null_resource", 27 | "name": "test2", 28 | "provider": "provider[\"registry.terraform.io/hashicorp/null\"]", 29 | "instances": [ 30 | { 31 | "schema_version": 0, 32 | "attributes": { 33 | "id": "aaaaa" 34 | }, 35 | "sensitive_attributes": [], 36 | "private": "bnVsbA==" 37 | } 38 | ] 39 | } 40 | ], 41 | "check_results": null 42 | } 43 | -------------------------------------------------------------------------------- /tfmerge/testdata/resource_conflict_same_id/state1: -------------------------------------------------------------------------------- 1 | { 2 | "version": 4, 3 | "terraform_version": "1.2.7", 4 | "serial": 1, 5 | "lineage": "01cbbc6d-7e75-ebce-ff78-f5d9c03501ab", 6 | "outputs": {}, 7 | "resources": [ 8 | { 9 | "mode": "managed", 10 | "type": "null_resource", 11 | "name": "test1", 12 | "provider": "provider[\"registry.terraform.io/hashicorp/null\"]", 13 | "instances": [ 14 | { 15 | "schema_version": 0, 16 | "attributes": { 17 | "id": "5839070286178946060" 18 | }, 19 | "sensitive_attributes": [], 20 | "private": "bnVsbA==" 21 | } 22 | ] 23 | }, 24 | { 25 | "mode": "managed", 26 | "type": "null_resource", 27 | "name": "test2", 28 | "provider": "provider[\"registry.terraform.io/hashicorp/null\"]", 29 | "instances": [ 30 | { 31 | "schema_version": 0, 32 | "attributes": { 33 | "id": "aaaaa" 34 | }, 35 | "sensitive_attributes": [], 36 | "private": "bnVsbA==" 37 | } 38 | ] 39 | } 40 | ], 41 | "check_results": null 42 | } 43 | -------------------------------------------------------------------------------- /tfmerge/testdata/resource_conflict_same_id/state2: -------------------------------------------------------------------------------- 1 | { 2 | "version": 4, 3 | "terraform_version": "1.2.7", 4 | "serial": 1, 5 | "lineage": "01cbbc6d-7e75-ebce-ff78-f5d9c03501ab", 6 | "outputs": {}, 7 | "resources": [ 8 | { 9 | "mode": "managed", 10 | "type": "null_resource", 11 | "name": "test1", 12 | "provider": "provider[\"registry.terraform.io/hashicorp/null\"]", 13 | "instances": [ 14 | { 15 | "schema_version": 0, 16 | "attributes": { 17 | "id": "5839070286178946060" 18 | }, 19 | "sensitive_attributes": [], 20 | "private": "bnVsbA==" 21 | } 22 | ] 23 | } 24 | ], 25 | "check_results": null 26 | } 27 | -------------------------------------------------------------------------------- /tfmerge/testdata/resource_only/expect: -------------------------------------------------------------------------------- 1 | { 2 | "version": 4, 3 | "terraform_version": "1.2.7", 4 | "serial": 0, 5 | "lineage": "00000000-0000-0000-0000-000000000000", 6 | "outputs": {}, 7 | "resources": [ 8 | { 9 | "mode": "managed", 10 | "type": "null_resource", 11 | "name": "test1", 12 | "provider": "provider[\"registry.terraform.io/hashicorp/null\"]", 13 | "instances": [ 14 | { 15 | "schema_version": 0, 16 | "attributes": {}, 17 | "sensitive_attributes": [], 18 | "private": "bnVsbA==" 19 | } 20 | ] 21 | }, 22 | { 23 | "mode": "managed", 24 | "type": "null_resource", 25 | "name": "test2", 26 | "provider": "provider[\"registry.terraform.io/hashicorp/null\"]", 27 | "instances": [ 28 | { 29 | "schema_version": 0, 30 | "attributes": {}, 31 | "sensitive_attributes": [], 32 | "private": "bnVsbA==" 33 | } 34 | ] 35 | } 36 | ], 37 | "check_results": null 38 | } 39 | -------------------------------------------------------------------------------- /tfmerge/testdata/resource_only/state1: -------------------------------------------------------------------------------- 1 | { 2 | "version": 4, 3 | "terraform_version": "1.2.7", 4 | "serial": 1, 5 | "lineage": "01cbbc6d-7e75-ebce-ff78-f5d9c03501ab", 6 | "outputs": {}, 7 | "resources": [ 8 | { 9 | "mode": "managed", 10 | "type": "null_resource", 11 | "name": "test1", 12 | "provider": "provider[\"registry.terraform.io/hashicorp/null\"]", 13 | "instances": [ 14 | { 15 | "schema_version": 0, 16 | "attributes": {}, 17 | "sensitive_attributes": [], 18 | "private": "bnVsbA==" 19 | } 20 | ] 21 | } 22 | ], 23 | "check_results": null 24 | } 25 | -------------------------------------------------------------------------------- /tfmerge/testdata/resource_only/state2: -------------------------------------------------------------------------------- 1 | { 2 | "version": 4, 3 | "terraform_version": "1.2.7", 4 | "serial": 1, 5 | "lineage": "76bfda81-f1f9-70c6-d7d4-df7d8c23de5a", 6 | "outputs": {}, 7 | "resources": [ 8 | { 9 | "mode": "managed", 10 | "type": "null_resource", 11 | "name": "test2", 12 | "provider": "provider[\"registry.terraform.io/hashicorp/null\"]", 13 | "instances": [ 14 | { 15 | "schema_version": 0, 16 | "attributes": {}, 17 | "sensitive_attributes": [], 18 | "private": "bnVsbA==" 19 | } 20 | ] 21 | } 22 | ], 23 | "check_results": null 24 | } 25 | -------------------------------------------------------------------------------- /tfmerge/tfmerge.go: -------------------------------------------------------------------------------- 1 | package tfmerge 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | 9 | "github.com/hashicorp/go-multierror" 10 | "github.com/hashicorp/terraform-exec/tfexec" 11 | tfjson "github.com/hashicorp/terraform-json" 12 | ) 13 | 14 | // Merge merges the state files to the base state. 15 | // 16 | // In case there is resource address conflict, if the conflict resources have the same "id", they are regarded as the same, and only one of them is kept. 17 | // Otherwise, it will error. 18 | // 19 | // baseState can be nil to indicate no base state file. 20 | func Merge(ctx context.Context, tf *tfexec.Terraform, baseState []byte, stateFiles ...string) ([]byte, error) { 21 | if baseState == nil { 22 | baseState = []byte{} 23 | } 24 | 25 | absStateFiles := []string{} 26 | for _, stateFile := range stateFiles { 27 | absPath, err := filepath.Abs(stateFile) 28 | if err != nil { 29 | return nil, err 30 | } 31 | absStateFiles = append(absStateFiles, absPath) 32 | } 33 | stateFiles = absStateFiles 34 | 35 | // Create an empty directory to hold the state files' copies and the merged state file 36 | tmpdir, err := os.MkdirTemp("", "") 37 | if err != nil { 38 | return nil, fmt.Errorf("creating an empty directory as the terraform working directroy: %v", err) 39 | } 40 | defer os.RemoveAll(tmpdir) 41 | 42 | baseStateFile := filepath.Join(tmpdir, "terraform.tfstate") 43 | if err := os.WriteFile(baseStateFile, baseState, 0644); err != nil { 44 | return nil, fmt.Errorf("creating the base state file: %v", err) 45 | } 46 | 47 | var result *multierror.Error 48 | 49 | type resourceInfo struct { 50 | stateFile string 51 | id string 52 | } 53 | 54 | resmap := map[string]resourceInfo{} 55 | 56 | // If there is no state file in the current working directory, "terraform state pull" returns an empty string. 57 | // In this case, we don't append it into the state file list for listing move items. 58 | stl := stateFiles[:] 59 | if len(baseState) != 0 { 60 | stl = append(stl, baseStateFile) 61 | } 62 | 63 | var checkConflict func(stateFile string, module *tfjson.StateModule) 64 | checkConflict = func(stateFile string, module *tfjson.StateModule) { 65 | if module == nil { 66 | return 67 | } 68 | for _, res := range module.Resources { 69 | // Ensure there is no resource address overlaps across all the state files 70 | if oResInfo, ok := resmap[res.Address]; ok { 71 | // Further check if the resource id are the same, in which case we regard they are the same resource and skip it 72 | if oResInfo.id != "" && oResInfo.id == getResourceId(res) { 73 | continue 74 | } 75 | result = multierror.Append(result, fmt.Errorf(`resource %s is defined in both state files %s and %s`, res.Address, stateFile, oResInfo.stateFile)) 76 | continue 77 | } 78 | 79 | resmap[res.Address] = resourceInfo{ 80 | stateFile: stateFile, 81 | id: getResourceId(res), 82 | } 83 | } 84 | for _, mod := range module.ChildModules { 85 | checkConflict(stateFile, mod) 86 | } 87 | } 88 | 89 | for _, stateFile := range stl { 90 | state, err := tf.ShowStateFile(ctx, stateFile) 91 | if err != nil { 92 | result = multierror.Append(result, fmt.Errorf("showing state file %s: %v", stateFile, err)) 93 | continue 94 | } 95 | if state.Values == nil { 96 | continue 97 | } 98 | checkConflict(stateFile, state.Values.RootModule) 99 | 100 | } 101 | if err := result.ErrorOrNil(); err != nil { 102 | return nil, err 103 | } 104 | 105 | // key: state file name; value: resource address 106 | stateItems := map[string][]string{} 107 | for k, v := range resmap { 108 | stateItems[v.stateFile] = append(stateItems[v.stateFile], k) 109 | } 110 | 111 | // Remove the items that belongs to the base state file 112 | delete(stateItems, baseStateFile) 113 | 114 | for stateFile, items := range stateItems { 115 | if err := move(ctx, tf, tmpdir, stateFile, baseStateFile, items); err != nil { 116 | return nil, fmt.Errorf("terraform state move from %s: %v", stateFile, err) 117 | } 118 | } 119 | 120 | b, err := os.ReadFile(baseStateFile) 121 | if err != nil { 122 | return nil, fmt.Errorf("reading from merged state file %s: %v", baseStateFile, err) 123 | } 124 | return b, nil 125 | } 126 | 127 | func getResourceId(res *tfjson.StateResource) string { 128 | var id string 129 | if idRaw, ok := res.AttributeValues["id"]; ok { 130 | if idStr, ok := idRaw.(string); ok { 131 | id = idStr 132 | } 133 | } 134 | return id 135 | } 136 | 137 | func copyFile(src, dst string) error { 138 | b, err := os.ReadFile(src) 139 | if err != nil { 140 | return fmt.Errorf("reading from %s: %v", src, err) 141 | } 142 | 143 | if err := os.WriteFile(dst, b, 0644); err != nil { 144 | return fmt.Errorf("writing to %s: %v", dst, err) 145 | } 146 | 147 | return nil 148 | } 149 | 150 | func move(ctx context.Context, tf *tfexec.Terraform, tmpdir, src, dst string, items []string) error { 151 | // Copy the state file to another one, to avoid `terraform state mv` mutating the original state file. 152 | f, err := os.CreateTemp(tmpdir, "") 153 | if err != nil { 154 | return fmt.Errorf("creating a temp state file for %s: %v", src, err) 155 | } 156 | f.Close() 157 | srcTmp := f.Name() 158 | if err := copyFile(src, srcTmp); err != nil { 159 | return fmt.Errorf("copying the source state file: %v", err) 160 | } 161 | 162 | for _, item := range items { 163 | if err := tf.StateMv(ctx, item, item, tfexec.State(srcTmp), tfexec.StateOut(dst)); err != nil { 164 | return fmt.Errorf(`terraform state move for %s`, item) 165 | } 166 | } 167 | return nil 168 | } 169 | -------------------------------------------------------------------------------- /tfmerge/tfmerge_test.go: -------------------------------------------------------------------------------- 1 | package tfmerge 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "io" 7 | "log" 8 | "os" 9 | "path/filepath" 10 | "testing" 11 | 12 | "github.com/hashicorp/go-version" 13 | install "github.com/hashicorp/hc-install" 14 | "github.com/hashicorp/hc-install/fs" 15 | "github.com/hashicorp/hc-install/product" 16 | "github.com/hashicorp/hc-install/src" 17 | "github.com/hashicorp/terraform-exec/tfexec" 18 | "github.com/stretchr/testify/require" 19 | ) 20 | 21 | func initTest(ctx context.Context, t *testing.T) *tfexec.Terraform { 22 | // Discard log output 23 | log.SetOutput(io.Discard) 24 | 25 | // Init terraform with null provider 26 | dir := t.TempDir() 27 | i := install.NewInstaller() 28 | tfpath, err := i.Ensure(ctx, []src.Source{ 29 | &fs.Version{ 30 | Product: product.Terraform, 31 | Constraints: version.MustConstraints(version.NewConstraint(">=1.1.0")), 32 | }, 33 | }) 34 | if err != nil { 35 | t.Fatalf("finding a terraform executable: %v", err) 36 | } 37 | tf, err := tfexec.NewTerraform(dir, tfpath) 38 | if err != nil { 39 | t.Fatalf("error running NewTerraform: %v", err) 40 | } 41 | if err := os.WriteFile(filepath.Join(dir, "terraform.tf"), []byte(`terraform { 42 | required_providers { 43 | null = { 44 | source = "hashicorp/null" 45 | } 46 | } 47 | } 48 | `), 0644); err != nil { 49 | t.Fatal(err) 50 | } 51 | 52 | if err := tf.Init(ctx); err != nil { 53 | t.Fatal(err) 54 | } 55 | 56 | return tf 57 | } 58 | 59 | func testFixture(t *testing.T, name string) (stateFiles []string, expectState []byte) { 60 | dir := filepath.Join("./testdata", name) 61 | entries, err := os.ReadDir(dir) 62 | if err != nil { 63 | t.Fatalf("reading dir entries: %v", err) 64 | } 65 | for _, entry := range entries { 66 | path := filepath.Join(dir, entry.Name()) 67 | if entry.Name() == "expect" { 68 | b, err := os.ReadFile(path) 69 | if err != nil { 70 | t.Fatalf("reading file %s: %v", path, err) 71 | } 72 | expectState = b 73 | continue 74 | } 75 | stateFiles = append(stateFiles, path) 76 | } 77 | return 78 | } 79 | 80 | func assertStateEqual(t *testing.T, actual, expect []byte, mergedCount int, hasBaseState bool) { 81 | var actualState, expectState map[string]interface{} 82 | if err := json.Unmarshal(actual, &actualState); err != nil { 83 | t.Fatalf("unmarshal actual state\n%s\n: %v", string(actual), err) 84 | } 85 | if err := json.Unmarshal(expect, &expectState); err != nil { 86 | t.Fatalf("unmarshal expect state\n%s\n: %v", string(expect), err) 87 | } 88 | 89 | if !hasBaseState { 90 | delete(actualState, "lineage") 91 | delete(expectState, "lineage") 92 | } 93 | if hasBaseState { 94 | mergedCount += 1 95 | } 96 | expectState["serial"] = mergedCount 97 | 98 | // The terraform version used to create the testdata might be different than the one running this test. 99 | delete(actualState, "terraform_version") 100 | delete(expectState, "terraform_version") 101 | 102 | actualJson, err := json.Marshal(actualState) 103 | if err != nil { 104 | t.Fatalf("marshal modified actual state: %v", err) 105 | } 106 | expectJson, err := json.Marshal(expectState) 107 | if err != nil { 108 | t.Fatalf("marshal modified expect state: %v", err) 109 | } 110 | require.JSONEq(t, string(expectJson), string(actualJson)) 111 | } 112 | 113 | func TestMerge(t *testing.T) { 114 | cases := []struct { 115 | name string 116 | dir string 117 | baseState string 118 | hasError bool 119 | }{ 120 | { 121 | name: "Resource Only (no base state)", 122 | dir: "resource_only", 123 | }, 124 | { 125 | name: "Resource Only (base state)", 126 | dir: "resource_only", 127 | baseState: `{ 128 | "version": 4, 129 | "terraform_version": "1.2.8", 130 | "serial": 1, 131 | "lineage": "00000000-0000-0000-0000-000000000000", 132 | "outputs": {}, 133 | "resources": [] 134 | } 135 | `, 136 | }, 137 | { 138 | name: "Module no cross (no base state)", 139 | dir: "module_no_cross", 140 | }, 141 | { 142 | name: "Module no cross (base state)", 143 | dir: "module_no_cross", 144 | baseState: `{ 145 | "version": 4, 146 | "terraform_version": "1.2.8", 147 | "serial": 1, 148 | "lineage": "00000000-0000-0000-0000-000000000000", 149 | "outputs": {}, 150 | "resources": [] 151 | } 152 | `, 153 | }, 154 | { 155 | name: "Module cross (no base state)", 156 | dir: "module_cross", 157 | }, 158 | { 159 | name: "Module cross (base state)", 160 | dir: "module_cross", 161 | baseState: `{ 162 | "version": 4, 163 | "terraform_version": "1.2.8", 164 | "serial": 1, 165 | "lineage": "00000000-0000-0000-0000-000000000000", 166 | "outputs": {}, 167 | "resources": [] 168 | } 169 | `, 170 | }, 171 | { 172 | name: "Module instance", 173 | dir: "module_instance", 174 | }, 175 | { 176 | name: "Resource conflict", 177 | dir: "resource_conflict", 178 | hasError: true, 179 | }, 180 | { 181 | name: "Resource conflict are the same resource", 182 | dir: "resource_conflict_same_id", 183 | }, 184 | { 185 | name: "Resource conflict with base state", 186 | dir: "resource_only", 187 | baseState: `{ 188 | "version": 4, 189 | "terraform_version": "1.2.8", 190 | "serial": 1, 191 | "lineage": "00000000-0000-0000-0000-000000000000", 192 | "outputs": {}, 193 | "resources": [ 194 | { 195 | "mode": "managed", 196 | "type": "null_resource", 197 | "name": "test1", 198 | "provider": "provider[\"registry.terraform.io/hashicorp/null\"]", 199 | "instances": [ 200 | { 201 | "schema_version": 0, 202 | "attributes": {}, 203 | "sensitive_attributes": [], 204 | "private": "bnVsbA==" 205 | } 206 | ] 207 | } 208 | ] 209 | } 210 | `, 211 | hasError: true, 212 | }, 213 | { 214 | name: "Module conflict", 215 | dir: "module_conflict", 216 | hasError: true, 217 | }, 218 | { 219 | name: "Module conflict are the same resource", 220 | dir: "module_conflict_same_id", 221 | }, 222 | } 223 | 224 | for _, tt := range cases { 225 | tt := tt 226 | t.Run(tt.name, func(t *testing.T) { 227 | t.Parallel() 228 | ctx := context.Background() 229 | tf := initTest(ctx, t) 230 | stateFiles, expect := testFixture(t, tt.dir) 231 | actual, err := Merge(context.Background(), tf, []byte(tt.baseState), stateFiles...) 232 | if tt.hasError { 233 | require.Error(t, err) 234 | return 235 | } 236 | require.NoError(t, err) 237 | assertStateEqual(t, actual, expect, len(stateFiles), tt.baseState != "") 238 | }) 239 | } 240 | } 241 | --------------------------------------------------------------------------------