paramsync 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,217 @@
1
+ # This software is public domain. No rights are reserved. See LICENSE for more information.
2
+
3
+ class Paramsync
4
+ class Diff
5
+ def initialize(target:, local:, remote:, mode:)
6
+ @target = target
7
+ @local = local
8
+ @remote = remote
9
+ @mode = mode
10
+
11
+ @all_keys = (@local.keys + @remote.keys).sort.uniq
12
+
13
+ @diff =
14
+ @all_keys.collect do |key|
15
+ excluded = false
16
+ op = :noop
17
+ if @remote.has_key?(key) and not @local.has_key?(key)
18
+ case @mode
19
+ when :push
20
+ op = @target.delete? ? :delete : :ignore
21
+ when :pull
22
+ op = :create
23
+ end
24
+ elsif @local.has_key?(key) and not @remote.has_key?(key)
25
+ case @mode
26
+ when :push
27
+ op = :create
28
+ when :pull
29
+ op = @target.delete? ? :delete : :ignore
30
+ end
31
+ else
32
+ if @remote[key] == @local[key]
33
+ op = :noop
34
+ else
35
+ op = :update
36
+ end
37
+ end
38
+
39
+ ssm_key = [@target.prefix, key].compact.join("/").gsub('/.', '/').squeeze("/")
40
+
41
+ if @target.exclude.include?(key) or @target.exclude.include?(ssm_key)
42
+ op = :ignore
43
+ excluded = true
44
+ end
45
+
46
+ filename =
47
+ case @target.type
48
+ when :dir then File.join(@target.base_path, key)
49
+ when :file then @target.base_path
50
+ end
51
+
52
+ display_filename =
53
+ case @target.type
54
+ when :dir then File.join(@target.base_path, key).trim_path
55
+ when :file then "#{@target.base_path.trim_path}#{':'.gray}#{key.cyan}"
56
+ end
57
+
58
+ OpenStruct.new(
59
+ op: op,
60
+ excluded: excluded,
61
+ relative_path: key,
62
+ filename: filename,
63
+ display_filename: display_filename,
64
+ ssm_key: ssm_key,
65
+ local_content: @local[key],
66
+ remote_content: @remote[key],
67
+ )
68
+ end
69
+ end
70
+
71
+ def items_to_delete
72
+ @diff.select { |d| d.op == :delete }
73
+ end
74
+
75
+ def items_to_update
76
+ @diff.select { |d| d.op == :update }
77
+ end
78
+
79
+ def items_to_create
80
+ @diff.select { |d| d.op == :create }
81
+ end
82
+
83
+ def items_to_ignore
84
+ @diff.select { |d| d.op == :ignore }
85
+ end
86
+
87
+ def items_to_exclude
88
+ @diff.select { |d| d.op == :ignore and d.excluded == true }
89
+ end
90
+
91
+ def items_to_noop
92
+ @diff.select { |d| d.op == :noop }
93
+ end
94
+
95
+ def items_to_change
96
+ @diff.select { |d| [:delete, :update, :create].include?(d.op) }
97
+ end
98
+
99
+ def final_items
100
+ case @mode
101
+ when :push then @local
102
+ when :pull then @remote
103
+ end
104
+ end
105
+
106
+ def any_changes?
107
+ self.items_to_change.count > 0
108
+ end
109
+
110
+ def print_report
111
+ puts '='*85
112
+ puts @target.description(@mode)
113
+
114
+ puts " Keys scanned: #{@diff.count}"
115
+ if Paramsync.config.verbose?
116
+ puts " Keys ignored: #{self.items_to_ignore.count}"
117
+ puts " Keys in sync: #{self.items_to_noop.count}"
118
+ end
119
+
120
+ puts if self.any_changes?
121
+
122
+ from_content_key, to_content_key, to_path_key, to_type_display_name =
123
+ case @mode
124
+ when :push then [:local_content, :remote_content, :ssm_key, "Keys"]
125
+ when :pull
126
+ case @target.type
127
+ when :dir then [:remote_content, :local_content, :display_filename, "Files"]
128
+ when :file then [:remote_content, :local_content, :display_filename, "File entries"]
129
+ end
130
+ end
131
+
132
+ @diff.each do |item|
133
+ case item.op
134
+ when :create
135
+ puts "CREATE".bold.green + " #{item[to_path_key]}"
136
+ if(item[from_content_key][1])
137
+ puts '[SECURE]'
138
+ end
139
+ puts '-'*85
140
+ # simulate diff but without complaints about line endings
141
+ item[from_content_key][0].each_line do |line|
142
+ puts "+#{line.chomp}".green
143
+ end
144
+ puts '-'*85
145
+
146
+ when :update
147
+ puts "UPDATE".bold + " #{item[to_path_key]}"
148
+ if item[to_content_key][1] != item[from_content_key][1]
149
+ puts "#{item[to_content_key][1] ? '[SECURE]' : 'insecure'} => #{item[from_content_key][1] ? '[SECURE]' : 'insecure'}"
150
+ end
151
+ puts '-'*85
152
+ if item[to_content_key][0] != item[from_content_key][0]
153
+ puts Diffy::Diff.new(item[to_content_key][0], item[from_content_key][0]).to_s(:color)
154
+ end
155
+ puts '-'*85
156
+
157
+ when :delete
158
+ if @target.delete?
159
+ puts "DELETE".bold.red + " #{item[to_path_key]}"
160
+ puts '-'*85
161
+ # simulate diff but without complaints about line endings
162
+ item[to_content_key][0].each_line do |line|
163
+ puts "-#{line.chomp}".red
164
+ end
165
+ puts '-'*85
166
+ else
167
+ if Paramsync.config.verbose?
168
+ puts "IGNORE".bold + " #{item[to_path_key]}"
169
+ end
170
+ end
171
+
172
+ when :ignore
173
+ if Paramsync.config.verbose?
174
+ puts "IGNORE".bold + " #{item[to_path_key]}"
175
+ end
176
+
177
+ when :noop
178
+ if Paramsync.config.verbose?
179
+ puts "NO-OP!".bold + " #{item[to_path_key]}"
180
+ end
181
+
182
+ else
183
+ if Paramsync.config.verbose?
184
+ STDERR.puts "WARNING: unexpected operation '#{item.op}' for #{item[to_path_key]}"
185
+ end
186
+
187
+ end
188
+ end
189
+
190
+ if self.items_to_create.count > 0
191
+ puts
192
+ puts "#{to_type_display_name} to create: #{self.items_to_create.count}".bold
193
+ self.items_to_create.each do |item|
194
+ puts "+ #{item[to_path_key]}".green
195
+ end
196
+ end
197
+
198
+ if self.items_to_update.count > 0
199
+ puts
200
+ puts "#{to_type_display_name} to update: #{self.items_to_update.count}".bold
201
+ self.items_to_update.each do |item|
202
+ puts "~ #{item[to_path_key]}".blue
203
+ end
204
+ end
205
+
206
+ if @target.delete?
207
+ if self.items_to_delete.count > 0
208
+ puts
209
+ puts "#{to_type_display_name} to delete: #{self.items_to_delete.count}".bold
210
+ self.items_to_delete.each do |item|
211
+ puts "- #{item[to_path_key]}".red
212
+ end
213
+ end
214
+ end
215
+ end
216
+ end
217
+ end
@@ -0,0 +1,191 @@
1
+ # This software is public domain. No rights are reserved. See LICENSE for more information.
2
+
3
+ class Paramsync
4
+ class SyncTarget
5
+ VALID_CONFIG_KEYS = %w( name type region prefix path exclude chomp delete erb_enabled account )
6
+ attr_accessor :name, :type, :region, :prefix, :path, :exclude, :erb_enabled, :account, :ssm
7
+
8
+ REQUIRED_CONFIG_KEYS = %w( prefix region account )
9
+ VALID_TYPES = [ :dir, :file ]
10
+ DEFAULT_TYPE = :dir
11
+
12
+ def initialize(config:, account:, base_dir:)
13
+ if not config.is_a? Hash
14
+ raise Paramsync::ConfigFileInvalid.new("Sync target entries must be specified as hashes")
15
+ end
16
+
17
+ if (config.keys - Paramsync::SyncTarget::VALID_CONFIG_KEYS) != []
18
+ raise Paramsync::ConfigFileInvalid.new("Only the following keys are valid in a sync target entry: #{Paramsync::SyncTarget::VALID_CONFIG_KEYS.join(", ")}")
19
+ end
20
+
21
+ if (Paramsync::SyncTarget::REQUIRED_CONFIG_KEYS - config.keys) != []
22
+ raise Paramsync::ConfigFileInvalid.new("The following keys are required in a sync target entry: #{Paramsync::SyncTarget::REQUIRED_CONFIG_KEYS.join(", ")}")
23
+ end
24
+
25
+ @base_dir = base_dir
26
+ self.region = config['region']
27
+ self.prefix = config['prefix']
28
+ self.account = config['account']
29
+ self.path = config['path'] || config['prefix']
30
+ self.name = config['name']
31
+ self.type = (config['type'] || Paramsync::SyncTarget::DEFAULT_TYPE).to_sym
32
+ unless Paramsync::SyncTarget::VALID_TYPES.include?(self.type)
33
+ raise Paramsync::ConfigFileInvalid.new("Sync target '#{self.name || self.path}' has type '#{self.type}'. But only the following types are valid: #{Paramsync::SyncTarget::VALID_TYPES.collect(&:to_s).join(", ")}")
34
+ end
35
+
36
+ if self.type == :file and File.directory?(self.base_path)
37
+ raise Paramsync::ConfigFileInvalid.new("Sync target '#{self.name || self.path}' has type 'file', but path '#{self.path}' is a directory.")
38
+ end
39
+
40
+ self.exclude = config['exclude'] || []
41
+ if config.has_key?('chomp')
42
+ @do_chomp = config['chomp'] ? true : false
43
+ end
44
+ if config.has_key?('delete')
45
+ @do_delete = config['delete'] ? true : false
46
+ else
47
+ @do_delete = false
48
+ end
49
+
50
+ self.ssm = Aws::SSM::Client.new(
51
+ region: region,
52
+ credentials: Aws::AssumeRoleCredentials.new(
53
+ client: Aws::STS::Client.new(region: region),
54
+ role_arn: account,
55
+ role_session_name: "paramsync"
56
+ ),
57
+ )
58
+ self.erb_enabled = config['erb_enabled']
59
+ end
60
+
61
+ def erb_enabled?
62
+ @erb_enabled
63
+ end
64
+
65
+ def chomp?
66
+ @do_chomp
67
+ end
68
+
69
+ def delete?
70
+ @do_delete
71
+ end
72
+
73
+ def description(mode = :push)
74
+ if mode == :pull
75
+ "#{self.name.nil? ? '' : self.name.bold + "\n"}#{'ssm'.cyan}:#{self.region.green}:#{self.prefix} => #{'local'.blue}:#{self.path}"
76
+ else
77
+ "#{self.name.nil? ? '' : self.name.bold + "\n"}#{'local'.blue}:#{self.path} => #{'ssm'.cyan}:#{self.region.green}:#{self.prefix}"
78
+ end
79
+ end
80
+
81
+ def clear_cache
82
+ @base_path = nil
83
+ @local_files = nil
84
+ @local_items = nil
85
+ @remote_items = nil
86
+ end
87
+
88
+ def base_path
89
+ @base_path ||= File.join(@base_dir, self.path)
90
+ end
91
+
92
+ def local_files
93
+ # see https://stackoverflow.com/questions/357754/can-i-traverse-symlinked-directories-in-ruby-with-a-glob
94
+ @local_files ||= Dir["#{self.base_path}/**{,/*/**}/*"].select { |f| File.file?(f) }
95
+ end
96
+
97
+ def local_items
98
+ return @local_items if not @local_items.nil?
99
+ @local_items = {}
100
+
101
+ case self.type
102
+ when :dir
103
+ self.local_files.each do |local_file|
104
+ @local_items[local_file.sub(%r{^#{self.base_path}/?}, '')] =
105
+ load_local_file(local_file)
106
+ end
107
+
108
+ when :file
109
+ if File.exist?(self.base_path)
110
+ @local_items = local_items_from_file
111
+ end
112
+ end
113
+ @local_items.transform_values! do |val|
114
+ is_kms = val.bytes[0] == 0x01
115
+ if is_kms
116
+ val = Paramsync.config.kms_client.decrypt(
117
+ ciphertext_blob: val
118
+ ).plaintext
119
+ end
120
+ [val, is_kms]
121
+ end
122
+ @local_items
123
+ end
124
+
125
+ def local_items_from_file
126
+ if erb_enabled?
127
+ loaded_file = YAML.load(ERB.new(File.read(self.base_path)).result)
128
+ else
129
+ loaded_file = YAML.load_file(self.base_path)
130
+ end
131
+
132
+ flatten_hash(nil, loaded_file)
133
+ end
134
+
135
+ def load_local_file(local_file)
136
+ file = File.read(local_file)
137
+
138
+ if self.chomp?
139
+ encoded_file = file.chomp.force_encoding(Encoding::ASCII_8BIT)
140
+ else
141
+ encoded_file = file.force_encoding(Encoding::ASCII_8BIT)
142
+ end
143
+
144
+ return ERB.new(encoded_file).result if erb_enabled?
145
+ encoded_file
146
+ end
147
+
148
+ def remote_items
149
+ return @remote_items if not @remote_items.nil?
150
+ @remote_items = {}
151
+
152
+ resp = self.ssm.get_parameters_by_path(
153
+ path: self.prefix,
154
+ recursive: true,
155
+ with_decryption: true
156
+ )
157
+
158
+ return @remote_items if resp.values.nil?
159
+ resp.flat_map(&:parameters).each do |param|
160
+ @remote_items[param.name.gsub(self.prefix, '')] = [(param.value.nil? ? '' : param.value), param.type == 'SecureString']
161
+ end
162
+
163
+ @remote_items
164
+ end
165
+
166
+ def diff(mode)
167
+ Paramsync::Diff.new(target: self, local: self.local_items, remote: self.remote_items, mode: mode)
168
+ end
169
+
170
+ private def flatten_hash(prefix, hash)
171
+ new_hash = {}
172
+
173
+ hash.each do |k, v|
174
+ if k == '_' && !prefix.nil?
175
+ new_key = prefix
176
+ else
177
+ new_key = [prefix, k].compact.join('.').gsub('/.', '/')
178
+ end
179
+
180
+ case v
181
+ when Hash
182
+ new_hash.merge!(flatten_hash(new_key, v))
183
+ else
184
+ new_hash[new_key] = v.to_s
185
+ end
186
+ end
187
+
188
+ new_hash
189
+ end
190
+ end
191
+ end
@@ -0,0 +1,5 @@
1
+ # This software is public domain. No rights are reserved. See LICENSE for more information.
2
+
3
+ class Paramsync
4
+ VERSION = "0.1.0"
5
+ end
data/paramsync.gemspec ADDED
@@ -0,0 +1,31 @@
1
+ require_relative 'lib/paramsync/version.rb'
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = 'paramsync'
5
+ s.version = Paramsync::VERSION
6
+ s.authors = ['David Adams', 'Jacob Burroughs']
7
+ s.email = 'maths22@gmail.com'
8
+ s.date = Time.now.strftime('%Y-%m-%d')
9
+ s.license = 'CC0'
10
+ s.homepage = 'https://github.com/maths22/paramsync'
11
+ s.required_ruby_version = '>=2.4.0'
12
+
13
+ s.summary = 'Simple filesystem-to-aws parameter store synchronization'
14
+ s.description =
15
+ 'Syncs content from the filesystem to the aws parameter store. Derived from constancy for consul'
16
+
17
+ s.require_paths = ['lib']
18
+ s.files = Dir["lib/**/*.rb"] + [
19
+ 'bin/paramsync',
20
+ 'README.md',
21
+ 'LICENSE',
22
+ 'paramsync.gemspec'
23
+ ]
24
+ s.bindir = 'bin'
25
+ s.executables = ['paramsync']
26
+
27
+ s.add_dependency 'aws-sdk-ssm', '~> 1.89.0'
28
+ s.add_dependency 'aws-sdk-kms', '~> 1.37.0'
29
+
30
+ s.add_development_dependency 'rspec', '~> 3.0'
31
+ end
metadata ADDED
@@ -0,0 +1,103 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: paramsync
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - David Adams
8
+ - Jacob Burroughs
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2021-02-22 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: aws-sdk-ssm
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - "~>"
19
+ - !ruby/object:Gem::Version
20
+ version: 1.89.0
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - "~>"
26
+ - !ruby/object:Gem::Version
27
+ version: 1.89.0
28
+ - !ruby/object:Gem::Dependency
29
+ name: aws-sdk-kms
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - "~>"
33
+ - !ruby/object:Gem::Version
34
+ version: 1.37.0
35
+ type: :runtime
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - "~>"
40
+ - !ruby/object:Gem::Version
41
+ version: 1.37.0
42
+ - !ruby/object:Gem::Dependency
43
+ name: rspec
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - "~>"
47
+ - !ruby/object:Gem::Version
48
+ version: '3.0'
49
+ type: :development
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - "~>"
54
+ - !ruby/object:Gem::Version
55
+ version: '3.0'
56
+ description: Syncs content from the filesystem to the aws parameter store. Derived
57
+ from constancy for consul
58
+ email: maths22@gmail.com
59
+ executables:
60
+ - paramsync
61
+ extensions: []
62
+ extra_rdoc_files: []
63
+ files:
64
+ - LICENSE
65
+ - README.md
66
+ - bin/paramsync
67
+ - lib/paramsync.rb
68
+ - lib/paramsync/cli.rb
69
+ - lib/paramsync/cli/check_command.rb
70
+ - lib/paramsync/cli/config_command.rb
71
+ - lib/paramsync/cli/encrypt_command.rb
72
+ - lib/paramsync/cli/pull_command.rb
73
+ - lib/paramsync/cli/push_command.rb
74
+ - lib/paramsync/cli/targets_command.rb
75
+ - lib/paramsync/config.rb
76
+ - lib/paramsync/diff.rb
77
+ - lib/paramsync/sync_target.rb
78
+ - lib/paramsync/version.rb
79
+ - paramsync.gemspec
80
+ homepage: https://github.com/maths22/paramsync
81
+ licenses:
82
+ - CC0
83
+ metadata: {}
84
+ post_install_message:
85
+ rdoc_options: []
86
+ require_paths:
87
+ - lib
88
+ required_ruby_version: !ruby/object:Gem::Requirement
89
+ requirements:
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ version: 2.4.0
93
+ required_rubygems_version: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - ">="
96
+ - !ruby/object:Gem::Version
97
+ version: '0'
98
+ requirements: []
99
+ rubygems_version: 3.1.4
100
+ signing_key:
101
+ specification_version: 4
102
+ summary: Simple filesystem-to-aws parameter store synchronization
103
+ test_files: []