constancy 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []