consul_watcher 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/Dockerfile +13 -0
- data/Gemfile +8 -0
- data/README.md +33 -0
- data/Rakefile +55 -0
- data/VERSION +1 -0
- data/bin/console_watcher +14 -0
- data/bin/setup +8 -0
- data/consul_watcher.gemspec +28 -0
- data/docker-entrypoint.sh +25 -0
- data/docs/TODO.md +46 -0
- data/docs/destination/amqp.md +1 -0
- data/docs/destination/destination.md +2 -0
- data/docs/destination/jq.md +0 -0
- data/docs/docker-quickstart.md +30 -0
- data/docs/general_design.md +0 -0
- data/docs/messages_overview.md +61 -0
- data/docs/rake_tasks.md +0 -0
- data/docs/ruby-quickstart.md +0 -0
- data/docs/storage/consul.md +0 -0
- data/docs/storage/disk.md +2 -0
- data/docs/storage/storage.md +8 -0
- data/docs/watch_type/key.md +0 -0
- data/docs/watch_type/watch_type.md +7 -0
- data/exe/consul_watcher +24 -0
- data/lib/consul_watcher/class_helper.rb +19 -0
- data/lib/consul_watcher/destination/amqp.rb +29 -0
- data/lib/consul_watcher/destination/jq.rb +27 -0
- data/lib/consul_watcher/diff.rb +27 -0
- data/lib/consul_watcher/storage/consul.rb +13 -0
- data/lib/consul_watcher/storage/disk.rb +33 -0
- data/lib/consul_watcher/watch_type/checks.rb +28 -0
- data/lib/consul_watcher/watch_type/key.rb +40 -0
- data/lib/consul_watcher.rb +47 -0
- metadata +148 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 84e80ff2e7d93407b1fbf14774a8cb4d585962468da5a15e8c491a22408d3de9
|
4
|
+
data.tar.gz: 227c86658315bcbe920e6110b3818a3a7879b7899809c5f095293d085d7656e7
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 992881948d116b0ef750143056cc2d0595ea25795c6866e69ff3c28910d9939383f8eaf468401146a6e403fd5cfa39c294261141d1716253471dd3b2c0c2ec20
|
7
|
+
data.tar.gz: 9c6eaf4fd1c6c1e98eba0e2f640d83cb602e064de618cdc57280bd50a5a0f0bf7d2a15c6b4415474f7a33adf99d3b65c6039d609a6919eebb0d3bf3921c0c372
|
data/.gitignore
ADDED
data/Dockerfile
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
FROM consul:1.4.0
|
2
|
+
|
3
|
+
ARG gem_file
|
4
|
+
|
5
|
+
RUN apk add --no-cache jq build-base libc-dev linux-headers postgresql-dev libxml2-dev libxslt-dev ruby ruby-dev ruby-rdoc ruby-irb
|
6
|
+
|
7
|
+
COPY ./docker-entrypoint.sh /
|
8
|
+
COPY pkg/$gem_file /consul_watcher/
|
9
|
+
|
10
|
+
RUN cd /consul_watcher ; \
|
11
|
+
gem install $gem_file
|
12
|
+
|
13
|
+
ENTRYPOINT ["/docker-entrypoint.sh"]
|
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
# NOTICE
|
2
|
+
This gem is a work in progress. Please see the [TODO](https://github.com/fortman/consul_watcher/blob/master/docs/TODO.md) documentation
|
3
|
+
|
4
|
+
|
5
|
+
## Note on naming
|
6
|
+
This project name is a little confusing in that it called `consul_watcher`. It is wrapping the `consul watch` command line tool from Hashicorp. To avoid confusion, this project in the context of both the docker image and ruby gem will be referred to as `consul_watcher`. Any time that `consul watch` is used, that is a reference to the actualy Hashicorp command.
|
7
|
+
|
8
|
+
## Basic functionality
|
9
|
+
This git project produces a docker image. The basic premise of this docker image is to
|
10
|
+
* parse the json output of the [consul watch](https://www.consul.io/docs/commands/watch.html) command
|
11
|
+
* compare the output to the previous consul watch output
|
12
|
+
* send the diff to a destination
|
13
|
+
The primary destination at this time is an AMQP topic exchange. The underlying logic in the docker image is a ruby gem. To make the gem modular and extensible, the functionality has been broken up into 3 sections; storage, watch_type, and destination.
|
14
|
+
|
15
|
+
If we want to do a diff of consul watches, we need to store the previous consul watch json. This is because consul watches kick off new executions on every change, so we can't store the previous run in memory. Storage is accomplished with [storage classes](https://github.com/fortman/consul_watcher/blob/master/docs/storage/storage.md). The purpose of the storage class is to just store and retrieve previous consul watch json. At this time, backend storage is planned for both local filesystem, and consul kv storage.
|
16
|
+
|
17
|
+
The next type of class, is the [watch_type class](https://github.com/fortman/consul_watcher/blob/master/docs/watch_type/watch_type.md). This maps to the actual consul watch command `--type` flag. There are two unfortunate things about the way consul watch commands work. The first is that there is not one consul watch command that can watch for all changes. Each different type of watch requires its own definition. It would be nice to specify one watch that can monitor for changes in all consul areas. This is not the case however. The lack of a single definition bleeds into the next issue which is, each watch type has a very distinct json output format. For the watch json output, instead of having meta data that describes what type of watch the output represents, it is expected that you already know the format because you have to pass the type to the watch command. The entire purpose of the watch_type class is to handle the unique json output for each consul watch type. For every option to the consul watch --type flag, there is a watch_type class. The one exception to this is that `key` and `keyprefix` types share the same output. There is just one `key` watch_type class to represent both.
|
18
|
+
|
19
|
+
After the watch_type class handles the data, it will create a json diff between the previous run and the current. This json diff is the [message](https://github.com/fortman/consul_watcher/blob/master/docs/messages_overview.md) that is sent to the [destinations classes](https://github.com/fortman/consul_watcher/blob/master/docs/destination/destination.md). The initial implementation will support two destination classes; AMQP topic exchanges, and stdout through [jq](https://stedolan.github.io/jq/).
|
20
|
+
|
21
|
+
## Quick start guide
|
22
|
+
The quickest and easiest way to get started is to follow the [docker quickstart](https://github.com/fortman/consul_watcher/blob/master/docs/docker-quickstart.md).
|
23
|
+
|
24
|
+
If you are planning to write your own extension for storage/watch_type/destination, then you can check out the [ruby quickstart](https://github.com/fortman/consul_watcher/blob/master/docs/ruby-quickstart.md).
|
25
|
+
|
26
|
+
## Development
|
27
|
+
Please read the [general design documentation](https://github.com/fortman/consul_watcher/blob/master/docs/general_design.md). More detailed architectual documenation will be created in the future. All development [build and test commands](https://github.com/fortman/consul_watcher/blob/master/docs/rake_tasks.md) are defined as rake tasks.
|
28
|
+
|
29
|
+
## Contributing
|
30
|
+
|
31
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/fortman/consul_watcher.
|
32
|
+
|
33
|
+
PS. I swear I'm done changing the project name :)
|
data/Rakefile
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/gem_tasks'
|
4
|
+
require 'open3'
|
5
|
+
|
6
|
+
spec_file = Gem::Specification.load('consul_watcher.gemspec')
|
7
|
+
|
8
|
+
task default: :docker_build
|
9
|
+
|
10
|
+
task :docker_tag, [:version, :docker_image_id] do |_task, args|
|
11
|
+
puts "Docker id #{args['docker_image_id']} => tag rfortman/consul_watcher:#{args['version']}"
|
12
|
+
tag_cmd = "docker tag #{args['docker_image_id']} rfortman/consul_watcher:#{args['version']}"
|
13
|
+
Open3.popen3(tag_cmd) do |_stdin, _stdout, stderr, wait_thr|
|
14
|
+
error = stderr.read
|
15
|
+
puts error unless wait_thr.value.success?
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
task docker_build: [:build] do
|
20
|
+
docker_image_id = nil
|
21
|
+
build_cmd = "docker build --build-arg gem_file=consul_watcher-#{spec_file.version}.gem ."
|
22
|
+
threads = []
|
23
|
+
Open3.popen3(build_cmd) do |_stdin, stdout, stderr, wait_thr|
|
24
|
+
{ out: stdout, err: stderr }.each do |key, stream|
|
25
|
+
threads << Thread.new do
|
26
|
+
until (raw_line = stream.gets).nil?
|
27
|
+
match = raw_line.match(/Successfully built (.*)$/i)
|
28
|
+
docker_image_id = match.captures[0] if match
|
29
|
+
puts raw_line.to_s
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
threads.each(&:join)
|
34
|
+
if wait_thr.value.success?
|
35
|
+
Rake::Task['docker_tag'].invoke(spec_file.version, docker_image_id)
|
36
|
+
Rake::Task['docker_tag'].reenable
|
37
|
+
Rake::Task['docker_tag'].invoke('latest', docker_image_id)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
task :test do
|
43
|
+
build_cmd = 'docker-compose --file test/docker-compose.yml up rabbitmq consul consul-watcher'
|
44
|
+
threads = []
|
45
|
+
Open3.popen3(build_cmd) do |_stdin, stdout, stderr, wait_thr|
|
46
|
+
{ out: stdout, err: stderr }.each do |key, stream|
|
47
|
+
threads << Thread.new do
|
48
|
+
until (raw_line = stream.gets).nil?
|
49
|
+
puts raw_line.to_s
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
threads.each(&:join)
|
54
|
+
end
|
55
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.0.1
|
data/bin/console_watcher
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'consul_watcher'
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require 'irb'
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
lib = File.expand_path('lib', '..')
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'consul_watcher'
|
8
|
+
spec.version = IO.read('VERSION').chomp
|
9
|
+
spec.authors = ['Ryan Fortman']
|
10
|
+
spec.email = ['r.fortman.dev@gmail.com']
|
11
|
+
|
12
|
+
spec.summary = 'Send consul watch events to an amqp.'
|
13
|
+
spec.homepage = 'https://github.com/fortman/consul_watcher'
|
14
|
+
|
15
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
16
|
+
f.match(%r{^(test|spec|features)/})
|
17
|
+
end
|
18
|
+
spec.bindir = 'exe'
|
19
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
20
|
+
spec.require_paths = ['lib']
|
21
|
+
|
22
|
+
# spec.add_development_dependency 'bundler', '~> 2.0'
|
23
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
24
|
+
spec.add_runtime_dependency 'bundler', '~> 2.0'
|
25
|
+
spec.add_dependency 'bunny', '~> 1.7.0'
|
26
|
+
spec.add_dependency 'hashdiff', '~> 0.3'
|
27
|
+
spec.add_dependency 'slop', '~> 4.6'
|
28
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
#!/bin/sh
|
2
|
+
|
3
|
+
[ "x${CONSUL_HTTP_ADDR}" == "x" ] && export CONSUL_HTTP_ADDR="http://127.0.0.1:8500"
|
4
|
+
[ "x${WATCH_SCRIPT}" == "x" ] && export WATCH_SCRIPT="/usr/bin/env jq"
|
5
|
+
|
6
|
+
echo
|
7
|
+
echo "CLI_ARGS: \"${@}\""
|
8
|
+
echo "WATCH_ARGS: \"${WATCH_ARGS}\""
|
9
|
+
echo "WATCH_SCRIPT: \"${WATCH_SCRIPT}\""
|
10
|
+
echo "CONSUL_HTTP_ADDR: \"${CONSUL_HTTP_ADDR}\""
|
11
|
+
echo "RUN_ONCE: \"${RUN_ONCE}\""
|
12
|
+
if [[ ${#} != 0 ]]; then
|
13
|
+
exec $@
|
14
|
+
elif [[ "x${WATCH_ARGS}" != "x" && "x${RUN_ONCE}" == "x" ]]; then
|
15
|
+
echo "consul watch ${WATCH_ARGS} ${WATCH_SCRIPT}"
|
16
|
+
echo
|
17
|
+
exec consul watch ${WATCH_ARGS} ${WATCH_SCRIPT}
|
18
|
+
elif [[ "x${WATCH_ARGS}" != "x" && "x${RUN_ONCE}" != "x" ]]; then
|
19
|
+
echo "consul watch ${WATCH_ARGS} | ${WATCH_SCRIPT}"
|
20
|
+
echo
|
21
|
+
exec consul watch ${WATCH_ARGS} | ${WATCH_SCRIPT}
|
22
|
+
else
|
23
|
+
echo "Don't know what to do, WATCH_ARGS not defined and no arguments passed in"
|
24
|
+
exit 1
|
25
|
+
fi
|
data/docs/TODO.md
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
|
2
|
+
# TODO:
|
3
|
+
|
4
|
+
#### Status abbrevations
|
5
|
+
|
6
|
+
| abbrevation | meaning |
|
7
|
+
| ----------- |:-------------------------------------------- |
|
8
|
+
| COMP | completed (per initial design) |
|
9
|
+
| FUNC | functional (working, but will have updates) |
|
10
|
+
| IP | in progress |
|
11
|
+
| PL | planned |
|
12
|
+
| PROP | proposed |
|
13
|
+
|
14
|
+
## High level tasks
|
15
|
+
| task | status |
|
16
|
+
|:---------------------------------- |:------ |
|
17
|
+
| Create documentation | IP |
|
18
|
+
| Publish docker image and ruby gem | IP |
|
19
|
+
| Write unit tests | PL |
|
20
|
+
|
21
|
+
## Functional ruby classes implementation
|
22
|
+
|
23
|
+
#### Storage class implementation status
|
24
|
+
|
25
|
+
| storage class | status |
|
26
|
+
| ------------- |:----------- |
|
27
|
+
| file system | FUNC |
|
28
|
+
| consul | PL |
|
29
|
+
|
30
|
+
#### Watch Type class implementation status
|
31
|
+
|
32
|
+
| watch_type class | status |
|
33
|
+
| ---------------- |:----------- |
|
34
|
+
| key/keyprefix | FUNC |
|
35
|
+
| checks | IP |
|
36
|
+
| services | planned |
|
37
|
+
| service | planned |
|
38
|
+
| nodes | planned |
|
39
|
+
| event | planned |
|
40
|
+
|
41
|
+
#### Destination class implementation status
|
42
|
+
|
43
|
+
| destination class | status |
|
44
|
+
| ----------------- |:----------- |
|
45
|
+
| AMQP | FUNC |
|
46
|
+
| JQ | FUNC |
|
@@ -0,0 +1 @@
|
|
1
|
+
# AMQP message destination
|
File without changes
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# Docker quickstart
|
2
|
+
|
3
|
+
The easiest way to get started with consul_watcher is with the docker image. The docker entry point will run a consul watch and then pipe the output to the ruby consul_watcher program. The behavior is mostly driven by passing environmental variables into docker. Environmental variables, command line parameters and a configuration file are being tested to determine which is best way to configure consul_watch. There is a docker compose file to test things out locally The following steps should have you working with a local cluster fairly quick. A rake task will be created to automate these steps at some point.
|
4
|
+
|
5
|
+
From the root of the git repository execute the following<br/>
|
6
|
+
:> `docker-compose --file test/docker-compose.yml up --no-start`<br/>
|
7
|
+
:> `docker-compose --file test/docker-compose.yml start consul rabbitmq`<br/>
|
8
|
+
Wait a bit of time for those services to start. 30 seconds should suffice.<br/>
|
9
|
+
Login to rabbitmq locally http://localhost:15672. Use guest/guest as password. <br/>
|
10
|
+
Create a queue and bind it to the amq.topic exchange. Use routing key `consul_watcher.key.#` for the bind.<br/>
|
11
|
+
:> `docker-compose --file test/docker-compose.yml start consul-watcher`<br/>
|
12
|
+
Login to consul locally http://localhost:8500. You should be able to start creating, updating and deleting entries in the kv store. Go back to rabbitmq and you should be seeing messages in the queue you created.<br/>
|
13
|
+
|
14
|
+
# environmental variables used by docker image
|
15
|
+
| environment variable | description | example value |
|
16
|
+
| -------------------- | ------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
|
17
|
+
| WATCH_ARGS | arguments passed into the consul watch command | --type keyprefix --prefix / |
|
18
|
+
| WATCH_SCRIPT | the output of the consul watch is piped to this command | /usr/bin/consul_watcher --config-file /etc/consul_watch/config.json --storage-name testing --watch-type key |
|
19
|
+
| RUN_ONCE | determins if the consul watch runs once, or multiple times | set to any string to enable `run once` mode. Unset/empty string means run continuously |
|
20
|
+
| CONSUL_HTTP_ADDR | used by consul, any consul environmental variables can be specified | http://localhost:8500 |
|
21
|
+
|
22
|
+
## consul_watcher command line arguments
|
23
|
+
| command line argument | description | example value |
|
24
|
+
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------- |
|
25
|
+
| --config-file (file) | reference to the configuration file, need to mount in as volume to docker | /etc/consul_watch/config.json |
|
26
|
+
| --watch-type (type) | must match the --watch-type value passed to consul, keyprefix is should just be specified as key | key/services/nodes/service/checks/event |
|
27
|
+
| --storage-name (name) | just a unqiue name for this watch. if you are storing multiple watches to the same backend, you should make each watch have a unique name | test-watch |
|
28
|
+
|
29
|
+
## Configuration file
|
30
|
+
|
File without changes
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# Messages
|
2
|
+
Messages are the json diffs that are sent to the destinations. Each watch_type will be unique because each json watch type has it's own json formatting. Please look the documentation for each watch type for an example of that types message format. The following is an overview of the shared properties that each watch type shares. As well as the actual json diff, there is a little bit of meta data included as well.
|
3
|
+
|
4
|
+
Example of a key watch message
|
5
|
+
```
|
6
|
+
{
|
7
|
+
"watch_type": "key",
|
8
|
+
"id": "key.test",
|
9
|
+
"diff": [
|
10
|
+
"~",
|
11
|
+
[
|
12
|
+
"test",
|
13
|
+
"Value"
|
14
|
+
],
|
15
|
+
"old_value",
|
16
|
+
"new_value"
|
17
|
+
]
|
18
|
+
}
|
19
|
+
```
|
20
|
+
#### watch_type
|
21
|
+
The top level of the data structure is a hash with 3 elements. `watch_type`, `id` and `diff`. `watch_type` should be obvious. It matches both the `--type` sent to the consul watch command, as well as the `--watch-type`. The example output above was generated by the following command;
|
22
|
+
|
23
|
+
`consul watch --type keyprefix --prefix / /usr/bin/consul_watcher --config-file /etc/consul_watch/config.json --storage-name testing --watch-type key`
|
24
|
+
|
25
|
+
Note that the `--type keyprefix` and `--type key` specified to the consul watch command share the same json output format. For these two, they both just use `--watch-type key` parameter for the consul_watcher ruby command.
|
26
|
+
|
27
|
+
#### id
|
28
|
+
This is a unique id that can be used to identify what change. For a `key` watch, this will result in an id of `key_(path to key that change)`.
|
29
|
+
|
30
|
+
#### diff
|
31
|
+
The comparision between the previous json and the current json is handled by a ruby gem called [hashdiff](https://github.com/liufengyun/hashdiff#diff). The `hashdiff` github page has a good explaination of the diff output. Because arrays can be unordered in the consul watch output, this can lead to false positives in diff changes. To avoid this, data manipulation is done in the consul_watcher logic to remove arrays. The json diff format can be summed up as follows.
|
32
|
+
|
33
|
+
The first element in the diff array is the operation. '+' refers to a new element. '-' refers to an element that was removed. '~' refers to an element that changed.
|
34
|
+
|
35
|
+
The number of elements for diff arrays of '+' and '-' are both size 3. The size for a '~' is size for.
|
36
|
+
|
37
|
+
'+' operation
|
38
|
+
The first element defines it as an addition operation, and will always be '+'
|
39
|
+
The second element is an array that represents the nested path inside the json to the key that was added
|
40
|
+
The third element is the value of the new key
|
41
|
+
|
42
|
+
'-' operation
|
43
|
+
The first element defines it as a removal operation, and will always be '-'
|
44
|
+
The second element is an array that represents the nested path inside the json to the key that was removed
|
45
|
+
The third element is the value of the key that was removed
|
46
|
+
|
47
|
+
'~' operation ( note the 4th array element )
|
48
|
+
The first element defines it as a modification operation, and will always be '~'
|
49
|
+
The second element is an array that represents the nested path inside the json to the key that was change
|
50
|
+
The third element is the previous value of the key that was changed
|
51
|
+
The fourth element is the new value of the key that was changed
|
52
|
+
|
53
|
+
#### Wrapping up
|
54
|
+
|
55
|
+
Summing up our example using this information:
|
56
|
+
* There was a modification to the key located at 'test' in the consul kv store
|
57
|
+
* This was represented in the consul watch json output as { "test": { "Value": "new value" } }
|
58
|
+
* The old value was a string of 'old_value'
|
59
|
+
* and the new value a string of 'new_value'
|
60
|
+
|
61
|
+
This will enable consumers of the messages to have all the information they need; the type of operation, the location of what changed, and old/new value(s)
|
data/docs/rake_tasks.md
ADDED
File without changes
|
File without changes
|
File without changes
|
@@ -0,0 +1,8 @@
|
|
1
|
+
# Storage classes
|
2
|
+
The storage classes are responsible for storing previous consul watch output. Even though multiple backends are supported, you will only ever have one backend enabled and configured at a time. This is needed so that future runs of consul watch can compare output with previous watch runs. The entire purpose of the consul_watcher project is to create this diff and send it to a destination.
|
3
|
+
|
4
|
+
## Disk storage class
|
5
|
+
The most basic form of storage is the [disk storage class](https://github.com/fortman/consul_watcher/blob/master/lib/consul_watcher/storage/disk.rb). The behavior of this class is driven by the configuration file. Please see the detailed [disk class documentation](https://github.com/fortman/consul_watcher/blob/master/docs/storage/disk.md) for configuration options.
|
6
|
+
|
7
|
+
## Consul kv storage class
|
8
|
+
Since we know for sure that consul is installed and running, it is probably a good idea to enable consul kv as a backend storage type. The main concern here is that we need to make sure we ignore the storage path from the consul watch, so we do not get into an infinite watch loop. The [consul storage class](https://github.com/fortman/consul_watcher/blob/master/lib/consul_watcher/storage/consul.rb) is responsible for reading and writing previous json runs in the consul kv store. Configuration for this is tricky as we need to drive the docker entrypoint with environmental variables. The docker entrypoint is what is running the consul watch command, so it needs access to consul environmental variables. The direction is to start using the configuration files for all things related to the ruby script. For now the consul_watcher will only look at the configuration file for consul backend settings. This will lead to duplicated settings (env vars and config file). The behavior might change in the future (allow env var references in config file for example).
|
File without changes
|
@@ -0,0 +1,7 @@
|
|
1
|
+
# watch_type classes
|
2
|
+
The watch_type classes have a few different functions. The first and main function is to create a json diff between consul watch runs. This diff will later be sent to a [destination class](https://github.com/fortman/consul_watcher/blob/master/docs/destination/destination.md). The second purpose of this class is to do some data translation. The consul watch json output is not ideal for doing a diff. A good example is that arrays in the output are un-ordered, and between runs can change position. This causes things to appear to have changed, when really all they did was change their order in the array. To get around this, the watch_type classes strive to obscure these non-changes and provide a good json format for diffs.
|
3
|
+
|
4
|
+
## key and keyprefix
|
5
|
+
Key and keyprefix have identical output format, so they will share a [watch_type class named key](https://github.com/fortman/consul_watcher/blob/master/lib/consul_watcher/watch_type/key.rb). Configuration is driven by the consul_watcher configuration file and [key specific configuration](https://github.com/fortman/consul_watcher/blob/master/docs/watch_type/key.md).
|
6
|
+
|
7
|
+
## More to come later, there will need to be watch_type for every single `consul watch --type`
|
data/exe/consul_watcher
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# frozen_string_literal: true
|
4
|
+
|
5
|
+
require 'slop'
|
6
|
+
require 'logger'
|
7
|
+
require 'json'
|
8
|
+
require 'consul_watcher'
|
9
|
+
|
10
|
+
logger = Logger.new(STDOUT)
|
11
|
+
|
12
|
+
opts = Slop.parse do |o|
|
13
|
+
o.string '--watch-type', required: true
|
14
|
+
o.string '--storage-name', required: true
|
15
|
+
o.string '--config-file', required: true
|
16
|
+
o.on '-h', '--help', 'print help' do
|
17
|
+
logger.warn("\n#{o}")
|
18
|
+
exit(false)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
config = JSON.parse(File.read(opts['config-file']))
|
23
|
+
|
24
|
+
ConsulWatcher.watch(opts['watch-type'], opts['storage-name'], config)
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ConsulWatcher
|
4
|
+
# Define methods to handle default initialization behavior
|
5
|
+
module ClassHelper
|
6
|
+
def populate_variables(config = {})
|
7
|
+
defaults.each_pair do |key, default_value|
|
8
|
+
key = key.to_s
|
9
|
+
if config[key]
|
10
|
+
instance_variable_set("@#{key}", config[key])
|
11
|
+
elsif ENV[key.upcase.to_s]
|
12
|
+
instance_variable_set("@#{key}", ENV[key.upcase.to_s])
|
13
|
+
else
|
14
|
+
instance_variable_set("@#{key}", default_value)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bunny'
|
4
|
+
|
5
|
+
module ConsulWatcher
|
6
|
+
module Destination
|
7
|
+
# Send diff output to jq command line
|
8
|
+
class Amqp
|
9
|
+
def initialize(destination_config)
|
10
|
+
@conn = Bunny.new(host: destination_config['rabbitmq']['server'] || 'localhost',
|
11
|
+
port: destination_config['rabbitmq']['port'] || '5672',
|
12
|
+
vhost: destination_config['rabbitmq']['vhost'] || '/',
|
13
|
+
username: destination_config['rabbitmq']['username'] || 'guest',
|
14
|
+
password: destination_config['rabbitmq']['password'] || 'guest')
|
15
|
+
@conn.start
|
16
|
+
@ch = @conn.create_channel
|
17
|
+
@ex = Bunny::Exchange.new(@ch,
|
18
|
+
:topic,
|
19
|
+
destination_config['rabbitmq']['exchange'] || 'amq.topic',
|
20
|
+
durable: true)
|
21
|
+
end
|
22
|
+
|
23
|
+
def send(change)
|
24
|
+
puts 'publishing message'
|
25
|
+
@ex.publish(change, routing_key: "consul_watcher.#{change['id']}")
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'open3'
|
4
|
+
|
5
|
+
module ConsulWatcher
|
6
|
+
module Destination
|
7
|
+
# Send diff output to jq command line
|
8
|
+
class Jq
|
9
|
+
def initialize(destination_config)
|
10
|
+
end
|
11
|
+
|
12
|
+
def send(change)
|
13
|
+
change_json = JSON.pretty_generate(change)
|
14
|
+
Open3.popen3("/usr/bin/env jq '.'") do |stdin, stdout, stderr, wait_thr|
|
15
|
+
stdin.puts "#{change_json}\r\n"
|
16
|
+
stdin.close
|
17
|
+
error = stderr.read
|
18
|
+
stderr.close
|
19
|
+
puts stdout.read
|
20
|
+
stdout.close
|
21
|
+
puts error unless wait_thr.value.success?
|
22
|
+
puts
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'hashdiff'
|
4
|
+
require 'logger'
|
5
|
+
|
6
|
+
module ConsulWatcher
|
7
|
+
class Diff
|
8
|
+
def initialize(parser_config) end
|
9
|
+
|
10
|
+
def parse(previous_watch_data, current_watch_data)
|
11
|
+
diff = HashDiff.diff(sanitize_data(previous_watch_data), sanitize_data(current_watch_data), array_path: true)
|
12
|
+
diff.each do |change|
|
13
|
+
puts "change: #{change}"
|
14
|
+
change[1] = change[1]&.join('/')
|
15
|
+
end
|
16
|
+
puts "hashdiff: #{diff}"
|
17
|
+
diff
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def sanitize_data(data)
|
23
|
+
data = '[]' if data.nil? || data == "null\n"
|
24
|
+
data
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# This will be a module to store previous consul watch json to compare with previous watch data
|
4
|
+
module ConsulWatcher
|
5
|
+
module Storage
|
6
|
+
# Consul storage for previous watch data
|
7
|
+
class Disk
|
8
|
+
def initialize(storage_config)
|
9
|
+
@parent_dir = storage_config['storage_parent_dir'] || '/tmp'
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ConsulWatcher
|
4
|
+
module Storage
|
5
|
+
# Disk storage for previous watch data
|
6
|
+
class Disk
|
7
|
+
def initialize(storage_config)
|
8
|
+
@parent_dir = storage_config['storage_parent_dir'] || '/tmp'
|
9
|
+
end
|
10
|
+
|
11
|
+
def fetch(watch_name)
|
12
|
+
file = File.open(cache_file_name(watch_name), mode: 'r')
|
13
|
+
data = file.read
|
14
|
+
file.close
|
15
|
+
data
|
16
|
+
rescue Errno::ENOENT
|
17
|
+
'{}'
|
18
|
+
end
|
19
|
+
|
20
|
+
def push(watch_name, data)
|
21
|
+
file = File.open(cache_file_name(watch_name), mode: 'w')
|
22
|
+
file.write(data)
|
23
|
+
file.close
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def cache_file_name(watch_name)
|
29
|
+
"#{@parent_dir}/#{watch_name}.json"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'open3'
|
4
|
+
|
5
|
+
module ConsulWatcher
|
6
|
+
module WatchType
|
7
|
+
class Checks
|
8
|
+
def initialize(destination_config) end
|
9
|
+
|
10
|
+
def get_changes(previous_watch_json, current_watch_json)
|
11
|
+
HashDiff.diff(json_to_hash(previous_watch_json),
|
12
|
+
json_to_hash(current_watch_json),
|
13
|
+
array_path: true)
|
14
|
+
end
|
15
|
+
|
16
|
+
def id(change)
|
17
|
+
"key.#{change[1][0].tr('/', '.')}"
|
18
|
+
end
|
19
|
+
|
20
|
+
def json_to_hash(json)
|
21
|
+
json = '{}' if json.nil? || json == "null\n"
|
22
|
+
JSON.parse(json).map do |check|
|
23
|
+
{ check['Node'] => { check['CheckID'] => check } }
|
24
|
+
end.reduce({}, :merge)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'base64'
|
4
|
+
|
5
|
+
module ConsulWatcher
|
6
|
+
module WatchType
|
7
|
+
class Key
|
8
|
+
def initialize(destination_config) end
|
9
|
+
|
10
|
+
def get_changes(previous_watch_json, current_watch_json)
|
11
|
+
json_diff = HashDiff.diff(json_to_hash(previous_watch_json),
|
12
|
+
json_to_hash(current_watch_json),
|
13
|
+
array_path: true)
|
14
|
+
json_diff.each.collect do |change|
|
15
|
+
# change[1] = change[1].join('/')
|
16
|
+
change[2] = Base64.decode64(change[2]) if change[2].is_a? String
|
17
|
+
change[3] = Base64.decode64(change[3]) if change[3].is_a? String
|
18
|
+
{
|
19
|
+
'watch_type' => 'key',
|
20
|
+
'id' => id(change),
|
21
|
+
'diff' => change
|
22
|
+
}
|
23
|
+
end
|
24
|
+
#json_diff.reject { |change| change[0] == '~' && change[1][-1] == 'ModifyIndex' }
|
25
|
+
end
|
26
|
+
|
27
|
+
def id(change)
|
28
|
+
"key.#{change[1][0].tr('/', '.')}"
|
29
|
+
end
|
30
|
+
|
31
|
+
def json_to_hash(json)
|
32
|
+
json = '{}' if json.nil? || json == "null\n"
|
33
|
+
JSON.parse(json).map do |kv|
|
34
|
+
{ kv['Key'] => kv.reject { |key, _value| key == 'Key' } }
|
35
|
+
end.reduce({}, :merge)
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# frozen_string_literal: true
|
4
|
+
|
5
|
+
require 'json'
|
6
|
+
require 'hashdiff'
|
7
|
+
|
8
|
+
# Top Level module to run watch logic
|
9
|
+
module ConsulWatcher
|
10
|
+
def self.watch(watch_type, storage_name, config)
|
11
|
+
storage = get_storage(config['storage']) if config['storage']
|
12
|
+
watch_type = get_watch_type(config['watch_type']) if config['watch_type']
|
13
|
+
filters = get_filters(config['filters']) if config['filters']
|
14
|
+
destination = get_destination(config['destination']) if config['destination']
|
15
|
+
|
16
|
+
current_watch_json = $stdin.read
|
17
|
+
previous_watch_json = storage.fetch(storage_name)
|
18
|
+
changes = watch_type.get_changes(previous_watch_json, current_watch_json)
|
19
|
+
changes.each do |change|
|
20
|
+
destination.send(change)
|
21
|
+
end
|
22
|
+
storage.push(storage_name, current_watch_json)
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.get_storage(storage_config)
|
26
|
+
classname = storage_config['classname'] || 'ConsulWatcher::Storage::Disk'
|
27
|
+
require classname_to_file(classname)
|
28
|
+
Object.const_get(classname).new(storage_config)
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.get_watch_type(watch_type_config)
|
32
|
+
classname = watch_type_config['classname'] || 'ConsulWatcher::WatchType::Checks'
|
33
|
+
require classname_to_file(classname)
|
34
|
+
Object.const_get(classname).new(watch_type_config)
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.get_destination(destination_config)
|
38
|
+
classname = destination_config['classname'] || 'ConsulWatcher::Destination::Jq'
|
39
|
+
require classname_to_file(classname)
|
40
|
+
Object.const_get(classname).new(destination_config)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Dynamically require handler class from passed in handler class
|
44
|
+
def self.classname_to_file(classname)
|
45
|
+
classname.gsub('::', '/').gsub(/([a-zA-Z])([A-Z])/, '\1_\2').downcase
|
46
|
+
end
|
47
|
+
end
|
metadata
ADDED
@@ -0,0 +1,148 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: consul_watcher
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Ryan Fortman
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-04-14 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rake
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '10.0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '10.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '2.0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '2.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: bunny
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 1.7.0
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 1.7.0
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: hashdiff
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0.3'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0.3'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: slop
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '4.6'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '4.6'
|
83
|
+
description:
|
84
|
+
email:
|
85
|
+
- r.fortman.dev@gmail.com
|
86
|
+
executables:
|
87
|
+
- consul_watcher
|
88
|
+
extensions: []
|
89
|
+
extra_rdoc_files: []
|
90
|
+
files:
|
91
|
+
- ".gitignore"
|
92
|
+
- Dockerfile
|
93
|
+
- Gemfile
|
94
|
+
- README.md
|
95
|
+
- Rakefile
|
96
|
+
- VERSION
|
97
|
+
- bin/console_watcher
|
98
|
+
- bin/setup
|
99
|
+
- consul_watcher.gemspec
|
100
|
+
- docker-entrypoint.sh
|
101
|
+
- docs/TODO.md
|
102
|
+
- docs/destination/amqp.md
|
103
|
+
- docs/destination/destination.md
|
104
|
+
- docs/destination/jq.md
|
105
|
+
- docs/docker-quickstart.md
|
106
|
+
- docs/general_design.md
|
107
|
+
- docs/messages_overview.md
|
108
|
+
- docs/rake_tasks.md
|
109
|
+
- docs/ruby-quickstart.md
|
110
|
+
- docs/storage/consul.md
|
111
|
+
- docs/storage/disk.md
|
112
|
+
- docs/storage/storage.md
|
113
|
+
- docs/watch_type/key.md
|
114
|
+
- docs/watch_type/watch_type.md
|
115
|
+
- exe/consul_watcher
|
116
|
+
- lib/consul_watcher.rb
|
117
|
+
- lib/consul_watcher/class_helper.rb
|
118
|
+
- lib/consul_watcher/destination/amqp.rb
|
119
|
+
- lib/consul_watcher/destination/jq.rb
|
120
|
+
- lib/consul_watcher/diff.rb
|
121
|
+
- lib/consul_watcher/storage/consul.rb
|
122
|
+
- lib/consul_watcher/storage/disk.rb
|
123
|
+
- lib/consul_watcher/watch_type/checks.rb
|
124
|
+
- lib/consul_watcher/watch_type/key.rb
|
125
|
+
homepage: https://github.com/fortman/consul_watcher
|
126
|
+
licenses: []
|
127
|
+
metadata: {}
|
128
|
+
post_install_message:
|
129
|
+
rdoc_options: []
|
130
|
+
require_paths:
|
131
|
+
- lib
|
132
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
133
|
+
requirements:
|
134
|
+
- - ">="
|
135
|
+
- !ruby/object:Gem::Version
|
136
|
+
version: '0'
|
137
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
138
|
+
requirements:
|
139
|
+
- - ">="
|
140
|
+
- !ruby/object:Gem::Version
|
141
|
+
version: '0'
|
142
|
+
requirements: []
|
143
|
+
rubyforge_project:
|
144
|
+
rubygems_version: 2.7.8
|
145
|
+
signing_key:
|
146
|
+
specification_version: 4
|
147
|
+
summary: Send consul watch events to an amqp.
|
148
|
+
test_files: []
|