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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +198 -0
- data/bin/constancy +8 -0
- data/constancy.gemspec +29 -0
- data/lib/constancy.rb +72 -0
- data/lib/constancy/cli.rb +129 -0
- data/lib/constancy/cli/check_command.rb +21 -0
- data/lib/constancy/cli/config_command.rb +44 -0
- data/lib/constancy/cli/push_command.rb +74 -0
- data/lib/constancy/config.rb +161 -0
- data/lib/constancy/sync_target.rb +259 -0
- data/lib/constancy/version.rb +5 -0
- metadata +84 -0
checksums.yaml
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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.
|
data/bin/constancy
ADDED
data/constancy.gemspec
ADDED
@@ -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
|
data/lib/constancy.rb
ADDED
@@ -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
|
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: []
|