flapjack_configurator 1.0.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/.gitignore +2 -0
- data/.rubocop.yml +47 -0
- data/Dockerfile +11 -0
- data/Gemfile +3 -0
- data/README.md +139 -0
- data/Rakefile +19 -0
- data/bin/flapjack_configurator +82 -0
- data/example.yml +121 -0
- data/flapjack_configurator.gemspec +29 -0
- data/lib/flapjack_configurator.rb +32 -0
- data/lib/flapjack_configurator/entity_mapper.rb +70 -0
- data/lib/flapjack_configurator/flapjack_config.rb +72 -0
- data/lib/flapjack_configurator/flapjack_contact.rb +156 -0
- data/lib/flapjack_configurator/flapjack_media.rb +23 -0
- data/lib/flapjack_configurator/flapjack_notification_rule.rb +39 -0
- data/lib/flapjack_configurator/flapjack_object_base.rb +86 -0
- data/lib/flapjack_configurator/flapjack_pagerduty.rb +28 -0
- data/lib/flapjack_configurator/flapjack_sub_object_base.rb +33 -0
- data/lib/flapjack_configurator/user_configuration.rb +107 -0
- data/lib/flapjack_configurator/version.rb +6 -0
- data/spec/docker_test_wrapper.rb +52 -0
- data/spec/functional/all_entity_spec.rb +19 -0
- data/spec/functional/config_test_common.rb +58 -0
- data/spec/functional/configuration_contact_attributes_spec.rb +18 -0
- data/spec/functional/configuration_contact_entities_spec.rb +116 -0
- data/spec/functional/configuration_contact_notification_media_spec.rb +73 -0
- data/spec/functional/configuration_contact_notification_rules_spec.rb +58 -0
- data/spec/functional/configuration_contact_removal_spec.rb +83 -0
- data/spec/functional/test_configs/changes/attributes.yaml +24 -0
- data/spec/functional/test_configs/changes/notification_media.yaml +155 -0
- data/spec/functional/test_configs/changes/notification_rules.yaml +143 -0
- data/spec/functional/test_configs/entities.yaml +71 -0
- data/spec/functional/test_configs/initial/attributes.yaml +24 -0
- data/spec/functional/test_configs/initial/notification_media.yaml +155 -0
- data/spec/functional/test_configs/initial/notification_rules.yaml +143 -0
- data/spec/functional/test_configs/obj_removal_setup.yaml +106 -0
- data/spec/spec_helper.rb +9 -0
- metadata +211 -0
@@ -0,0 +1,28 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require_relative 'flapjack_sub_object_base.rb'
|
4
|
+
|
5
|
+
module FlapjackConfigurator
|
6
|
+
# Class representing Pagerduty credentials
|
7
|
+
# In Flapjack 1.x Pagerduty is somewhat duct-taped to the side of the thing and not handled as media.
|
8
|
+
# However, to make our lives easier, make this class look like FlapjackMedia so that it can be handled like a media entry
|
9
|
+
class FlapjackPagerduty < FlapjackSubObjectBase
|
10
|
+
def initialize(current_config, diner, logger)
|
11
|
+
# The contact ID is essentially the pagerduty credentials ID; 1-1
|
12
|
+
# Pull the ID from the config. Contacts is an array but in practice it only appears to ever be single-element.
|
13
|
+
conf_id = current_config.nil? ? nil : current_config[:links][:contacts][0]
|
14
|
+
super(conf_id, current_config, diner.method(:pagerduty_credentials), diner.method(:create_contact_pagerduty_credentials),
|
15
|
+
diner.method(:update_pagerduty_credentials), diner.method(:delete_pagerduty_credentials), logger, 'pagerduty')
|
16
|
+
@allowed_config_keys = [:subdomain, :token, :service_key]
|
17
|
+
end
|
18
|
+
|
19
|
+
def create(contact_id, config)
|
20
|
+
_create(contact_id, _filter_config(config))
|
21
|
+
end
|
22
|
+
|
23
|
+
# Type helper to match FlapjackMedia
|
24
|
+
def type
|
25
|
+
return 'pagerduty'
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require_relative 'flapjack_object_base.rb'
|
4
|
+
|
5
|
+
module FlapjackConfigurator
|
6
|
+
# Class representing a Flapjack sub-object (media, pagerduty creds, etc...)
|
7
|
+
class FlapjackSubObjectBase < FlapjackObjectBase
|
8
|
+
# Create the object
|
9
|
+
def _create(contact_id, config)
|
10
|
+
fail("Object #{id} exists") if @obj_exists
|
11
|
+
# AFAIK there is not an easy way to convert hash keys to symbols outside of Rails
|
12
|
+
sym_config = {}
|
13
|
+
config.each { |k, v| sym_config[k.to_sym] = v }
|
14
|
+
|
15
|
+
msg_id = id.nil? ? sym_config[:type] : id
|
16
|
+
@logger.info("Creating #{@log_name} #{msg_id} for contact #{contact_id}")
|
17
|
+
@logger.debug("#{@log_name} #{id} config: #{sym_config}")
|
18
|
+
fail "Failed to create #{@log_name} #{id}" unless @create_method.call(contact_id, sym_config)
|
19
|
+
_reload_config
|
20
|
+
end
|
21
|
+
|
22
|
+
def _filter_config(config)
|
23
|
+
filtered_config = config.select { |k, _| @allowed_config_keys.include? k.to_sym }
|
24
|
+
@logger.debug("#{@log_name} #{id}: Config keys filtered out: #{config.keys - filtered_config.keys}")
|
25
|
+
return filtered_config
|
26
|
+
end
|
27
|
+
|
28
|
+
# Update the media from a config hash of updated values
|
29
|
+
def update(config)
|
30
|
+
return _update(_filter_config(config))
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'deep_merge'
|
4
|
+
require_relative 'entity_mapper.rb'
|
5
|
+
|
6
|
+
module FlapjackConfigurator
|
7
|
+
# User Configuration: Class representing the desired configuration as passed into the utility
|
8
|
+
class UserConfiguration
|
9
|
+
attr_reader :config, :entity_map
|
10
|
+
|
11
|
+
def initialize(config, diner, logger)
|
12
|
+
@config = config
|
13
|
+
@logger = logger
|
14
|
+
@media_config = {}
|
15
|
+
|
16
|
+
_sanity_check
|
17
|
+
|
18
|
+
@entity_map = EntityMapper.new(self, diner)
|
19
|
+
end
|
20
|
+
|
21
|
+
def _sanity_check
|
22
|
+
# Check that required keys are present
|
23
|
+
fail('Config missing contacts block') unless @config.key? 'contacts'
|
24
|
+
@config['contacts'].each do |contact_id, contact_val|
|
25
|
+
%w(details notification_media notification_rules).each do |contact_opt|
|
26
|
+
fail("#{contact_id} contact config missing #{contact_opt} block") unless contact_val.key? contact_opt
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def contact_ids
|
32
|
+
@config['contacts'].keys
|
33
|
+
end
|
34
|
+
|
35
|
+
def contact_config(contact_id)
|
36
|
+
return nil unless @config['contacts'].key? contact_id
|
37
|
+
|
38
|
+
# Merge in defaults for keys which may be omitted
|
39
|
+
return {
|
40
|
+
'entities' => { 'exact' => [], 'regex' => [] },
|
41
|
+
'entities_blacklist' => { 'exact' => [], 'regex' => [] }
|
42
|
+
}.deep_merge(@config['contacts'][contact_id])
|
43
|
+
end
|
44
|
+
|
45
|
+
# Return a list of contacts with the default bit set
|
46
|
+
# This is pretty entitymapper centric, but it makes more sense here due to current layout.
|
47
|
+
def default_contacts
|
48
|
+
@config['contacts'].select { |_, contact| contact.key? 'entities' }.select { |_, c| c['entities']['default'] }.keys
|
49
|
+
end
|
50
|
+
|
51
|
+
def baseline_config
|
52
|
+
if @config.key? 'baseline_options'
|
53
|
+
return @config['baseline_options']
|
54
|
+
else
|
55
|
+
return {}
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def _complete_config_merge(contact_id, config_key)
|
60
|
+
contact_settings = contact_config(contact_id)[config_key]
|
61
|
+
fail("Missing #{config_key} settings for contact #{contact_id}") if contact_settings.nil?
|
62
|
+
|
63
|
+
baseline_opts = baseline_config.key?(config_key) ? baseline_config[config_key] : {}
|
64
|
+
contact_defaults = contact_settings.key?('defaults') ? contact_settings['defaults'] : {}
|
65
|
+
|
66
|
+
merged_config = {}
|
67
|
+
(contact_settings.keys - %w(defaults)).each do |key|
|
68
|
+
# Only merge baseline/defaults if the contact has the setting defined
|
69
|
+
# This is to prevent errors from partial configs built from only partial defaults.
|
70
|
+
if baseline_opts.key? key
|
71
|
+
merged_config[key] = baseline_opts[key].merge(contact_defaults.merge(contact_settings[key]))
|
72
|
+
else
|
73
|
+
merged_config[key] = contact_defaults.merge(contact_settings[key])
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
@logger.debug("#{contact_id} #{config_key} complete config: #{merged_config}")
|
78
|
+
return merged_config
|
79
|
+
end
|
80
|
+
|
81
|
+
def media(contact_id)
|
82
|
+
if @media_config[contact_id].nil?
|
83
|
+
@media_config[contact_id] = _complete_config_merge(contact_id, 'notification_media')
|
84
|
+
end
|
85
|
+
return @media_config[contact_id]
|
86
|
+
end
|
87
|
+
|
88
|
+
def notification_rules(contact_id)
|
89
|
+
notification_rules = _complete_config_merge(contact_id, 'notification_rules')
|
90
|
+
|
91
|
+
# Double check that the defined rules call for media which exists
|
92
|
+
notification_rules.each do |nr_id, nr_val|
|
93
|
+
%w(warning_media critical_media unknown_media).each do |alert_type|
|
94
|
+
next unless nr_val.key? alert_type
|
95
|
+
nr_val[alert_type].each do |alert_media|
|
96
|
+
unless media(contact_id).keys.include? alert_media
|
97
|
+
@logger.warn("Notification rule #{nr_id} for contact #{contact_id} calls for media #{alert_media} in #{alert_type} which isn't defined for #{contact_id}")
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# The notification rules need to have unique IDs contianing the contact id
|
104
|
+
return {}.tap { |rv| notification_rules.each { |nr_id, nr_val| rv["#{contact_id}_#{nr_id}"] = nr_val } }
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# Class to create Flapjack in Docker for testing
|
2
|
+
require 'docker'
|
3
|
+
require 'flapjack-diner'
|
4
|
+
|
5
|
+
# Class which starts a Flapjack instance in Docker for testing
|
6
|
+
# Container is removed by the grabage collector
|
7
|
+
class FlapjackTestContainer
|
8
|
+
attr_reader :container
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
image = 'flapjack/flapjack:latest'
|
12
|
+
|
13
|
+
# Ensure the image is pulled down
|
14
|
+
Docker::Image.create(fromImage: image)
|
15
|
+
|
16
|
+
# Start the container, binding the API to a random port
|
17
|
+
@container = Docker::Container.create(Image: image)
|
18
|
+
@container.start(PortBindings: { '3081/tcp' => [{ HostPort: '' }] })
|
19
|
+
|
20
|
+
# Define the destructor
|
21
|
+
ObjectSpace.define_finalizer(self, self.class.finalize(@container))
|
22
|
+
|
23
|
+
# TODO: Properly detect if Flapjack is up
|
24
|
+
sleep(2)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Destructor
|
28
|
+
def self.finalize(container)
|
29
|
+
proc do
|
30
|
+
container.stop
|
31
|
+
container.delete
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def api_port
|
36
|
+
return @container.json['NetworkSettings']['Ports']['3081/tcp'][0]['HostPort']
|
37
|
+
end
|
38
|
+
|
39
|
+
def api_url
|
40
|
+
return "http://127.0.0.1:#{api_port}"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Class which sets up Flapjack::Diner against the test container
|
45
|
+
class FlapjackTestDiner
|
46
|
+
attr_reader :diner
|
47
|
+
|
48
|
+
def initialize(test_container)
|
49
|
+
@diner = Flapjack::Diner
|
50
|
+
@diner.base_uri(test_container.api_url)
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require_relative '../spec_helper.rb'
|
2
|
+
require_relative 'config_test_common.rb'
|
3
|
+
|
4
|
+
TestCommon.setup_test(:each) do |rspec_obj|
|
5
|
+
dummy_config = { 'contacts' => {} }
|
6
|
+
rspec_obj.describe 'All Magic Entity' do
|
7
|
+
it 'is not created when enable_all_entity is false' do
|
8
|
+
ret_val = FlapjackConfigurator.configure_flapjack(dummy_config, @test_container.api_url, @logger, false)
|
9
|
+
expect(ret_val).to eq(false)
|
10
|
+
expect(@test_diner.diner.entities('ALL')).to be_nil
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'is created when enable_all_entity is true' do
|
14
|
+
ret_val = FlapjackConfigurator.configure_flapjack(dummy_config, @test_container.api_url, @logger, true)
|
15
|
+
expect(ret_val).to eq(true)
|
16
|
+
expect(@test_diner.diner.entities('ALL')).to eq([{ id: 'ALL', name: 'ALL', links: { contacts: [], checks: [] } }])
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require_relative '../spec_helper.rb'
|
2
|
+
require 'yaml'
|
3
|
+
require 'pathname'
|
4
|
+
|
5
|
+
# Common support methods for testing the gem
|
6
|
+
module TestCommon
|
7
|
+
# Set up the test baseline
|
8
|
+
def self.setup_test(before_when = :all)
|
9
|
+
describe 'FlapjackConfigurator gem' do
|
10
|
+
before before_when do
|
11
|
+
@test_container = FlapjackTestContainer.new
|
12
|
+
@test_diner = FlapjackTestDiner.new(@test_container)
|
13
|
+
|
14
|
+
# Silence the logger
|
15
|
+
@logger = Logger.new(STDOUT)
|
16
|
+
@logger.level = Logger::FATAL
|
17
|
+
end
|
18
|
+
|
19
|
+
yield(self)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.load_config(name, subdir = nil)
|
24
|
+
path_obj = Pathname.new('spec/functional/test_configs')
|
25
|
+
path_obj += subdir if subdir
|
26
|
+
path_obj += "#{name}.yaml"
|
27
|
+
config = YAML.load_file(path_obj)
|
28
|
+
fail "Failed to load test config from #{filename}" unless config
|
29
|
+
return config
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.setup_config_test(name)
|
33
|
+
setup_test do |rspec_obj|
|
34
|
+
rspec_obj.describe 'Config hash' do
|
35
|
+
# Update passes
|
36
|
+
{ 'initial data' => { subdir: 'initial', retval: true },
|
37
|
+
'inital update' => { subdir: 'initial', retval: false },
|
38
|
+
'changed data' => { subdir: 'changes', retval: true },
|
39
|
+
'changed update' => { subdir: 'changes', retval: false }
|
40
|
+
}.each do |test_name, test_data|
|
41
|
+
test_config = TestCommon.load_config(name, test_data[:subdir])
|
42
|
+
|
43
|
+
describe "Contact #{name} #{test_name}" do
|
44
|
+
before :all do
|
45
|
+
@config_output = FlapjackConfigurator.configure_flapjack(test_config, @test_container.api_url, @logger, true)
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'returns true if changes are made' do
|
49
|
+
expect(@config_output).to eq(test_data[:retval])
|
50
|
+
end
|
51
|
+
|
52
|
+
yield(self, test_config)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require_relative '../spec_helper.rb'
|
2
|
+
require_relative 'config_test_common.rb'
|
3
|
+
|
4
|
+
TestCommon.setup_config_test('attributes') do |rspec_obj, test_config|
|
5
|
+
test_config['contacts'].each do |test_contact, test_contact_settings|
|
6
|
+
rspec_obj.describe "#{test_contact} config attributes" do
|
7
|
+
before :all do
|
8
|
+
@api_contact = @test_diner.diner.contacts(test_contact)[0]
|
9
|
+
end
|
10
|
+
|
11
|
+
test_contact_settings['details'].each do |name, value|
|
12
|
+
it "#{name} set to #{value}" do
|
13
|
+
expect(@api_contact[name.to_sym]).to eq(value)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
require_relative '../spec_helper.rb'
|
2
|
+
require_relative 'config_test_common.rb'
|
3
|
+
|
4
|
+
# Class representing a set of identities
|
5
|
+
# Used to build sets of entities for testing
|
6
|
+
class TestEntityIDs
|
7
|
+
attr_reader :idlist
|
8
|
+
|
9
|
+
def initialize(idlist)
|
10
|
+
@idlist = idlist
|
11
|
+
end
|
12
|
+
|
13
|
+
def -(other)
|
14
|
+
return TestEntityIDs.new(@idlist - other.idlist)
|
15
|
+
end
|
16
|
+
|
17
|
+
def id_name(id)
|
18
|
+
fail 'Bad ID' unless id
|
19
|
+
return "testentity-#{id}"
|
20
|
+
end
|
21
|
+
|
22
|
+
def entity_create_list
|
23
|
+
# Returns a list in the format needed by Flapjack::Diner.create_entities
|
24
|
+
return @idlist.map { |id| { name: id_name(id), id: id_name(id) } }
|
25
|
+
end
|
26
|
+
|
27
|
+
def names
|
28
|
+
return @idlist.map { |id| id_name(id) }
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
TestCommon.setup_test do |rspec_obj|
|
33
|
+
rspec_obj.describe 'contact entities' do
|
34
|
+
before :all do
|
35
|
+
# Load flapjack with test entities
|
36
|
+
@remaining_entities = TestEntityIDs.new(Array.new(100) { |c| c })
|
37
|
+
fail 'Failed to create test entities' unless @test_diner.diner.create_entities(@remaining_entities.entity_create_list)
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'Returns true as the config is updated' do
|
41
|
+
expect(FlapjackConfigurator.configure_flapjack(TestCommon.load_config('entities'), @test_container.api_url, @logger, true)).to eq(true)
|
42
|
+
end
|
43
|
+
|
44
|
+
describe 'matchers' do
|
45
|
+
before :all do
|
46
|
+
@contact_entities = @test_diner.diner.contacts('search_test')[0][:links][:entities]
|
47
|
+
|
48
|
+
# Generate the lists for the tests here to only define the IDs once.
|
49
|
+
# popping magic doesn't work due to rspec test independence.
|
50
|
+
@whitelist_exact_entities = TestEntityIDs.new([1, 3, 31, 71])
|
51
|
+
|
52
|
+
@blacklist_exact_entities = TestEntityIDs.new([21, 23, 61, 63])
|
53
|
+
# Blacklist regex ranges minus the two whitelist pins
|
54
|
+
@blacklist_regex_entities = TestEntityIDs.new((Array.new(10) { |c| c + 30 } + Array.new(10) { |c| c + 70 }) - [31, 71])
|
55
|
+
|
56
|
+
@whitelist_regex_entities = TestEntityIDs.new(Array.new(30) { |c| c + 20 } + Array.new(30) { |c| c + 60 })
|
57
|
+
@whitelist_regex_entities -= @blacklist_exact_entities
|
58
|
+
@whitelist_regex_entities -= @blacklist_regex_entities
|
59
|
+
|
60
|
+
@remaining_entities -= @whitelist_exact_entities
|
61
|
+
@remaining_entities -= @whitelist_regex_entities
|
62
|
+
end
|
63
|
+
|
64
|
+
describe 'entities/exact' do
|
65
|
+
it 'whitelists specified entities' do
|
66
|
+
@whitelist_exact_entities.names.each do |ename|
|
67
|
+
expect(@contact_entities).to include(ename)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
describe 'entities_blacklist/exact' do
|
73
|
+
it 'blacklists specified regexes' do
|
74
|
+
@blacklist_exact_entities.names.each do |ename|
|
75
|
+
expect(@contact_entities).not_to include(ename)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
describe 'entities_blacklist/regex' do
|
81
|
+
it 'blacklists specified regexes' do
|
82
|
+
@blacklist_regex_entities.names.each do |ename|
|
83
|
+
expect(@contact_entities).not_to include(ename)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
describe 'entities/regex' do
|
89
|
+
it 'whitelists specified regexes minus blacklists' do
|
90
|
+
@whitelist_regex_entities.names.each do |ename|
|
91
|
+
expect(@contact_entities).to include(ename)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
describe 'non-specified entities' do
|
97
|
+
it 'are not present' do
|
98
|
+
@remaining_entities.names.each do |ename|
|
99
|
+
expect(@contact_entities).not_to include(ename)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
describe 'entities/default' do
|
105
|
+
it 'matches anything orphaned' do
|
106
|
+
expect(@test_diner.diner.contacts('default_test')[0][:links][:entities].sort).to eq(@remaining_entities.names.sort)
|
107
|
+
end
|
108
|
+
|
109
|
+
# Redundant, as the above will catch this, but I like having the explicit test.
|
110
|
+
it "doesn't match the magic ALL entitiy" do
|
111
|
+
expect(@test_diner.diner.contacts('default_test')[0][:links][:entities]).not_to include('ALL')
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|