paramsync 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +12 -0
- data/README.md +334 -0
- data/bin/paramsync +8 -0
- data/lib/paramsync.rb +73 -0
- data/lib/paramsync/cli.rb +148 -0
- data/lib/paramsync/cli/check_command.rb +28 -0
- data/lib/paramsync/cli/config_command.rb +45 -0
- data/lib/paramsync/cli/encrypt_command.rb +22 -0
- data/lib/paramsync/cli/pull_command.rb +141 -0
- data/lib/paramsync/cli/push_command.rb +85 -0
- data/lib/paramsync/cli/targets_command.rb +21 -0
- data/lib/paramsync/config.rb +172 -0
- data/lib/paramsync/diff.rb +217 -0
- data/lib/paramsync/sync_target.rb +191 -0
- data/lib/paramsync/version.rb +5 -0
- data/paramsync.gemspec +31 -0
- metadata +103 -0
@@ -0,0 +1,28 @@
|
|
1
|
+
# This software is public domain. No rights are reserved. See LICENSE for more information.
|
2
|
+
|
3
|
+
class Paramsync
|
4
|
+
class CLI
|
5
|
+
class CheckCommand
|
6
|
+
class << self
|
7
|
+
def run(args)
|
8
|
+
Paramsync::CLI.configure
|
9
|
+
|
10
|
+
mode = if args.include?("--pull")
|
11
|
+
:pull
|
12
|
+
else
|
13
|
+
:push
|
14
|
+
end
|
15
|
+
|
16
|
+
Paramsync.config.sync_targets.each do |target|
|
17
|
+
diff = target.diff(mode)
|
18
|
+
diff.print_report
|
19
|
+
if not diff.any_changes?
|
20
|
+
puts "No changes to make for this sync target."
|
21
|
+
end
|
22
|
+
puts
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# This software is public domain. No rights are reserved. See LICENSE for more information.
|
2
|
+
|
3
|
+
class Paramsync
|
4
|
+
class CLI
|
5
|
+
class ConfigCommand
|
6
|
+
class << self
|
7
|
+
def run
|
8
|
+
Paramsync::CLI.configure
|
9
|
+
|
10
|
+
puts " Config file: #{Paramsync.config.config_file}"
|
11
|
+
puts " Verbose: #{Paramsync.config.verbose?.to_s.bold}"
|
12
|
+
puts
|
13
|
+
puts "Sync target defaults:"
|
14
|
+
puts " Chomp trailing newlines from local files: #{Paramsync.config.chomp?.to_s.bold}"
|
15
|
+
puts " Delete remote keys with no local file: #{Paramsync.config.delete?.to_s.bold}"
|
16
|
+
puts
|
17
|
+
puts "Sync targets:"
|
18
|
+
|
19
|
+
Paramsync.config.sync_targets.each do |target|
|
20
|
+
if target.name
|
21
|
+
puts "* #{target.name.bold}"
|
22
|
+
print ' '
|
23
|
+
else
|
24
|
+
print '*'
|
25
|
+
end
|
26
|
+
puts " Region: #{target.region}"
|
27
|
+
puts " Local type: #{target.type == :dir ? 'Directory' : 'Single file'}"
|
28
|
+
puts " #{target.type == :dir ? " Dir" : "File"} path: #{target.path}"
|
29
|
+
puts " Prefix: #{target.prefix}"
|
30
|
+
puts " Account: #{target.account}"
|
31
|
+
puts " Autochomp? #{target.chomp?}"
|
32
|
+
puts " Delete? #{target.delete?}"
|
33
|
+
if not target.exclude.empty?
|
34
|
+
puts " Exclusions:"
|
35
|
+
target.exclude.each do |exclusion|
|
36
|
+
puts " - #{exclusion}"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
puts
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# This software is public domain. No rights are reserved. See LICENSE for more information.
|
2
|
+
|
3
|
+
class Paramsync
|
4
|
+
class CLI
|
5
|
+
class EncryptCommand
|
6
|
+
class << self
|
7
|
+
def run(args)
|
8
|
+
Paramsync::CLI.configure
|
9
|
+
STDOUT.sync = true
|
10
|
+
|
11
|
+
ciphertext = Paramsync.config.kms_client.encrypt(
|
12
|
+
key_id: Paramsync.config.kms_key,
|
13
|
+
plaintext: args[1]
|
14
|
+
).ciphertext_blob
|
15
|
+
|
16
|
+
hash = { args[0] => ciphertext }
|
17
|
+
puts YAML.dump(hash).gsub("---\n", '')
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,141 @@
|
|
1
|
+
# This software is public domain. No rights are reserved. See LICENSE for more information.
|
2
|
+
|
3
|
+
class Paramsync
|
4
|
+
class CLI
|
5
|
+
class PullCommand
|
6
|
+
class << self
|
7
|
+
def run(args)
|
8
|
+
Paramsync::CLI.configure
|
9
|
+
STDOUT.sync = true
|
10
|
+
|
11
|
+
Paramsync.config.sync_targets.each do |target|
|
12
|
+
diff = target.diff(:pull)
|
13
|
+
|
14
|
+
diff.print_report
|
15
|
+
|
16
|
+
if not diff.any_changes?
|
17
|
+
puts
|
18
|
+
puts "Everything is in sync. No changes need to be made to this sync target."
|
19
|
+
next
|
20
|
+
end
|
21
|
+
|
22
|
+
puts
|
23
|
+
puts "Do you want to pull these changes?"
|
24
|
+
print " Enter '" + "yes".bold + "' to continue: "
|
25
|
+
answer = args.include?('--yes') ? 'yes' : gets.chomp
|
26
|
+
|
27
|
+
if answer.downcase != "yes"
|
28
|
+
puts
|
29
|
+
puts "Pull cancelled. No changes will be made to this sync target."
|
30
|
+
next
|
31
|
+
end
|
32
|
+
|
33
|
+
puts
|
34
|
+
case target.type
|
35
|
+
when :dir then self.pull_dir(diff)
|
36
|
+
when :file then self.pull_file(diff)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def pull_dir(diff)
|
42
|
+
diff.items_to_change.each do |item|
|
43
|
+
case item.op
|
44
|
+
when :create
|
45
|
+
print "CREATE".bold.green + " " + item.display_filename
|
46
|
+
begin
|
47
|
+
FileUtils.mkdir_p(File.dirname(item.filename))
|
48
|
+
# attempt to write atomically-ish
|
49
|
+
tmpfile = item.filename + ".paramsync-tmp"
|
50
|
+
File.open(tmpfile, "w") do |f|
|
51
|
+
f.write(writable_content(item.remote_content))
|
52
|
+
end
|
53
|
+
FileUtils.move(tmpfile, item.filename)
|
54
|
+
puts " OK".bold
|
55
|
+
rescue => e
|
56
|
+
puts " ERROR".bold.red
|
57
|
+
puts " #{e}"
|
58
|
+
end
|
59
|
+
|
60
|
+
when :update
|
61
|
+
print "UPDATE".bold.blue + " " + item.display_filename
|
62
|
+
begin
|
63
|
+
# attempt to write atomically-ish
|
64
|
+
tmpfile = item.filename + ".paramsync-tmp"
|
65
|
+
File.open(tmpfile, "w") do |f|
|
66
|
+
f.write(writable_content(item.remote_content))
|
67
|
+
end
|
68
|
+
FileUtils.move(tmpfile, item.filename)
|
69
|
+
puts " OK".bold
|
70
|
+
rescue => e
|
71
|
+
puts " ERROR".bold.red
|
72
|
+
puts " #{e}"
|
73
|
+
end
|
74
|
+
|
75
|
+
when :delete
|
76
|
+
print "DELETE".bold.red + " " + item.display_filename
|
77
|
+
begin
|
78
|
+
File.unlink(item.filename)
|
79
|
+
puts " OK".bold
|
80
|
+
rescue => e
|
81
|
+
puts " ERROR".bold.red
|
82
|
+
puts " #{e}"
|
83
|
+
end
|
84
|
+
|
85
|
+
else
|
86
|
+
if Paramsync.config.verbose?
|
87
|
+
STDERR.puts "paramsync: WARNING: unexpected operation '#{item.op}' for #{item.display_filename}"
|
88
|
+
next
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def pull_file(diff)
|
96
|
+
# build and write the file
|
97
|
+
filename_list = diff.items_to_change.collect(&:filename).uniq
|
98
|
+
if filename_list.length != 1
|
99
|
+
raise Paramsync::InternalError.new("Multiple filenames found for a 'file' type sync target. Something has gone wrong.")
|
100
|
+
end
|
101
|
+
filename = filename_list.first
|
102
|
+
display_filename = filename.trim_path
|
103
|
+
|
104
|
+
if File.exist?(filename)
|
105
|
+
print "UPDATE".bold.blue + " " + display_filename
|
106
|
+
else
|
107
|
+
print "CREATE".bold.green + " " + display_filename
|
108
|
+
end
|
109
|
+
|
110
|
+
begin
|
111
|
+
FileUtils.mkdir_p(File.dirname(filename))
|
112
|
+
# attempt to write atomically-ish
|
113
|
+
tmpfile = filename + ".paramsync-tmp"
|
114
|
+
File.open(tmpfile, "w") do |f|
|
115
|
+
f.write(diff.final_items.transform_values { |v| writable_content(v) }.to_yaml)
|
116
|
+
end
|
117
|
+
FileUtils.move(tmpfile, filename)
|
118
|
+
puts " OK".bold
|
119
|
+
rescue => e
|
120
|
+
puts " ERROR".bold.red
|
121
|
+
puts " #{e}"
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
private
|
126
|
+
|
127
|
+
def writable_content(remote_content)
|
128
|
+
to_write = remote_content[0]
|
129
|
+
if(remote_content[1])
|
130
|
+
to_write = Paramsync.config.kms_client.encrypt(
|
131
|
+
key_id: Paramsync.config.kms_key,
|
132
|
+
plaintext: to_write
|
133
|
+
).ciphertext_blob
|
134
|
+
end
|
135
|
+
to_write
|
136
|
+
end
|
137
|
+
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# This software is public domain. No rights are reserved. See LICENSE for more information.
|
2
|
+
|
3
|
+
class Paramsync
|
4
|
+
class CLI
|
5
|
+
class PushCommand
|
6
|
+
class << self
|
7
|
+
def run(args)
|
8
|
+
Paramsync::CLI.configure
|
9
|
+
STDOUT.sync = true
|
10
|
+
|
11
|
+
Paramsync.config.sync_targets.each do |target|
|
12
|
+
diff = target.diff(:push)
|
13
|
+
|
14
|
+
diff.print_report
|
15
|
+
|
16
|
+
if not diff.any_changes?
|
17
|
+
puts
|
18
|
+
puts "Everything is in sync. No changes need to be made to this sync target."
|
19
|
+
next
|
20
|
+
end
|
21
|
+
|
22
|
+
puts
|
23
|
+
puts "Do you want to push these changes?"
|
24
|
+
print " Enter '" + "yes".bold + "' to continue: "
|
25
|
+
answer = args.include?('--yes') ? 'yes' : gets.chomp
|
26
|
+
|
27
|
+
if answer.downcase != "yes"
|
28
|
+
puts
|
29
|
+
puts "Push cancelled. No changes will be made to this sync target."
|
30
|
+
next
|
31
|
+
end
|
32
|
+
|
33
|
+
puts
|
34
|
+
diff.items_to_change.each do |item|
|
35
|
+
case item.op
|
36
|
+
when :create
|
37
|
+
print "CREATE".bold.green + " " + item.ssm_key
|
38
|
+
begin
|
39
|
+
target.ssm.put_parameter(
|
40
|
+
name: item.ssm_key,
|
41
|
+
value: item.local_content[0],
|
42
|
+
type: item.local_content[1] ? 'SecureString' : 'String'
|
43
|
+
)
|
44
|
+
puts " OK".bold
|
45
|
+
rescue
|
46
|
+
puts " ERROR".bold.red
|
47
|
+
end
|
48
|
+
|
49
|
+
when :update
|
50
|
+
print "UPDATE".bold.blue + " " + item.ssm_key
|
51
|
+
begin
|
52
|
+
target.ssm.put_parameter(
|
53
|
+
name: item.ssm_key,
|
54
|
+
value: item.local_content[0],
|
55
|
+
type: item.local_content[1] ? 'SecureString' : 'String',
|
56
|
+
overwrite: true
|
57
|
+
)
|
58
|
+
puts " OK".bold
|
59
|
+
rescue
|
60
|
+
puts " ERROR".bold.red
|
61
|
+
end
|
62
|
+
|
63
|
+
when :delete
|
64
|
+
print "DELETE".bold.red + " " + item.ssm_key
|
65
|
+
begin
|
66
|
+
target.ssm.delete_parameter(name: item.ssm_key)
|
67
|
+
puts " OK".bold
|
68
|
+
rescue
|
69
|
+
puts " ERROR".bold.red
|
70
|
+
end
|
71
|
+
|
72
|
+
else
|
73
|
+
if Paramsync.config.verbose?
|
74
|
+
STDERR.puts "paramsync: WARNING: unexpected operation '#{item.op}' for #{item.ssm_key}"
|
75
|
+
next
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# This software is public domain. No rights are reserved. See LICENSE for more information.
|
2
|
+
|
3
|
+
class Paramsync
|
4
|
+
class CLI
|
5
|
+
class TargetsCommand
|
6
|
+
class << self
|
7
|
+
def run
|
8
|
+
Paramsync::CLI.configure
|
9
|
+
|
10
|
+
Paramsync.config.sync_targets.each do |target|
|
11
|
+
if target.name
|
12
|
+
puts target.name
|
13
|
+
else
|
14
|
+
puts "[unnamed target] #{target.region}:#{target.prefix}"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,172 @@
|
|
1
|
+
# This software is public domain. No rights are reserved. See LICENSE for more information.
|
2
|
+
|
3
|
+
require 'ostruct'
|
4
|
+
|
5
|
+
class Paramsync
|
6
|
+
class ConfigFileNotFound < RuntimeError; end
|
7
|
+
class ConfigFileInvalid < RuntimeError; end
|
8
|
+
|
9
|
+
class Config
|
10
|
+
CONFIG_FILENAMES = %w( paramsync.yml )
|
11
|
+
VALID_CONFIG_KEYS = %w( sync ssm paramsync )
|
12
|
+
VALID_SSM_CONFIG_KEYS = %w( accounts kms )
|
13
|
+
VALID_PARAMSYNC_CONFIG_KEYS = %w( verbose chomp delete color )
|
14
|
+
|
15
|
+
attr_accessor :config_file, :base_dir, :sync_targets, :target_allowlist, :ssm_accounts, :kms_client, :kms_key
|
16
|
+
|
17
|
+
class << self
|
18
|
+
# discover the nearest config file
|
19
|
+
def discover(dir: nil)
|
20
|
+
dir ||= Dir.pwd
|
21
|
+
|
22
|
+
CONFIG_FILENAMES.each do |filename|
|
23
|
+
full_path = File.join(dir, filename)
|
24
|
+
if File.exist?(full_path)
|
25
|
+
return full_path
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
dir == "/" ? nil : self.discover(dir: File.dirname(dir))
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def initialize(path: nil, targets: nil)
|
34
|
+
if path.nil? or File.directory?(path)
|
35
|
+
self.config_file = Paramsync::Config.discover(dir: path)
|
36
|
+
elsif File.exist?(path)
|
37
|
+
self.config_file = path
|
38
|
+
else
|
39
|
+
raise Paramsync::ConfigFileNotFound.new
|
40
|
+
end
|
41
|
+
|
42
|
+
if self.config_file.nil? or not File.exist?(self.config_file) or not File.readable?(self.config_file)
|
43
|
+
raise Paramsync::ConfigFileNotFound.new
|
44
|
+
end
|
45
|
+
|
46
|
+
self.config_file = File.expand_path(self.config_file)
|
47
|
+
self.base_dir = File.dirname(self.config_file)
|
48
|
+
self.target_allowlist = targets
|
49
|
+
parse!
|
50
|
+
end
|
51
|
+
|
52
|
+
def verbose?
|
53
|
+
@is_verbose
|
54
|
+
end
|
55
|
+
|
56
|
+
def chomp?
|
57
|
+
@do_chomp
|
58
|
+
end
|
59
|
+
|
60
|
+
def delete?
|
61
|
+
@do_delete
|
62
|
+
end
|
63
|
+
|
64
|
+
def color?
|
65
|
+
@use_color
|
66
|
+
end
|
67
|
+
|
68
|
+
def parse!
|
69
|
+
raw = {}
|
70
|
+
begin
|
71
|
+
raw = YAML.load(ERB.new(File.read(self.config_file)).result)
|
72
|
+
rescue
|
73
|
+
raise Paramsync::ConfigFileInvalid.new("Unable to parse config file as YAML")
|
74
|
+
end
|
75
|
+
|
76
|
+
if raw.is_a? FalseClass
|
77
|
+
# this generally means an empty config file
|
78
|
+
raw = {}
|
79
|
+
end
|
80
|
+
|
81
|
+
if not raw.is_a? Hash
|
82
|
+
raise Paramsync::ConfigFileInvalid.new("Config file must form a hash")
|
83
|
+
end
|
84
|
+
|
85
|
+
raw['ssm'] ||= {}
|
86
|
+
if not raw['ssm'].is_a? Hash
|
87
|
+
raise Paramsync::ConfigFileInvalid.new("'ssm' must be a hash")
|
88
|
+
end
|
89
|
+
|
90
|
+
if (raw['ssm'].keys - VALID_SSM_CONFIG_KEYS) != []
|
91
|
+
raise Paramsync::ConfigFileInvalid.new("Only the following keys are valid in the ssm config: #{VALID_SSM_CONFIG_KEYS.join(", ")}")
|
92
|
+
end
|
93
|
+
|
94
|
+
self.ssm_accounts = raw['ssm']['accounts']
|
95
|
+
|
96
|
+
raw['paramsync'] ||= {}
|
97
|
+
if not raw['paramsync'].is_a? Hash
|
98
|
+
raise Paramsync::ConfigFileInvalid.new("'paramsync' must be a hash")
|
99
|
+
end
|
100
|
+
|
101
|
+
if (raw['paramsync'].keys - VALID_PARAMSYNC_CONFIG_KEYS) != []
|
102
|
+
raise Paramsync::ConfigFileInvalid.new("Only the following keys are valid in the 'paramsync' config block: #{VALID_PARAMSYNC_CONFIG_KEYS.join(", ")}")
|
103
|
+
end
|
104
|
+
|
105
|
+
# verbose: default false
|
106
|
+
@is_verbose = raw['paramsync']['verbose'] ? true : false
|
107
|
+
if ENV['PARAMSYNC_VERBOSE']
|
108
|
+
@is_verbose = true
|
109
|
+
end
|
110
|
+
|
111
|
+
# chomp: default true
|
112
|
+
if raw['paramsync'].has_key?('chomp')
|
113
|
+
@do_chomp = raw['paramsync']['chomp'] ? true : false
|
114
|
+
else
|
115
|
+
@do_chomp = true
|
116
|
+
end
|
117
|
+
|
118
|
+
# delete: default false
|
119
|
+
@do_delete = raw['paramsync']['delete'] ? true : false
|
120
|
+
|
121
|
+
raw['sync'] ||= []
|
122
|
+
if not raw['sync'].is_a? Array
|
123
|
+
raise Paramsync::ConfigFileInvalid.new("'sync' must be an array")
|
124
|
+
end
|
125
|
+
|
126
|
+
# color: default true
|
127
|
+
if raw['paramsync'].has_key?('color')
|
128
|
+
@use_color = raw['paramsync']['color'] ? true : false
|
129
|
+
else
|
130
|
+
@use_color = true
|
131
|
+
end
|
132
|
+
|
133
|
+
if raw['ssm'].has_key?('kms')
|
134
|
+
self.kms_client = Aws::KMS::Client.new(
|
135
|
+
region: raw['ssm']['kms']['region'],
|
136
|
+
credentials: Aws::AssumeRoleCredentials.new(
|
137
|
+
client: Aws::STS::Client.new(region: raw['ssm']['kms']['region']),
|
138
|
+
role_arn: raw['ssm']['kms']['role'],
|
139
|
+
role_session_name: "paramsync"
|
140
|
+
),
|
141
|
+
)
|
142
|
+
self.kms_key = raw['ssm']['kms']['arn']
|
143
|
+
end
|
144
|
+
|
145
|
+
self.sync_targets = []
|
146
|
+
raw['sync'].each do |target|
|
147
|
+
if target.is_a? Hash
|
148
|
+
if target['chomp'].nil?
|
149
|
+
target['chomp'] = self.chomp?
|
150
|
+
end
|
151
|
+
if target['delete'].nil?
|
152
|
+
target['delete'] = self.delete?
|
153
|
+
end
|
154
|
+
account = self.ssm_accounts[target['account']]
|
155
|
+
if account.nil?
|
156
|
+
raise Paramsync::ConfigFileInvalid.new("Account '#{target['account']}' is not defined")
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
if not self.target_allowlist.nil?
|
161
|
+
# unnamed targets cannot be allowlisted
|
162
|
+
next if target['name'].nil?
|
163
|
+
|
164
|
+
# named targets must be on the allowlist
|
165
|
+
next if not self.target_allowlist.include?(target['name'])
|
166
|
+
end
|
167
|
+
|
168
|
+
self.sync_targets << Paramsync::SyncTarget.new(config: target, account: account['role'], base_dir: self.base_dir)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|