├── .gitignore ├── LICENSE ├── README.md └── tfstate-merge /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | /.config 4 | /coverage/ 5 | /InstalledFiles 6 | /pkg/ 7 | /spec/reports/ 8 | /spec/examples.txt 9 | /test/tmp/ 10 | /test/version_tmp/ 11 | /tmp/ 12 | 13 | # Used by dotenv library to load environment variables. 14 | # .env 15 | 16 | # Ignore Byebug command history file. 17 | .byebug_history 18 | 19 | ## Specific to RubyMotion: 20 | .dat* 21 | .repl_history 22 | build/ 23 | *.bridgesupport 24 | build-iPhoneOS/ 25 | build-iPhoneSimulator/ 26 | 27 | ## Specific to RubyMotion (use of CocoaPods): 28 | # 29 | # We recommend against adding the Pods directory to your .gitignore. However 30 | # you should judge for yourself, the pros and cons are mentioned at: 31 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 32 | # 33 | # vendor/Pods/ 34 | 35 | ## Documentation cache and generated files: 36 | /.yardoc/ 37 | /_yardoc/ 38 | /doc/ 39 | /rdoc/ 40 | 41 | ## Environment normalization: 42 | /.bundle/ 43 | /vendor/bundle 44 | /lib/bundler/man/ 45 | 46 | # for a library or gem, you might want to ignore these files since the code is 47 | # intended to run in multiple environments; otherwise, check them in: 48 | # Gemfile.lock 49 | # .ruby-version 50 | # .ruby-gemset 51 | 52 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 53 | .rvmrc 54 | 55 | # Used by RuboCop. Remote config files pulled in from inherit_from directive. 56 | # .rubocop-https?--* 57 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 FUJIWARA Shunichiro 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tfstate-merge 2 | 3 | A tool to merge tfstate files. 4 | 5 | ## Usage 6 | 7 | ```console 8 | $ tfstate-merge [-i.bak] dest.tfstate src.tfstate 9 | ``` 10 | 11 | tfstate-merge adds resources in src.tfstate into dest.tfstate. 12 | 13 | ## Requirements 14 | 15 | Ruby 16 | 17 | ## Limitations 18 | 19 | - Src and dest tfstate files must have same "version" and "terraform_version". 20 | - Resource addresses must be unique in each state files. 21 | - Except for data resources which have same attributes. 22 | 23 | ## LICENSE 24 | 25 | MIT 26 | -------------------------------------------------------------------------------- /tfstate-merge: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'json' 4 | require 'optparse' 5 | require 'fileutils' 6 | 7 | def main 8 | options = {} 9 | opt = OptionParser.new 10 | opt.on('-i[ext]') {|v| options[:inplace] = v } 11 | opt.parse!(ARGV) 12 | 13 | dest_file = ARGV[0] 14 | src_file = ARGV[1] 15 | dest = load_file(dest_file) 16 | src = load_file(src_file) 17 | 18 | state = merge_tfstates(dest, src) 19 | 20 | if options.has_key?(:inplace) 21 | if ext = options[:inplace] 22 | FileUtils.mv(dest_file, dest_file+ext) 23 | end 24 | File.open(dest_file, mode = 'w') do |file| 25 | file.write(JSON.pretty_generate(state)) 26 | end 27 | else 28 | puts JSON.pretty_generate(state) 29 | end 30 | end 31 | 32 | def load_file(name) 33 | state = {} 34 | File.open(name) do |file| 35 | state = JSON.load(file) 36 | end 37 | state 38 | end 39 | 40 | def address(resource) 41 | [resource['module'] || '', resource['mode'], resource['type'], resource['name']].join('.') 42 | end 43 | 44 | def merge_tfstates(dest, src) 45 | if dest['version'] != 4 46 | raise "Unsupported tfstate version %d", dest['version'] 47 | end 48 | if dest['version'] != src['version'] 49 | raise "tfstate version mismatch #{dest['version']} != #{src['version']}" 50 | end 51 | if dest['terraform_version'] != src['terraform_version'] 52 | raise "terraform_version mismatch #{dest['terraform_version']} != #{src['terraform_version']}" 53 | end 54 | 55 | dest_resources = {} 56 | dest['resources'].each do |r| 57 | dest_resources[address(r)] = r 58 | end 59 | src['resources'].each do |sr| 60 | addr = address(sr) 61 | dr = dest_resources[addr] 62 | if dr != nil 63 | if sr['mode'] == 'data' 64 | if sr == dr 65 | warn "resource #{addr} is duplicated" 66 | next # skip to add same data resource 67 | else 68 | raise "resources #{addr} are different" if sr != dr 69 | end 70 | else 71 | raise "resources #{addr} are conflict" 72 | end 73 | end 74 | dest['resources'].push(sr) 75 | end 76 | 77 | dest['serial'] += 1 78 | dest 79 | end 80 | 81 | main 82 | --------------------------------------------------------------------------------