consul_watcher 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
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
@@ -0,0 +1,8 @@
1
+ source "https://rubygems.org"
2
+
3
+ repo_name = '/fortman/consul_watcher'
4
+
5
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
6
+
7
+ # Specify your gem's dependencies in consul_watcher.gemspec
8
+ gemspec
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
@@ -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,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -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
@@ -0,0 +1,2 @@
1
+ # Consul Watcher Destinations
2
+ The basic premise of consul_watcher is to monitor consul via a consul watch command. The json that is generated by consul watch, and then send a `diff`
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)
File without changes
File without changes
File without changes
@@ -0,0 +1,2 @@
1
+ # Disk Storage for consul watch output
2
+ storage_parent_dir
@@ -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`
@@ -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: []