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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 95dfa86b6395376d98383dccb8aaf492a34bea246f8fda4b084144f3bc95e297
4
- data.tar.gz: 233a2fcf66cdce878d38cb25f49404c30381c6fe6d9c202e704978fa743303fb
3
+ metadata.gz: 3119c8dc521f42f974a1f708273026eb49f59c19e25867408aae150dc83261ac
4
+ data.tar.gz: f6c729bfd5bbf17e29ecd21570076dcf212ae5af999b5f5d89f629216d1c33b0
5
5
  SHA512:
6
- metadata.gz: '09a3aa2bab4129d27abf9bc6f404fbeed92ef754a1a11876422cbbe8bbbc2127eec94c73d1feb4103ae082a56a5c498daccb5e604f31525f70dc0ef7796e76a8'
7
- data.tar.gz: d884c7212d308784b756a3f2900a6658cfc7c766584b7c7f236191f89649e997e0c2d46c2f370e3f8cd6be76f5ff537b32c5ac03019be20697dbd93af1116206
6
+ metadata.gz: 1928aead12f4a255cc9e8595d3c024b115a30b950fdcefe5df3ab50d35b122ba8340ed7a95f4db15a36b04f8b8f124d10e00467000088d78a1e6e88e72d0c3e0
7
+ data.tar.gz: f1d991706a3e0883bf2b22f9e53588a1c6472a55376fb424c5ead455a5c704d99bc6966d378deda7d07c648c606941fb5499fa23affd837104e4e1e513362265
data/README.md CHANGED
@@ -1,11 +1,12 @@
1
1
  # constancy
2
2
 
3
- Constancy provides simple filesystem-to-Consul KV synchronization.
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 directory
150
- # containing the files with content to synchronize to Consul.
151
- # This path is calculated relative to the directory containing
152
- # the configuration file.
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 very new software. There's more to be done. Some ideas:
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
@@ -27,4 +27,6 @@ Gem::Specification.new do |s|
27
27
  s.add_dependency 'imperium', '~>0.3'
28
28
  s.add_dependency 'diffy', '~>3.2'
29
29
  s.add_dependency 'vault', '~>0.12'
30
+
31
+ s.add_development_dependency 'rspec', '~> 3.0'
30
32
  end
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 colors
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.print_report
12
- if not target.any_changes?
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 " File path: #{target.path}"
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.print_report
12
+ diff = target.diff(:push)
13
13
 
14
- if not target.any_changes?
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
- target.items_to_change.each do |item|
33
- case item[:op]
34
+ diff.items_to_change.each do |item|
35
+ case item.op
34
36
  when :create
35
- print "CREATE".bold.green + " " + item[:consul_key]
36
- resp = target.consul.put(item[:consul_key], item[:local_content], dc: target.datacenter)
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[:consul_key]
45
- resp = target.consul.put(item[:consul_key], item[:local_content], dc: target.datacenter)
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[:consul_key]
54
- resp = target.consul.delete(item[:consul_key], dc: target.datacenter)
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[:op]}' for #{item[:consul_key]}"
65
+ STDERR.puts "constancy: WARNING: unexpected operation '#{item.op}' for #{item.consul_key}"
64
66
  next
65
67
  end
66
68
 
@@ -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 for a sync target entry: #{Constancy::SyncTarget::REQUIRED_CONFIG_KEYS.join(", ")}")
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
- "#{self.name.nil? ? '' : self.name.bold + "\n"}#{'local'.blue}:#{self.path} => #{'consul'.cyan}:#{self.datacenter.green}:#{self.prefix}"
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
- @base_dir = nil
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 base_dir
60
- @base_dir ||= File.join(Constancy.config.base_dir, self.path)
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.base_dir}/**{,/*/**}/*"].select { |f| File.file?(f) }
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.local_files.each do |local_file|
73
- @local_items[local_file.sub(%r{^#{self.base_dir}/?}, '')] = if self.chomp?
74
- File.read(local_file).chomp.force_encoding(Encoding::ASCII_8BIT)
75
- else
76
- File.read(local_file).force_encoding(Encoding::ASCII_8BIT)
77
- end
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 = self.local_items
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
@@ -1,5 +1,5 @@
1
1
  # This software is public domain. No rights are reserved. See LICENSE for more information.
2
2
 
3
3
  class Constancy
4
- VERSION = "0.2.2"
4
+ VERSION = "0.3.0"
5
5
  end
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.2.2
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: 2018-12-25 00:00:00.000000000 Z
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