├── .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 |
--------------------------------------------------------------------------------