constancy 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,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 72ea5278404520809d073eab084fe0a33f71221e
4
+ data.tar.gz: 8345a952b62e73266a2266045adb5a7bcdc9a99f
5
+ SHA512:
6
+ metadata.gz: f89c7c2246deed0012d258e8e5b3d8903332d68f8bfad49e74915a74eebabc68b604e8cef9d382d2bddce24316575dde0f73cefb62b41800cb0b3e869bbc9f85
7
+ data.tar.gz: 697665d664aef7b355ba2975d0edb0c87a2cde795dee4b1cca346ac69feff10832b51bb8fa75e88714fb35591eebf1476272b10a8bcf11360a4d6953da5e0fe5
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2017 Instructure, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,198 @@
1
+ # constancy
2
+
3
+ Constancy provides simple filesystem-to-Consul KV synchronization.
4
+
5
+ ## Basic Usage
6
+
7
+ Run `constancy check` to see what differences exist, and `constancy push` to
8
+ synchronize the changes.
9
+
10
+ $ constancy check
11
+ =====================================================================================
12
+ myapp-private
13
+ local:consul/private => consul:dc1:private/myapp
14
+ Keys scanned: 37
15
+ No changes to make for this sync target.
16
+
17
+ =====================================================================================
18
+ myapp-config
19
+ local:consul/config => consul:dc1:config/myapp
20
+ Keys scanned: 80
21
+
22
+ UPDATE config/myapp/prod/ip-whitelist.json
23
+ -------------------------------------------------------------------------------------
24
+ -["10.8.0.0/16"]
25
+ +["10.8.0.0/16","10.9.10.0/24"]
26
+ -------------------------------------------------------------------------------------
27
+
28
+ Keys to update: 1
29
+ ~ config/myapp/prod/ip-whitelist.json
30
+
31
+ You can also limit your command to specific synchronization targets by using
32
+ the `--target` flag:
33
+
34
+ $ constancy push --target myapp-config
35
+ =====================================================================================
36
+ myapp-config
37
+ local:consul/config => consul:dc1:config/myapp
38
+ Keys scanned: 80
39
+
40
+ UPDATE config/myapp/prod/ip-whitelist.json
41
+ -------------------------------------------------------------------------------------
42
+ -["10.8.0.0/16"]
43
+ +["10.8.0.0/16","10.9.10.0/24"]
44
+ -------------------------------------------------------------------------------------
45
+
46
+ Keys to update: 1
47
+ ~ config/myapp/prod/ip-whitelist.json
48
+
49
+ Do you want to push these changes?
50
+ Enter 'yes' to continue: yes
51
+
52
+ UPDATE config/myapp/prod/ip-whitelist.json OK
53
+
54
+ Run `constancy --help` for additional options and commands.
55
+
56
+
57
+ ## Configuration
58
+
59
+ Constancy will automatically configure itself using the first `constancy.yml`
60
+ file it comes across when searching backwards through the directory tree from
61
+ the current working directory. So, typically you may wish to place the config
62
+ file in the root of your git repository or the base directory of your config
63
+ file tree.
64
+
65
+ You can also specify a config file using the `--config <filename>` command line
66
+ argument.
67
+
68
+
69
+ ### Configuration file structure
70
+
71
+ The configuration file is a Hash represented in YAML format with three possible
72
+ top-level keys: `constancy`, `consul`, and `sync`. The `constancy` section sets
73
+ global defaults and app options. The `consul` section specifies the URL to the
74
+ Consul REST API endpoint. And the `sync` section lists the directories and
75
+ Consul prefixes you wish to synchronize. Only the `sync` section is strictly
76
+ required. An example `constancy.yml` is below including explanatory comments:
77
+
78
+ # constancy.yml
79
+
80
+ constancy:
81
+ # verbose - defaults to `false`
82
+ # Set this to `true` for more verbose output.
83
+ verbose: false
84
+
85
+ # chomp - defaults to `true`
86
+ # Automatically runs `chomp` on the strings read in from files to
87
+ # eliminate a single trailing newline character (commonly inserted
88
+ # by text editors). Set to `false` to disable this by default for
89
+ # all sync targets (it can be overridden on a per-target basis).
90
+ chomp: true
91
+
92
+ # delete - defaults to `false`
93
+ # Set this to `true` to make the default for all sync targets to
94
+ # delete any keys found in Consul that do not have a corresponding
95
+ # file on disk. By default, extraneous remote keys will be ignored.
96
+ # If `verbose` is set to `true` the extraneous keys will be named
97
+ # in the output.
98
+ delete: false
99
+
100
+ # color - defaults to `true`
101
+ # Set this to `false` to disable colorized output (eg when running
102
+ # with an automated tool).
103
+ color: true
104
+
105
+ consul:
106
+ # url - defaults to `http://localhost:8500`
107
+ # The REST API endpoint for the Consul agent.
108
+ url: http://localhost:8500
109
+
110
+ # datacenter - defaults to nil
111
+ # Set this to change the default datacenter for sync targets to
112
+ # something other than the datacenter of the Consul agent.
113
+ datacenter: dc1
114
+
115
+ sync:
116
+ # sync is an array of hashes of sync target configurations
117
+ # Fields:
118
+ # name - The arbitrary friendly name of the sync target. Only
119
+ # required if you wish to target specific sync targets using
120
+ # the `--target` CLI flag.
121
+ # prefix - (required) The Consul KV prefix to synchronize to.
122
+ # datacenter - The Consul datacenter to synchronize to. If not
123
+ # specified, the `datacenter` setting in the `consul` section
124
+ # will be used. If that is also not specified, the sync will
125
+ # happen with the local datacenter of the Consul agent.
126
+ # path - (required) The relative filesystem path to the directory
127
+ # containing the files with content to synchronize to Consul.
128
+ # This path is calculated relative to the directory containing
129
+ # the configuration file.
130
+ # delete - Whether or not to delete remote keys that do not exist
131
+ # in the local filesystem. This inherits the setting from the
132
+ # `constancy` section, or if not specified, defaults to `false`.
133
+ # chomp - Whether or not to chomp a single newline character off
134
+ # the contents of local files before synchronizing to Consul.
135
+ # This inherits the setting from the `constancy` section, or if
136
+ # not specified, defaults to `true`.
137
+ # exclude - An array of Consul KV paths to exclude from the
138
+ # sync process. These exclusions will be noted in output if the
139
+ # verbose mode is in effect, otherwise they will be silently
140
+ # ignored. At this time there is no provision for specifying
141
+ # prefixes or patterns. Each key must be fully and explicitly
142
+ # specified.
143
+ - name: myapp-config
144
+ prefix: config/myapp
145
+ datacenter: dc1
146
+ path: consul/config
147
+ exclude:
148
+ - config/myapp/beta/cowboy-yolo
149
+ - config/myapp/prod/cowboy-yolo
150
+ - name: myapp-private
151
+ prefix: private/myapp
152
+ datacenter: dc1
153
+ path: consul/private
154
+ delete: true
155
+
156
+ You can run `constancy config` to get a summary of the defined configuration
157
+ and to double-check config syntax.
158
+
159
+
160
+ ### Environment configuration
161
+
162
+ Constancy may be partially configured using environment variables:
163
+ * `CONSTANCY_VERBOSE` - set this variable to any value to enable verbose mode
164
+ * `CONSUL_HTTP_TOKEN` or `CONSUL_TOKEN` - use one of these variables (priority
165
+ is given to `CONSUL_HTTP_TOKEN`) to set an explicit Consul token to use when
166
+ interacting with the API. Otherwise, by default the agent's `acl_token`
167
+ setting is used implicitly.
168
+
169
+
170
+ ## Automation
171
+
172
+ For version 0.1, Constancy does not fully support running non-interactively.
173
+ This is primarily to ensure human observation of any changes being made while
174
+ the software matures. Later versions will allow for full automation.
175
+
176
+
177
+ ## Roadmap
178
+
179
+ Constancy is very new software. There's more to be done. Some ideas:
180
+
181
+ * Pattern- and prefix-based exclusions
182
+ * Other commands to assist in managing Consul KV sets
183
+ * Automation support for running non-interactively
184
+ * Git awareness (branches, commit state, etc)
185
+ * Automated tests
186
+ * Logging of changes to files, syslog, other services
187
+ * Allowing other means of providing a Consul token
188
+
189
+
190
+ ## Contributing
191
+
192
+ I'm happy to accept suggestions, bug reports, and pull requests through Github.
193
+
194
+
195
+ ## License
196
+
197
+ This software is public domain. No rights are reserved. See LICENSE for more
198
+ information.
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # This software is public domain. No rights are reserved. See LICENSE for more information.
4
+ #
5
+
6
+ require 'constancy/cli'
7
+
8
+ Constancy::CLI.run
@@ -0,0 +1,29 @@
1
+ require_relative 'lib/constancy/version.rb'
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = 'constancy'
5
+ s.version = Constancy::VERSION
6
+ s.authors = ['David Adams']
7
+ s.email = 'daveadams@gmail.com'
8
+ s.date = Time.now.strftime('%Y-%m-%d')
9
+ s.license = 'CC0'
10
+ s.homepage = 'https://github.com/daveadams/constancy'
11
+ s.required_ruby_version = '>=2.4.0'
12
+
13
+ s.summary = 'Simple filesystem-to-Consul KV synchronization'
14
+ s.description =
15
+ 'Syncs content from the filesystem to the Consul KV store.'
16
+
17
+ s.require_paths = ['lib']
18
+ s.files = Dir["lib/**/*.rb"] + [
19
+ 'bin/constancy',
20
+ 'README.md',
21
+ 'LICENSE',
22
+ 'constancy.gemspec'
23
+ ]
24
+ s.bindir = 'bin'
25
+ s.executables = ['constancy']
26
+
27
+ s.add_dependency 'imperium', '~>0.3'
28
+ s.add_dependency 'diffy', '~>3.2'
29
+ end
@@ -0,0 +1,72 @@
1
+ # This software is public domain. No rights are reserved. See LICENSE for more information.
2
+
3
+ require 'yaml'
4
+ require 'imperium'
5
+
6
+ require 'constancy/version'
7
+ require 'constancy/config'
8
+ require 'constancy/sync_target'
9
+
10
+ class Constancy
11
+ class << self
12
+ @@config = nil
13
+
14
+ def config
15
+ @@config ||= Constancy::Config.new
16
+ end
17
+
18
+ def configure(path: nil, targets: nil)
19
+ @@config = Constancy::Config.new(path: path, targets: targets)
20
+ end
21
+
22
+ def configured?
23
+ not @@config.nil?
24
+ end
25
+ end
26
+
27
+ class Util
28
+ class << self
29
+ # https://stackoverflow.com/questions/9647997/converting-a-nested-hash-into-a-flat-hash
30
+ def flatten_hash(h,f=[],g={})
31
+ return g.update({ f=>h }) unless h.is_a? Hash
32
+ h.each { |k,r| flatten_hash(r,f+[k],g) }
33
+ g
34
+ end
35
+ end
36
+ end
37
+ end
38
+
39
+ # monkeypatch String for colors
40
+ class String
41
+ def colorize(s,e=0)
42
+ Constancy.config.color? ? "\e[#{s}m#{self}\e[#{e}m" : self
43
+ end
44
+
45
+ def red
46
+ colorize(31)
47
+ end
48
+
49
+ def green
50
+ colorize(32)
51
+ end
52
+
53
+ def blue
54
+ colorize(34)
55
+ end
56
+
57
+ def magenta
58
+ colorize(35)
59
+ end
60
+
61
+ def cyan
62
+ colorize(36)
63
+ end
64
+
65
+ def gray
66
+ colorize(37)
67
+ end
68
+
69
+ def bold
70
+ colorize(1,22)
71
+ end
72
+ end
@@ -0,0 +1,129 @@
1
+ # This software is public domain. No rights are reserved. See LICENSE for more information.
2
+
3
+ require 'constancy'
4
+ require 'diffy'
5
+ require 'constancy/cli/check_command'
6
+ require 'constancy/cli/push_command'
7
+ require 'constancy/cli/config_command'
8
+
9
+ class Constancy
10
+ class CLI
11
+ class << self
12
+ attr_accessor :command, :cli_mode, :config_file, :extra_args, :targets
13
+
14
+ def parse_args(args)
15
+ self.print_usage if args.count < 1
16
+ self.command = nil
17
+ self.config_file = nil
18
+ self.extra_args = []
19
+ self.cli_mode = :command
20
+
21
+ while arg = args.shift
22
+ case arg
23
+ when "--help"
24
+ self.cli_mode = :help
25
+
26
+ when "--config"
27
+ self.config_file = args.shift
28
+
29
+ when "--target"
30
+ self.targets = (args.shift||'').split(",")
31
+
32
+ when /^-/
33
+ # additional option, maybe for the command
34
+ self.extra_args << arg
35
+
36
+ else
37
+ if self.command.nil?
38
+ # if command is not set, this is probably the command
39
+ self.command = arg
40
+ else
41
+ # otherwise, pass it thru to the child command
42
+ self.extra_args << arg
43
+ end
44
+ end
45
+ end
46
+ end
47
+
48
+ def print_usage
49
+ STDERR.puts <<USAGE
50
+ Usage:
51
+ #{File.basename($0)} <command> [options]
52
+
53
+ Commands:
54
+ check Print a summary of changes to be made
55
+ push Push changes from filesystem to Consul
56
+ config Print a summary of the active configuration
57
+
58
+ General options:
59
+ --help Print help for the given command
60
+ --config <file> Use the specified config file
61
+ --target <tgt> Only apply to the specified target name or names (comma-separated)
62
+
63
+ USAGE
64
+ exit 1
65
+ end
66
+
67
+ def configure
68
+ return if Constancy.configured?
69
+
70
+ begin
71
+ Constancy.configure(path: self.config_file, targets: self.targets)
72
+
73
+ rescue Constancy::ConfigFileNotFound
74
+ if self.config_file.nil?
75
+ STDERR.puts "constancy: ERROR: No configuration file found"
76
+ else
77
+ STDERR.puts "constancy: ERROR: Configuration file '#{self.config_file}' was not found"
78
+ end
79
+ exit 1
80
+
81
+ rescue Constancy::ConfigFileInvalid => e
82
+ if self.config_file.nil?
83
+ STDERR.puts "constancy: ERROR: Configuration file is invalid:"
84
+ else
85
+ STDERR.puts "constancy: ERROR: Configuration file '#{self.config_file}' is invalid:"
86
+ end
87
+ STDERR.puts " #{e}"
88
+ exit 1
89
+ end
90
+
91
+ if Constancy.config.sync_targets.count < 1
92
+ if self.targets.nil?
93
+ STDERR.puts "constancy: WARNING: No sync targets are defined"
94
+ else
95
+ STDERR.puts "constancy: WARNING: No sync targets were found that matched the specified list"
96
+ end
97
+ end
98
+ end
99
+
100
+ def run
101
+ self.parse_args(ARGV)
102
+
103
+ case self.cli_mode
104
+ when :help
105
+ # TODO: per-command help
106
+ self.print_usage
107
+
108
+ when :command
109
+ case self.command
110
+ when 'check' then Constancy::CLI::CheckCommand.run
111
+ when 'push' then Constancy::CLI::PushCommand.run
112
+ when 'config' then Constancy::CLI::ConfigCommand.run
113
+ when nil then self.print_usage
114
+
115
+ else
116
+ STDERR.puts "constancy: ERROR: unknown command '#{self.command}'"
117
+ STDERR.puts
118
+ self.print_usage
119
+ end
120
+
121
+ else
122
+ STDERR.puts "constancy: ERROR: unknown CLI mode '#{self.cli_mode}'"
123
+ exit 1
124
+
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,21 @@
1
+ # This software is public domain. No rights are reserved. See LICENSE for more information.
2
+
3
+ class Constancy
4
+ class CLI
5
+ class CheckCommand
6
+ class << self
7
+ def run
8
+ Constancy::CLI.configure
9
+
10
+ Constancy.config.sync_targets.each do |target|
11
+ target.print_report
12
+ if not target.any_changes?
13
+ puts "No changes to make for this sync target."
14
+ end
15
+ puts
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,44 @@
1
+ # This software is public domain. No rights are reserved. See LICENSE for more information.
2
+
3
+ class Constancy
4
+ class CLI
5
+ class ConfigCommand
6
+ class << self
7
+ def run
8
+ Constancy::CLI.configure
9
+
10
+ puts "Config file: #{Constancy.config.config_file}"
11
+ puts " Consul URL: #{Constancy.config.consul.url}"
12
+ puts " Verbose: #{Constancy.config.verbose?.to_s.bold}"
13
+ puts
14
+ puts "Sync target defaults:"
15
+ puts " Chomp trailing newlines from local files: #{Constancy.config.chomp?.to_s.bold}"
16
+ puts " Delete remote keys with no local file: #{Constancy.config.delete?.to_s.bold}"
17
+ puts
18
+ puts "Sync targets:"
19
+
20
+ Constancy.config.sync_targets.each do |target|
21
+ if target.name
22
+ puts "* #{target.name.bold}"
23
+ print ' '
24
+ else
25
+ print '*'
26
+ end
27
+ puts " Datacenter: #{target.datacenter}"
28
+ puts " File path: #{target.path}"
29
+ puts " Prefix: #{target.prefix}"
30
+ puts " Autochomp? #{target.chomp?}"
31
+ puts " Delete? #{target.delete?}"
32
+ if not target.exclude.empty?
33
+ puts " Exclusions:"
34
+ target.exclude.each do |exclusion|
35
+ puts " - #{exclusion}"
36
+ end
37
+ end
38
+ puts
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,74 @@
1
+ # This software is public domain. No rights are reserved. See LICENSE for more information.
2
+
3
+ class Constancy
4
+ class CLI
5
+ class PushCommand
6
+ class << self
7
+ def run
8
+ Constancy::CLI.configure
9
+ STDOUT.sync = true
10
+
11
+ Constancy.config.sync_targets.each do |target|
12
+ target.print_report
13
+
14
+ if not target.any_changes?
15
+ puts
16
+ puts "Everything is in sync. No changes need to be made to this sync target."
17
+ next
18
+ end
19
+
20
+ puts
21
+ puts "Do you want to push these changes?"
22
+ print " Enter '" + "yes".bold + "' to continue: "
23
+ answer = gets.chomp
24
+
25
+ if answer.downcase != "yes"
26
+ puts
27
+ puts "Push cancelled. No changes will be made to this sync target."
28
+ next
29
+ end
30
+
31
+ puts
32
+ target.items_to_change.each do |item|
33
+ case item[:op]
34
+ 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
+ if resp.success?
38
+ puts " OK".bold
39
+ else
40
+ puts " ERROR".bold.red
41
+ end
42
+
43
+ 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
+ if resp.success?
47
+ puts " OK".bold
48
+ else
49
+ puts " ERROR".bold.red
50
+ end
51
+
52
+ when :delete
53
+ print "DELETE".bold.red + " " + item[:consul_key]
54
+ resp = target.consul.delete(item[:consul_key], dc: target.datacenter)
55
+ if resp.success?
56
+ puts " OK".bold
57
+ else
58
+ puts " ERROR".bold.red
59
+ end
60
+
61
+ else
62
+ if Constancy.config.verbose?
63
+ STDERR.puts "constancy: WARNING: unexpected operation '#{item[:op]}' for #{item[:consul_key]}"
64
+ next
65
+ end
66
+
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,161 @@
1
+ # This software is public domain. No rights are reserved. See LICENSE for more information.
2
+
3
+ class Constancy
4
+ class ConfigFileNotFound < RuntimeError; end
5
+ class ConfigFileInvalid < RuntimeError; end
6
+
7
+ class Config
8
+ CONFIG_FILENAMES = %w( constancy.yml )
9
+ VALID_CONFIG_KEYS = %w( sync consul constancy )
10
+ VALID_CONSUL_CONFIG_KEYS = %w( url datacenter )
11
+ VALID_CONSTANCY_CONFIG_KEYS = %w( verbose chomp delete color )
12
+ DEFAULT_CONSUL_URL = "http://localhost:8500"
13
+
14
+ attr_accessor :config_file, :base_dir, :consul, :sync_targets, :target_whitelist
15
+
16
+ class << self
17
+ # discover the nearest config file
18
+ def discover(dir: nil)
19
+ dir ||= Dir.pwd
20
+
21
+ CONFIG_FILENAMES.each do |filename|
22
+ full_path = File.join(dir, filename)
23
+ if File.exist?(full_path)
24
+ return full_path
25
+ end
26
+ end
27
+
28
+ dir == "/" ? nil : self.discover(dir: File.dirname(dir))
29
+ end
30
+ end
31
+
32
+ def initialize(path: nil, targets: nil)
33
+ if path.nil? or File.directory?(path)
34
+ self.config_file = Constancy::Config.discover(dir: path)
35
+ elsif File.exist?(path)
36
+ self.config_file = path
37
+ else
38
+ raise Constancy::ConfigFileNotFound.new
39
+ end
40
+
41
+ if self.config_file.nil? or not File.exist?(self.config_file) or not File.readable?(self.config_file)
42
+ raise Constancy::ConfigFileNotFound.new
43
+ end
44
+
45
+ self.config_file = File.expand_path(self.config_file)
46
+ self.base_dir = File.dirname(self.config_file)
47
+ self.target_whitelist = targets
48
+ parse!
49
+ end
50
+
51
+ def verbose?
52
+ @is_verbose
53
+ end
54
+
55
+ def chomp?
56
+ @do_chomp
57
+ end
58
+
59
+ def delete?
60
+ @do_delete
61
+ end
62
+
63
+ def color?
64
+ @use_color
65
+ end
66
+
67
+ private
68
+
69
+ def parse!
70
+ raw = {}
71
+ begin
72
+ raw = YAML.load(File.read(self.config_file))
73
+ rescue
74
+ raise Constancy::ConfigFileInvalid.new("Unable to parse config file as YAML")
75
+ end
76
+
77
+ if raw.is_a? FalseClass
78
+ # this generally means an empty config file
79
+ raw = {}
80
+ end
81
+
82
+ if not raw.is_a? Hash
83
+ raise Constancy::ConfigFileInvalid.new("Config file must form a hash")
84
+ end
85
+
86
+ if (raw.keys - Constancy::Config::VALID_CONFIG_KEYS) != []
87
+ raise Constancy::ConfigFileInvalid.new("Only the following keys are valid at the top level of the config: #{Constancy::Config::VALID_CONFIG_KEYS.join(", ")}")
88
+ end
89
+
90
+ raw['consul'] ||= {}
91
+ if not raw['consul'].is_a? Hash
92
+ raise Constancy::ConfigFileInvalid.new("'consul' must be a hash")
93
+ end
94
+
95
+ if (raw['consul'].keys - Constancy::Config::VALID_CONSUL_CONFIG_KEYS) != []
96
+ raise Constancy::ConfigFileInvalid.new("Only the following keys are valid in the consul config: #{Constancy::Config::VALID_CONSUL_CONFIG_KEYS.join(", ")}")
97
+ end
98
+
99
+ consul_url = raw['consul']['url'] || Constancy::Config::DEFAULT_CONSUL_URL
100
+ consul_token = ENV['CONSUL_HTTP_TOKEN'] || ENV['CONSUL_TOKEN']
101
+ self.consul = Imperium::Configuration.new(url: consul_url, token: consul_token)
102
+
103
+ raw['constancy'] ||= {}
104
+ if not raw['constancy'].is_a? Hash
105
+ raise Constancy::ConfigFileInvalid.new("'constancy' must be a hash")
106
+ end
107
+
108
+ if (raw['constancy'].keys - Constancy::Config::VALID_CONSTANCY_CONFIG_KEYS) != []
109
+ raise Constancy::ConfigFileInvalid.new("Only the following keys are valid in the 'constancy' config block: #{Constancy::Config::VALID_CONSTANCY_CONFIG_KEYS.join(", ")}")
110
+ end
111
+
112
+ # verbose: default false
113
+ @is_verbose = raw['constancy']['verbose'] ? true : false
114
+ if ENV['CONSTANCY_VERBOSE']
115
+ @is_verbose = true
116
+ end
117
+
118
+ # chomp: default true
119
+ if raw['constancy'].has_key?('chomp')
120
+ @do_chomp = raw['constancy']['chomp'] ? true : false
121
+ else
122
+ @do_chomp = true
123
+ end
124
+
125
+ # delete: default false
126
+ @do_delete = raw['constancy']['delete'] ? true : false
127
+
128
+ raw['sync'] ||= []
129
+ if not raw['sync'].is_a? Array
130
+ raise Constancy::ConfigFileInvalid.new("'sync' must be an array")
131
+ end
132
+
133
+ # color: default true
134
+ if raw['constancy'].has_key?('color')
135
+ @use_color = raw['constancy']['color'] ? true : false
136
+ else
137
+ @use_color = true
138
+ end
139
+
140
+ self.sync_targets = []
141
+ raw['sync'].each do |target|
142
+ if target.is_a? Hash
143
+ target['datacenter'] ||= raw['consul']['datacenter']
144
+ target['chomp'] ||= self.chomp?
145
+ target['delete'] ||= self.delete?
146
+ end
147
+
148
+ if not self.target_whitelist.nil?
149
+ # unnamed targets cannot be whitelisted
150
+ next if target['name'].nil?
151
+
152
+ # named targets must be on the whitelist
153
+ next if not self.target_whitelist.include?(target['name'])
154
+ end
155
+
156
+ self.sync_targets << Constancy::SyncTarget.new(config: target, imperium_config: self.consul)
157
+ end
158
+
159
+ end
160
+ end
161
+ end
@@ -0,0 +1,259 @@
1
+ # This software is public domain. No rights are reserved. See LICENSE for more information.
2
+
3
+ class Constancy
4
+ class SyncTarget
5
+ VALID_CONFIG_KEYS = %w( name datacenter prefix path exclude chomp delete )
6
+ attr_accessor :name, :datacenter, :prefix, :path, :exclude, :consul
7
+
8
+ REQUIRED_CONFIG_KEYS = %w( prefix )
9
+
10
+ def initialize(config:, imperium_config:)
11
+ if not config.is_a? Hash
12
+ raise Constancy::ConfigFileInvalid.new("Sync target entries must be specified as hashes")
13
+ end
14
+
15
+ if (config.keys - Constancy::SyncTarget::VALID_CONFIG_KEYS) != []
16
+ raise Constancy::ConfigFileInvalid.new("Only the following keys are valid in a sync target entry: #{Constancy::SyncTarget::VALID_CONFIG_KEYS.join(", ")}")
17
+ end
18
+
19
+ 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(", ")}")
21
+ end
22
+
23
+ self.datacenter = config['datacenter']
24
+ self.prefix = config['prefix']
25
+ self.path = config['path'] || config['prefix']
26
+ self.name = config['name']
27
+ self.exclude = config['exclude'] || []
28
+ if config.has_key?('chomp')
29
+ @do_chomp = config['chomp'] ? true : false
30
+ end
31
+ if config.has_key?('delete')
32
+ @do_delete = config['delete'] ? true : false
33
+ else
34
+ @do_delete = false
35
+ end
36
+
37
+ self.consul = Imperium::KV.new(imperium_config)
38
+ end
39
+
40
+ def chomp?
41
+ @do_chomp
42
+ end
43
+
44
+ def delete?
45
+ @do_delete
46
+ end
47
+
48
+ def description
49
+ "#{self.name.nil? ? '' : self.name.bold + "\n"}#{'local'.blue}:#{self.path} => #{'consul'.cyan}:#{self.datacenter.green}:#{self.prefix}"
50
+ end
51
+
52
+ def clear_cache
53
+ @base_dir = nil
54
+ @local_files = nil
55
+ @local_items = nil
56
+ @remote_items = nil
57
+ end
58
+
59
+ def base_dir
60
+ @base_dir ||= File.join(Constancy.config.base_dir, self.path)
61
+ end
62
+
63
+ def local_files
64
+ @local_files ||= Dir["#{self.base_dir}/**/*"].select { |f| File.file?(f) }
65
+ end
66
+
67
+ def local_items
68
+ return @local_items if not @local_items.nil?
69
+ @local_items = {}
70
+
71
+ self.local_files.each do |local_file|
72
+ @local_items[local_file.sub(%r{^#{self.base_dir}/?}, '')] = if self.chomp?
73
+ File.read(local_file).chomp
74
+ else
75
+ File.read(local_file)
76
+ end
77
+ end
78
+
79
+ @local_items
80
+ end
81
+
82
+ def remote_items
83
+ return @remote_items if not @remote_items.nil?
84
+ @remote_items = {}
85
+
86
+ resp = self.consul.get(self.prefix, :recurse, dc: self.datacenter)
87
+
88
+ Constancy::Util.flatten_hash(resp.values).each_pair do |key, value|
89
+ @remote_items[key.join("/")] = value
90
+ end
91
+
92
+ @remote_items
93
+ end
94
+
95
+ def diff
96
+ local = self.local_items
97
+ remote = self.remote_items
98
+ all_keys = (local.keys + remote.keys).sort.uniq
99
+
100
+ all_keys.collect do |key|
101
+ excluded = false
102
+ op = :noop
103
+ if remote.has_key?(key) and not local.has_key?(key)
104
+ if self.delete?
105
+ op = :delete
106
+ else
107
+ op = :ignore
108
+ end
109
+ elsif local.has_key?(key) and not remote.has_key?(key)
110
+ op = :create
111
+ else
112
+ if remote[key] == local[key]
113
+ op = :noop
114
+ else
115
+ op = :update
116
+ end
117
+ end
118
+
119
+ consul_key = [self.prefix, key].compact.join("/")
120
+
121
+ if self.exclude.include?(key) or self.exclude.include?(consul_key)
122
+ op = :ignore
123
+ excluded = true
124
+ end
125
+
126
+ {
127
+ :op => op,
128
+ :excluded => excluded,
129
+ :relative_path => key,
130
+ :filename => File.join(self.base_dir, key),
131
+ :consul_key => consul_key,
132
+ :local_content => local[key],
133
+ :remote_content => remote[key],
134
+ }
135
+ end
136
+ end
137
+
138
+ def items_to_delete
139
+ self.diff.select { |d| d[:op] == :delete }
140
+ end
141
+
142
+ def items_to_update
143
+ self.diff.select { |d| d[:op] == :update }
144
+ end
145
+
146
+ def items_to_create
147
+ self.diff.select { |d| d[:op] == :create }
148
+ end
149
+
150
+ def items_to_ignore
151
+ self.diff.select { |d| d[:op] == :ignore }
152
+ end
153
+
154
+ def items_to_exclude
155
+ self.diff.select { |d| d[:op] == :ignore and d[:excluded] == true }
156
+ end
157
+
158
+ def items_to_noop
159
+ self.diff.select { |d| d[:op] == :noop }
160
+ end
161
+
162
+ def items_to_change
163
+ self.diff.select { |d| [:delete, :update, :create].include?(d[:op]) }
164
+ end
165
+
166
+ def any_changes?
167
+ self.items_to_change.count > 0
168
+ end
169
+
170
+ def print_report
171
+ puts '='*85
172
+ puts self.description
173
+
174
+ puts " Keys scanned: #{self.diff.count}"
175
+ if Constancy.config.verbose?
176
+ puts " Keys ignored: #{self.items_to_ignore.count}"
177
+ puts " Keys in sync: #{self.items_to_noop.count}"
178
+ end
179
+
180
+ puts if self.any_changes?
181
+
182
+ self.diff.each do |item|
183
+ case item[:op]
184
+ when :create
185
+ puts "CREATE".bold.green + " #{item[:consul_key]}"
186
+ puts '-'*85
187
+ # simulate diff but without complaints about line endings
188
+ item[:local_content].each_line do |line|
189
+ puts "+#{line.chomp}".green
190
+ end
191
+ puts '-'*85
192
+
193
+ when :update
194
+ puts "UPDATE".bold + " #{item[:consul_key]}"
195
+ puts '-'*85
196
+ puts Diffy::Diff.new(item[:remote_content], item[:local_content]).to_s(:color)
197
+ puts '-'*85
198
+
199
+ when :delete
200
+ if self.delete?
201
+ puts "DELETE".bold.red + " #{item[:consul_key]}"
202
+ puts '-'*85
203
+ # simulate diff but without complaints about line endings
204
+ item[:remote_content].each_line do |line|
205
+ puts "-#{line.chomp}".red
206
+ end
207
+ puts '-'*85
208
+ else
209
+ if Constancy.config.verbose?
210
+ puts "IGNORE".bold + " #{item[:consul_key]}"
211
+ end
212
+ end
213
+
214
+ when :ignore
215
+ if Constancy.config.verbose?
216
+ puts "IGNORE".bold + " #{item[:consul_key]}"
217
+ end
218
+
219
+ when :noop
220
+ if Constancy.config.verbose?
221
+ puts "NO-OP!".bold + " #{item[:consul_key]}"
222
+ end
223
+
224
+ else
225
+ if Constancy.config.verbose?
226
+ STDERR.puts "WARNING: unexpected operation '#{item[:op]}' for #{item[:consul_key]}"
227
+ end
228
+
229
+ end
230
+ end
231
+
232
+ if self.items_to_create.count > 0
233
+ puts
234
+ puts "Keys to create: #{self.items_to_create.count}".bold
235
+ self.items_to_create.each do |item|
236
+ puts "+ #{item[:consul_key]}".green
237
+ end
238
+ end
239
+
240
+ if self.items_to_update.count > 0
241
+ puts
242
+ puts "Keys to update: #{self.items_to_update.count}".bold
243
+ self.items_to_update.each do |item|
244
+ puts "~ #{item[:consul_key]}".blue
245
+ end
246
+ end
247
+
248
+ if self.delete?
249
+ if self.items_to_delete.count > 0
250
+ puts
251
+ puts "Keys to delete: #{self.items_to_delete.count}".bold
252
+ self.items_to_delete.each do |item|
253
+ puts "- #{item[:consul_key]}".red
254
+ end
255
+ end
256
+ end
257
+ end
258
+ end
259
+ end
@@ -0,0 +1,5 @@
1
+ # This software is public domain. No rights are reserved. See LICENSE for more information.
2
+
3
+ class Constancy
4
+ VERSION = "0.1.0"
5
+ end
metadata ADDED
@@ -0,0 +1,84 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: constancy
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - David Adams
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-02-01 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: imperium
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.3'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.3'
27
+ - !ruby/object:Gem::Dependency
28
+ name: diffy
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3.2'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3.2'
41
+ description: Syncs content from the filesystem to the Consul KV store.
42
+ email: daveadams@gmail.com
43
+ executables:
44
+ - constancy
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - LICENSE
49
+ - README.md
50
+ - bin/constancy
51
+ - constancy.gemspec
52
+ - lib/constancy.rb
53
+ - lib/constancy/cli.rb
54
+ - lib/constancy/cli/check_command.rb
55
+ - lib/constancy/cli/config_command.rb
56
+ - lib/constancy/cli/push_command.rb
57
+ - lib/constancy/config.rb
58
+ - lib/constancy/sync_target.rb
59
+ - lib/constancy/version.rb
60
+ homepage: https://github.com/daveadams/constancy
61
+ licenses:
62
+ - CC0
63
+ metadata: {}
64
+ post_install_message:
65
+ rdoc_options: []
66
+ require_paths:
67
+ - lib
68
+ required_ruby_version: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: 2.4.0
73
+ required_rubygems_version: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ requirements: []
79
+ rubyforge_project:
80
+ rubygems_version: 2.6.13
81
+ signing_key:
82
+ specification_version: 4
83
+ summary: Simple filesystem-to-Consul KV synchronization
84
+ test_files: []