constancy 0.2.2 → 0.3.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.
- checksums.yaml +4 -4
- data/README.md +82 -12
- data/constancy.gemspec +2 -0
- data/lib/constancy.rb +12 -2
- data/lib/constancy/cli.rb +7 -1
- data/lib/constancy/cli/check_command.rb +10 -3
- data/lib/constancy/cli/config_command.rb +2 -1
- data/lib/constancy/cli/pull_command.rb +128 -0
- data/lib/constancy/cli/push_command.rb +13 -11
- data/lib/constancy/config.rb +1 -1
- data/lib/constancy/diff.rb +209 -0
- data/lib/constancy/sync_target.rb +43 -178
- data/lib/constancy/version.rb +1 -1
- metadata +18 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3119c8dc521f42f974a1f708273026eb49f59c19e25867408aae150dc83261ac
|
4
|
+
data.tar.gz: f6c729bfd5bbf17e29ecd21570076dcf212ae5af999b5f5d89f629216d1c33b0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1928aead12f4a255cc9e8595d3c024b115a30b950fdcefe5df3ab50d35b122ba8340ed7a95f4db15a36b04f8b8f124d10e00467000088d78a1e6e88e72d0c3e0
|
7
|
+
data.tar.gz: f1d991706a3e0883bf2b22f9e53588a1c6472a55376fb424c5ead455a5c704d99bc6966d378deda7d07c648c606941fb5499fa23affd837104e4e1e513362265
|
data/README.md
CHANGED
@@ -1,11 +1,12 @@
|
|
1
1
|
# constancy
|
2
2
|
|
3
|
-
Constancy
|
3
|
+
Constancy is a simple, straightforward CLI tool for synchronizing data from the
|
4
|
+
filesystem to the Consul KV store and vice-versa.
|
4
5
|
|
5
6
|
## Basic Usage
|
6
7
|
|
7
8
|
Run `constancy check` to see what differences exist, and `constancy push` to
|
8
|
-
synchronize the changes.
|
9
|
+
synchronize the changes from the filesystem to Consul.
|
9
10
|
|
10
11
|
$ constancy check
|
11
12
|
=====================================================================================
|
@@ -53,6 +54,16 @@ the `--target` flag:
|
|
53
54
|
|
54
55
|
Run `constancy --help` for additional options and commands.
|
55
56
|
|
57
|
+
## Pull Mode
|
58
|
+
|
59
|
+
Constancy can also sync _from_ Consul to the local filesystem. This can be
|
60
|
+
particularly useful for seeding a git repo with the current contents of a Consul
|
61
|
+
KV database.
|
62
|
+
|
63
|
+
Run `constancy check --pull` to get a summary of changes, and `constancy pull`
|
64
|
+
to actually sync the changes to the local filesystem. Additional arguments such
|
65
|
+
as `--target <name>` work in pull mode as well.
|
66
|
+
|
56
67
|
|
57
68
|
## Configuration
|
58
69
|
|
@@ -142,14 +153,20 @@ required. An example `constancy.yml` is below including explanatory comments:
|
|
142
153
|
# required if you wish to target specific sync targets using
|
143
154
|
# the `--target` CLI flag.
|
144
155
|
# prefix - (required) The Consul KV prefix to synchronize to.
|
156
|
+
# type - (default: "dir") The type of local file storage. Either
|
157
|
+
# 'dir' to indicate a directory tree of files corresponding to
|
158
|
+
# Consul keys; or 'file' to indicate a single YAML file with a
|
159
|
+
# map of relative key paths to values.
|
145
160
|
# datacenter - The Consul datacenter to synchronize to. If not
|
146
161
|
# specified, the `datacenter` setting in the `consul` section
|
147
162
|
# will be used. If that is also not specified, the sync will
|
148
163
|
# happen with the local datacenter of the Consul agent.
|
149
|
-
# path - (required) The relative filesystem path to the
|
150
|
-
# containing the files with content to synchronize
|
151
|
-
#
|
152
|
-
#
|
164
|
+
# path - (required) The relative filesystem path to either the
|
165
|
+
# directory containing the files with content to synchronize
|
166
|
+
# to Consul if this sync target has type=dir, or the local file
|
167
|
+
# containing a hash of remote keys if this sync target has
|
168
|
+
# type=file. This path is calculated relative to the directory
|
169
|
+
# containing the configuration file.
|
153
170
|
# delete - Whether or not to delete remote keys that do not exist
|
154
171
|
# in the local filesystem. This inherits the setting from the
|
155
172
|
# `constancy` section, or if not specified, defaults to `false`.
|
@@ -172,13 +189,67 @@ required. An example `constancy.yml` is below including explanatory comments:
|
|
172
189
|
- config/myapp/prod/cowboy-yolo
|
173
190
|
- name: myapp-private
|
174
191
|
prefix: private/myapp
|
192
|
+
type: dir
|
175
193
|
datacenter: dc1
|
176
194
|
path: consul/private
|
177
195
|
delete: true
|
196
|
+
- name: yourapp-config
|
197
|
+
prefix: config/yourapp
|
198
|
+
type: file
|
199
|
+
datacenter: dc1
|
200
|
+
path: consul/yourapp.yml
|
201
|
+
delete: true
|
178
202
|
|
179
203
|
You can run `constancy config` to get a summary of the defined configuration
|
180
204
|
and to double-check config syntax.
|
181
205
|
|
206
|
+
### File sync targets
|
207
|
+
|
208
|
+
When using `type: file` for a sync target (see example above), the local path
|
209
|
+
should be a YAML (or JSON) file containing a hash of relative key paths to the
|
210
|
+
contents of those keys. So for example, given this configuration:
|
211
|
+
|
212
|
+
sync:
|
213
|
+
- name: config
|
214
|
+
prefix: config/yourapp
|
215
|
+
type: file
|
216
|
+
datacenter: dc1
|
217
|
+
path: yourapp.yml
|
218
|
+
|
219
|
+
If the file `yourapp.yml` has the following content:
|
220
|
+
|
221
|
+
---
|
222
|
+
prod/dbname: yourapp
|
223
|
+
prod/message: |
|
224
|
+
Hello, world. This is a multiline message.
|
225
|
+
I hope you like it.
|
226
|
+
Thanks,
|
227
|
+
YourApp
|
228
|
+
prod/app/config.json: |
|
229
|
+
{
|
230
|
+
"port": 8080,
|
231
|
+
"listen": "0.0.0.0",
|
232
|
+
"enabled": true
|
233
|
+
}
|
234
|
+
|
235
|
+
Then `constancy push` will attempt to create and/or update the following keys
|
236
|
+
with the corresponding content from `yourapp.yml`:
|
237
|
+
|
238
|
+
config/yourapp/prod/dbname
|
239
|
+
config/yourapp/prod/message
|
240
|
+
config/yourapp/prod/app/config.json
|
241
|
+
|
242
|
+
Likewise, a `constancy pull` operation will work in reverse, and pull values
|
243
|
+
from any keys under `config/yourapp/` into the file `yourapp.yml`, overwriting
|
244
|
+
whatever values are there.
|
245
|
+
|
246
|
+
Note that JSON is also supported for this file for `push` operations, given that
|
247
|
+
YAML parsers will correctly parse JSON. However, `constancy pull` will only
|
248
|
+
write out YAML in the current version.
|
249
|
+
|
250
|
+
Also important to note that any comments in the YAML file will be lost on a
|
251
|
+
`pull` operation that updates a file sync target.
|
252
|
+
|
182
253
|
|
183
254
|
### Dynamic configuration
|
184
255
|
|
@@ -216,18 +287,17 @@ Constancy may be partially configured using environment variables:
|
|
216
287
|
|
217
288
|
## Roadmap
|
218
289
|
|
219
|
-
Constancy is
|
290
|
+
Constancy is relatively new software. There's more to be done. Some ideas, which
|
291
|
+
may or may not ever be implemented:
|
220
292
|
|
293
|
+
* Using CAS to verify the key has not changed in the interim before updating/deleting
|
294
|
+
* Automation support for running non-interactively
|
221
295
|
* Pattern- and prefix-based exclusions
|
296
|
+
* Logging of changes to files, syslog, other services
|
222
297
|
* Other commands to assist in managing Consul KV sets
|
223
|
-
* Automation support for running non-interactively
|
224
298
|
* Git awareness (branches, commit state, etc)
|
225
299
|
* Automated tests
|
226
|
-
* Logging of changes to files, syslog, other services
|
227
|
-
* Pull mode to sync from Consul to local filesystem
|
228
|
-
* Using CAS to verify the key has not changed in the interim before updating/deleting
|
229
300
|
* Submitting changes in batches using transactions
|
230
|
-
* Optional expansion of YAML or JSON files into multiple keys
|
231
301
|
|
232
302
|
|
233
303
|
## Contributing
|
data/constancy.gemspec
CHANGED
data/lib/constancy.rb
CHANGED
@@ -1,14 +1,19 @@
|
|
1
1
|
# This software is public domain. No rights are reserved. See LICENSE for more information.
|
2
2
|
|
3
3
|
require 'erb'
|
4
|
-
require 'yaml'
|
5
4
|
require 'imperium'
|
5
|
+
require 'fileutils'
|
6
|
+
require 'ostruct'
|
7
|
+
require 'yaml'
|
6
8
|
|
7
9
|
require 'constancy/version'
|
8
10
|
require 'constancy/config'
|
11
|
+
require 'constancy/diff'
|
9
12
|
require 'constancy/sync_target'
|
10
13
|
|
11
14
|
class Constancy
|
15
|
+
class InternalError < RuntimeError; end
|
16
|
+
|
12
17
|
class << self
|
13
18
|
@@config = nil
|
14
19
|
|
@@ -37,8 +42,13 @@ class Constancy
|
|
37
42
|
end
|
38
43
|
end
|
39
44
|
|
40
|
-
# monkeypatch String for
|
45
|
+
# monkeypatch String for display prettiness
|
41
46
|
class String
|
47
|
+
# trim_path replaces the HOME directory on an absolute path with '~'
|
48
|
+
def trim_path
|
49
|
+
self.sub(%r(^#{ENV['HOME']}), '~')
|
50
|
+
end
|
51
|
+
|
42
52
|
def colorize(s,e=0)
|
43
53
|
Constancy.config.color? ? "\e[#{s}m#{self}\e[#{e}m" : self
|
44
54
|
end
|
data/lib/constancy/cli.rb
CHANGED
@@ -4,6 +4,7 @@ require 'constancy'
|
|
4
4
|
require 'diffy'
|
5
5
|
require 'constancy/cli/check_command'
|
6
6
|
require 'constancy/cli/push_command'
|
7
|
+
require 'constancy/cli/pull_command'
|
7
8
|
require 'constancy/cli/config_command'
|
8
9
|
|
9
10
|
class Constancy
|
@@ -53,6 +54,7 @@ Usage:
|
|
53
54
|
Commands:
|
54
55
|
check Print a summary of changes to be made
|
55
56
|
push Push changes from filesystem to Consul
|
57
|
+
pull Pull changes from Consul to filesystem
|
56
58
|
config Print a summary of the active configuration
|
57
59
|
|
58
60
|
General options:
|
@@ -60,6 +62,9 @@ General options:
|
|
60
62
|
--config <file> Use the specified config file
|
61
63
|
--target <tgt> Only apply to the specified target name or names (comma-separated)
|
62
64
|
|
65
|
+
Options for 'check' command:
|
66
|
+
--pull Perform dry run in pull mode
|
67
|
+
|
63
68
|
USAGE
|
64
69
|
exit 1
|
65
70
|
end
|
@@ -116,8 +121,9 @@ USAGE
|
|
116
121
|
|
117
122
|
when :command
|
118
123
|
case self.command
|
119
|
-
when 'check' then Constancy::CLI::CheckCommand.run
|
124
|
+
when 'check' then Constancy::CLI::CheckCommand.run(self.extra_args)
|
120
125
|
when 'push' then Constancy::CLI::PushCommand.run
|
126
|
+
when 'pull' then Constancy::CLI::PullCommand.run
|
121
127
|
when 'config' then Constancy::CLI::ConfigCommand.run
|
122
128
|
when nil then self.print_usage
|
123
129
|
|
@@ -4,12 +4,19 @@ class Constancy
|
|
4
4
|
class CLI
|
5
5
|
class CheckCommand
|
6
6
|
class << self
|
7
|
-
def run
|
7
|
+
def run(args)
|
8
8
|
Constancy::CLI.configure
|
9
9
|
|
10
|
+
mode = if args.include?("--pull")
|
11
|
+
:pull
|
12
|
+
else
|
13
|
+
:push
|
14
|
+
end
|
15
|
+
|
10
16
|
Constancy.config.sync_targets.each do |target|
|
11
|
-
target.
|
12
|
-
|
17
|
+
diff = target.diff(mode)
|
18
|
+
diff.print_report
|
19
|
+
if not diff.any_changes?
|
13
20
|
puts "No changes to make for this sync target."
|
14
21
|
end
|
15
22
|
puts
|
@@ -36,7 +36,8 @@ class Constancy
|
|
36
36
|
print '*'
|
37
37
|
end
|
38
38
|
puts " Datacenter: #{target.datacenter}"
|
39
|
-
puts "
|
39
|
+
puts " Local type: #{target.type == :dir ? 'Directory' : 'Single file'}"
|
40
|
+
puts " #{target.type == :dir ? " Dir" : "File"} path: #{target.path}"
|
40
41
|
puts " Prefix: #{target.prefix}"
|
41
42
|
puts " Autochomp? #{target.chomp?}"
|
42
43
|
puts " Delete? #{target.delete?}"
|
@@ -0,0 +1,128 @@
|
|
1
|
+
# This software is public domain. No rights are reserved. See LICENSE for more information.
|
2
|
+
|
3
|
+
class Constancy
|
4
|
+
class CLI
|
5
|
+
class PullCommand
|
6
|
+
class << self
|
7
|
+
def run
|
8
|
+
Constancy::CLI.configure
|
9
|
+
STDOUT.sync = true
|
10
|
+
|
11
|
+
Constancy.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 = 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 + ".constancy-tmp"
|
50
|
+
File.open(tmpfile, "w") do |f|
|
51
|
+
f.write(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 + ".constancy-tmp"
|
65
|
+
File.open(tmpfile, "w") do |f|
|
66
|
+
f.write(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 Constancy.config.verbose?
|
87
|
+
STDERR.puts "constancy: 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 Constancy::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 + ".constancy-tmp"
|
114
|
+
File.open(tmpfile, "w") do |f|
|
115
|
+
f.write(diff.final_items.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
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
@@ -9,9 +9,11 @@ class Constancy
|
|
9
9
|
STDOUT.sync = true
|
10
10
|
|
11
11
|
Constancy.config.sync_targets.each do |target|
|
12
|
-
target.
|
12
|
+
diff = target.diff(:push)
|
13
13
|
|
14
|
-
|
14
|
+
diff.print_report
|
15
|
+
|
16
|
+
if not diff.any_changes?
|
15
17
|
puts
|
16
18
|
puts "Everything is in sync. No changes need to be made to this sync target."
|
17
19
|
next
|
@@ -29,11 +31,11 @@ class Constancy
|
|
29
31
|
end
|
30
32
|
|
31
33
|
puts
|
32
|
-
|
33
|
-
case item
|
34
|
+
diff.items_to_change.each do |item|
|
35
|
+
case item.op
|
34
36
|
when :create
|
35
|
-
print "CREATE".bold.green + " " + item
|
36
|
-
resp = target.consul.put(item
|
37
|
+
print "CREATE".bold.green + " " + item.consul_key
|
38
|
+
resp = target.consul.put(item.consul_key, item.local_content, dc: target.datacenter)
|
37
39
|
if resp.success?
|
38
40
|
puts " OK".bold
|
39
41
|
else
|
@@ -41,8 +43,8 @@ class Constancy
|
|
41
43
|
end
|
42
44
|
|
43
45
|
when :update
|
44
|
-
print "UPDATE".bold.blue + " " + item
|
45
|
-
resp = target.consul.put(item
|
46
|
+
print "UPDATE".bold.blue + " " + item.consul_key
|
47
|
+
resp = target.consul.put(item.consul_key, item.local_content, dc: target.datacenter)
|
46
48
|
if resp.success?
|
47
49
|
puts " OK".bold
|
48
50
|
else
|
@@ -50,8 +52,8 @@ class Constancy
|
|
50
52
|
end
|
51
53
|
|
52
54
|
when :delete
|
53
|
-
print "DELETE".bold.red + " " + item
|
54
|
-
resp = target.consul.delete(item
|
55
|
+
print "DELETE".bold.red + " " + item.consul_key
|
56
|
+
resp = target.consul.delete(item.consul_key, dc: target.datacenter)
|
55
57
|
if resp.success?
|
56
58
|
puts " OK".bold
|
57
59
|
else
|
@@ -60,7 +62,7 @@ class Constancy
|
|
60
62
|
|
61
63
|
else
|
62
64
|
if Constancy.config.verbose?
|
63
|
-
STDERR.puts "constancy: WARNING: unexpected operation '#{item
|
65
|
+
STDERR.puts "constancy: WARNING: unexpected operation '#{item.op}' for #{item.consul_key}"
|
64
66
|
next
|
65
67
|
end
|
66
68
|
|
data/lib/constancy/config.rb
CHANGED
@@ -251,7 +251,7 @@ class Constancy
|
|
251
251
|
next if not self.target_whitelist.include?(target['name'])
|
252
252
|
end
|
253
253
|
|
254
|
-
self.sync_targets << Constancy::SyncTarget.new(config: target, imperium_config: self.consul)
|
254
|
+
self.sync_targets << Constancy::SyncTarget.new(config: target, imperium_config: self.consul, base_dir: self.base_dir)
|
255
255
|
end
|
256
256
|
|
257
257
|
end
|
@@ -0,0 +1,209 @@
|
|
1
|
+
# This software is public domain. No rights are reserved. See LICENSE for more information.
|
2
|
+
|
3
|
+
class Constancy
|
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
|
+
consul_key = [@target.prefix, key].compact.join("/")
|
40
|
+
|
41
|
+
if @target.exclude.include?(key) or @target.exclude.include?(consul_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
|
+
consul_key: consul_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 Constancy.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, :consul_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
|
+
puts '-'*85
|
137
|
+
# simulate diff but without complaints about line endings
|
138
|
+
item[from_content_key].each_line do |line|
|
139
|
+
puts "+#{line.chomp}".green
|
140
|
+
end
|
141
|
+
puts '-'*85
|
142
|
+
|
143
|
+
when :update
|
144
|
+
puts "UPDATE".bold + " #{item[to_path_key]}"
|
145
|
+
puts '-'*85
|
146
|
+
puts Diffy::Diff.new(item[to_content_key], item[from_content_key]).to_s(:color)
|
147
|
+
puts '-'*85
|
148
|
+
|
149
|
+
when :delete
|
150
|
+
if @target.delete?
|
151
|
+
puts "DELETE".bold.red + " #{item[to_path_key]}"
|
152
|
+
puts '-'*85
|
153
|
+
# simulate diff but without complaints about line endings
|
154
|
+
item[to_content_key].each_line do |line|
|
155
|
+
puts "-#{line.chomp}".red
|
156
|
+
end
|
157
|
+
puts '-'*85
|
158
|
+
else
|
159
|
+
if Constancy.config.verbose?
|
160
|
+
puts "IGNORE".bold + " #{item[to_path_key]}"
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
when :ignore
|
165
|
+
if Constancy.config.verbose?
|
166
|
+
puts "IGNORE".bold + " #{item[to_path_key]}"
|
167
|
+
end
|
168
|
+
|
169
|
+
when :noop
|
170
|
+
if Constancy.config.verbose?
|
171
|
+
puts "NO-OP!".bold + " #{item[to_path_key]}"
|
172
|
+
end
|
173
|
+
|
174
|
+
else
|
175
|
+
if Constancy.config.verbose?
|
176
|
+
STDERR.puts "WARNING: unexpected operation '#{item.op}' for #{item[to_path_key]}"
|
177
|
+
end
|
178
|
+
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
if self.items_to_create.count > 0
|
183
|
+
puts
|
184
|
+
puts "#{to_type_display_name} to create: #{self.items_to_create.count}".bold
|
185
|
+
self.items_to_create.each do |item|
|
186
|
+
puts "+ #{item[to_path_key]}".green
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
if self.items_to_update.count > 0
|
191
|
+
puts
|
192
|
+
puts "#{to_type_display_name} to update: #{self.items_to_update.count}".bold
|
193
|
+
self.items_to_update.each do |item|
|
194
|
+
puts "~ #{item[to_path_key]}".blue
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
if @target.delete?
|
199
|
+
if self.items_to_delete.count > 0
|
200
|
+
puts
|
201
|
+
puts "#{to_type_display_name} to delete: #{self.items_to_delete.count}".bold
|
202
|
+
self.items_to_delete.each do |item|
|
203
|
+
puts "- #{item[to_path_key]}".red
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
@@ -2,12 +2,14 @@
|
|
2
2
|
|
3
3
|
class Constancy
|
4
4
|
class SyncTarget
|
5
|
-
VALID_CONFIG_KEYS = %w( name datacenter prefix path exclude chomp delete )
|
6
|
-
attr_accessor :name, :datacenter, :prefix, :path, :exclude, :consul
|
5
|
+
VALID_CONFIG_KEYS = %w( name type datacenter prefix path exclude chomp delete )
|
6
|
+
attr_accessor :name, :type, :datacenter, :prefix, :path, :exclude, :consul
|
7
7
|
|
8
8
|
REQUIRED_CONFIG_KEYS = %w( prefix )
|
9
|
+
VALID_TYPES = [ :dir, :file ]
|
10
|
+
DEFAULT_TYPE = :dir
|
9
11
|
|
10
|
-
def initialize(config:, imperium_config:)
|
12
|
+
def initialize(config:, imperium_config:, base_dir:)
|
11
13
|
if not config.is_a? Hash
|
12
14
|
raise Constancy::ConfigFileInvalid.new("Sync target entries must be specified as hashes")
|
13
15
|
end
|
@@ -17,13 +19,23 @@ class Constancy
|
|
17
19
|
end
|
18
20
|
|
19
21
|
if (Constancy::SyncTarget::REQUIRED_CONFIG_KEYS - config.keys) != []
|
20
|
-
raise Constancy::ConfigFileInvalid.new("The following keys are required
|
22
|
+
raise Constancy::ConfigFileInvalid.new("The following keys are required in a sync target entry: #{Constancy::SyncTarget::REQUIRED_CONFIG_KEYS.join(", ")}")
|
21
23
|
end
|
22
24
|
|
25
|
+
@base_dir = base_dir
|
23
26
|
self.datacenter = config['datacenter']
|
24
27
|
self.prefix = config['prefix']
|
25
28
|
self.path = config['path'] || config['prefix']
|
26
29
|
self.name = config['name']
|
30
|
+
self.type = (config['type'] || Constancy::SyncTarget::DEFAULT_TYPE).to_sym
|
31
|
+
unless Constancy::SyncTarget::VALID_TYPES.include?(self.type)
|
32
|
+
raise Constancy::ConfigFileInvalid.new("Sync target '#{self.name || self.path}' has type '#{self.type}'. But only the following types are valid: #{Constancy::SyncTarget::VALID_TYPES.collect(&:to_s).join(", ")}")
|
33
|
+
end
|
34
|
+
|
35
|
+
if self.type == :file and File.directory?(self.base_path)
|
36
|
+
raise Constancy::ConfigFileInvalid.new("Sync target '#{self.name || self.path}' has type 'file', but path '#{self.path}' is a directory.")
|
37
|
+
end
|
38
|
+
|
27
39
|
self.exclude = config['exclude'] || []
|
28
40
|
if config.has_key?('chomp')
|
29
41
|
@do_chomp = config['chomp'] ? true : false
|
@@ -45,36 +57,49 @@ class Constancy
|
|
45
57
|
@do_delete
|
46
58
|
end
|
47
59
|
|
48
|
-
def description
|
49
|
-
|
60
|
+
def description(mode = :push)
|
61
|
+
if mode == :pull
|
62
|
+
"#{self.name.nil? ? '' : self.name.bold + "\n"}#{'consul'.cyan}:#{self.datacenter.green}:#{self.prefix} => #{'local'.blue}:#{self.path}"
|
63
|
+
else
|
64
|
+
"#{self.name.nil? ? '' : self.name.bold + "\n"}#{'local'.blue}:#{self.path} => #{'consul'.cyan}:#{self.datacenter.green}:#{self.prefix}"
|
65
|
+
end
|
50
66
|
end
|
51
67
|
|
52
68
|
def clear_cache
|
53
|
-
@
|
69
|
+
@base_path = nil
|
54
70
|
@local_files = nil
|
55
71
|
@local_items = nil
|
56
72
|
@remote_items = nil
|
57
73
|
end
|
58
74
|
|
59
|
-
def
|
60
|
-
@
|
75
|
+
def base_path
|
76
|
+
@base_path ||= File.join(@base_dir, self.path)
|
61
77
|
end
|
62
78
|
|
63
79
|
def local_files
|
64
80
|
# see https://stackoverflow.com/questions/357754/can-i-traverse-symlinked-directories-in-ruby-with-a-glob
|
65
|
-
@local_files ||= Dir["#{self.
|
81
|
+
@local_files ||= Dir["#{self.base_path}/**{,/*/**}/*"].select { |f| File.file?(f) }
|
66
82
|
end
|
67
83
|
|
68
84
|
def local_items
|
69
85
|
return @local_items if not @local_items.nil?
|
70
86
|
@local_items = {}
|
71
87
|
|
72
|
-
self.
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
88
|
+
case self.type
|
89
|
+
when :dir
|
90
|
+
self.local_files.each do |local_file|
|
91
|
+
@local_items[local_file.sub(%r{^#{self.base_path}/?}, '')] =
|
92
|
+
if self.chomp?
|
93
|
+
File.read(local_file).chomp.force_encoding(Encoding::ASCII_8BIT)
|
94
|
+
else
|
95
|
+
File.read(local_file).force_encoding(Encoding::ASCII_8BIT)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
when :file
|
100
|
+
if File.exist?(self.base_path)
|
101
|
+
@local_items = YAML.load_file(self.base_path)
|
102
|
+
end
|
78
103
|
end
|
79
104
|
|
80
105
|
@local_items
|
@@ -94,168 +119,8 @@ class Constancy
|
|
94
119
|
@remote_items
|
95
120
|
end
|
96
121
|
|
97
|
-
def diff
|
98
|
-
local
|
99
|
-
remote = self.remote_items
|
100
|
-
all_keys = (local.keys + remote.keys).sort.uniq
|
101
|
-
|
102
|
-
all_keys.collect do |key|
|
103
|
-
excluded = false
|
104
|
-
op = :noop
|
105
|
-
if remote.has_key?(key) and not local.has_key?(key)
|
106
|
-
if self.delete?
|
107
|
-
op = :delete
|
108
|
-
else
|
109
|
-
op = :ignore
|
110
|
-
end
|
111
|
-
elsif local.has_key?(key) and not remote.has_key?(key)
|
112
|
-
op = :create
|
113
|
-
else
|
114
|
-
if remote[key] == local[key]
|
115
|
-
op = :noop
|
116
|
-
else
|
117
|
-
op = :update
|
118
|
-
end
|
119
|
-
end
|
120
|
-
|
121
|
-
consul_key = [self.prefix, key].compact.join("/")
|
122
|
-
|
123
|
-
if self.exclude.include?(key) or self.exclude.include?(consul_key)
|
124
|
-
op = :ignore
|
125
|
-
excluded = true
|
126
|
-
end
|
127
|
-
|
128
|
-
{
|
129
|
-
:op => op,
|
130
|
-
:excluded => excluded,
|
131
|
-
:relative_path => key,
|
132
|
-
:filename => File.join(self.base_dir, key),
|
133
|
-
:consul_key => consul_key,
|
134
|
-
:local_content => local[key],
|
135
|
-
:remote_content => remote[key],
|
136
|
-
}
|
137
|
-
end
|
138
|
-
end
|
139
|
-
|
140
|
-
def items_to_delete
|
141
|
-
self.diff.select { |d| d[:op] == :delete }
|
142
|
-
end
|
143
|
-
|
144
|
-
def items_to_update
|
145
|
-
self.diff.select { |d| d[:op] == :update }
|
146
|
-
end
|
147
|
-
|
148
|
-
def items_to_create
|
149
|
-
self.diff.select { |d| d[:op] == :create }
|
150
|
-
end
|
151
|
-
|
152
|
-
def items_to_ignore
|
153
|
-
self.diff.select { |d| d[:op] == :ignore }
|
154
|
-
end
|
155
|
-
|
156
|
-
def items_to_exclude
|
157
|
-
self.diff.select { |d| d[:op] == :ignore and d[:excluded] == true }
|
158
|
-
end
|
159
|
-
|
160
|
-
def items_to_noop
|
161
|
-
self.diff.select { |d| d[:op] == :noop }
|
162
|
-
end
|
163
|
-
|
164
|
-
def items_to_change
|
165
|
-
self.diff.select { |d| [:delete, :update, :create].include?(d[:op]) }
|
166
|
-
end
|
167
|
-
|
168
|
-
def any_changes?
|
169
|
-
self.items_to_change.count > 0
|
170
|
-
end
|
171
|
-
|
172
|
-
def print_report
|
173
|
-
puts '='*85
|
174
|
-
puts self.description
|
175
|
-
|
176
|
-
puts " Keys scanned: #{self.diff.count}"
|
177
|
-
if Constancy.config.verbose?
|
178
|
-
puts " Keys ignored: #{self.items_to_ignore.count}"
|
179
|
-
puts " Keys in sync: #{self.items_to_noop.count}"
|
180
|
-
end
|
181
|
-
|
182
|
-
puts if self.any_changes?
|
183
|
-
|
184
|
-
self.diff.each do |item|
|
185
|
-
case item[:op]
|
186
|
-
when :create
|
187
|
-
puts "CREATE".bold.green + " #{item[:consul_key]}"
|
188
|
-
puts '-'*85
|
189
|
-
# simulate diff but without complaints about line endings
|
190
|
-
item[:local_content].each_line do |line|
|
191
|
-
puts "+#{line.chomp}".green
|
192
|
-
end
|
193
|
-
puts '-'*85
|
194
|
-
|
195
|
-
when :update
|
196
|
-
puts "UPDATE".bold + " #{item[:consul_key]}"
|
197
|
-
puts '-'*85
|
198
|
-
puts Diffy::Diff.new(item[:remote_content], item[:local_content]).to_s(:color)
|
199
|
-
puts '-'*85
|
200
|
-
|
201
|
-
when :delete
|
202
|
-
if self.delete?
|
203
|
-
puts "DELETE".bold.red + " #{item[:consul_key]}"
|
204
|
-
puts '-'*85
|
205
|
-
# simulate diff but without complaints about line endings
|
206
|
-
item[:remote_content].each_line do |line|
|
207
|
-
puts "-#{line.chomp}".red
|
208
|
-
end
|
209
|
-
puts '-'*85
|
210
|
-
else
|
211
|
-
if Constancy.config.verbose?
|
212
|
-
puts "IGNORE".bold + " #{item[:consul_key]}"
|
213
|
-
end
|
214
|
-
end
|
215
|
-
|
216
|
-
when :ignore
|
217
|
-
if Constancy.config.verbose?
|
218
|
-
puts "IGNORE".bold + " #{item[:consul_key]}"
|
219
|
-
end
|
220
|
-
|
221
|
-
when :noop
|
222
|
-
if Constancy.config.verbose?
|
223
|
-
puts "NO-OP!".bold + " #{item[:consul_key]}"
|
224
|
-
end
|
225
|
-
|
226
|
-
else
|
227
|
-
if Constancy.config.verbose?
|
228
|
-
STDERR.puts "WARNING: unexpected operation '#{item[:op]}' for #{item[:consul_key]}"
|
229
|
-
end
|
230
|
-
|
231
|
-
end
|
232
|
-
end
|
233
|
-
|
234
|
-
if self.items_to_create.count > 0
|
235
|
-
puts
|
236
|
-
puts "Keys to create: #{self.items_to_create.count}".bold
|
237
|
-
self.items_to_create.each do |item|
|
238
|
-
puts "+ #{item[:consul_key]}".green
|
239
|
-
end
|
240
|
-
end
|
241
|
-
|
242
|
-
if self.items_to_update.count > 0
|
243
|
-
puts
|
244
|
-
puts "Keys to update: #{self.items_to_update.count}".bold
|
245
|
-
self.items_to_update.each do |item|
|
246
|
-
puts "~ #{item[:consul_key]}".blue
|
247
|
-
end
|
248
|
-
end
|
249
|
-
|
250
|
-
if self.delete?
|
251
|
-
if self.items_to_delete.count > 0
|
252
|
-
puts
|
253
|
-
puts "Keys to delete: #{self.items_to_delete.count}".bold
|
254
|
-
self.items_to_delete.each do |item|
|
255
|
-
puts "- #{item[:consul_key]}".red
|
256
|
-
end
|
257
|
-
end
|
258
|
-
end
|
122
|
+
def diff(mode)
|
123
|
+
Constancy::Diff.new(target: self, local: self.local_items, remote: self.remote_items, mode: mode)
|
259
124
|
end
|
260
125
|
end
|
261
126
|
end
|
data/lib/constancy/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: constancy
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- David Adams
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2019-01-07 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: imperium
|
@@ -52,6 +52,20 @@ dependencies:
|
|
52
52
|
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '0.12'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '3.0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '3.0'
|
55
69
|
description: Syncs content from the filesystem to the Consul KV store.
|
56
70
|
email: daveadams@gmail.com
|
57
71
|
executables:
|
@@ -67,8 +81,10 @@ files:
|
|
67
81
|
- lib/constancy/cli.rb
|
68
82
|
- lib/constancy/cli/check_command.rb
|
69
83
|
- lib/constancy/cli/config_command.rb
|
84
|
+
- lib/constancy/cli/pull_command.rb
|
70
85
|
- lib/constancy/cli/push_command.rb
|
71
86
|
- lib/constancy/config.rb
|
87
|
+
- lib/constancy/diff.rb
|
72
88
|
- lib/constancy/sync_target.rb
|
73
89
|
- lib/constancy/version.rb
|
74
90
|
homepage: https://github.com/daveadams/constancy
|