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.
Files changed (39) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +2 -0
  3. data/.rubocop.yml +47 -0
  4. data/Dockerfile +11 -0
  5. data/Gemfile +3 -0
  6. data/README.md +139 -0
  7. data/Rakefile +19 -0
  8. data/bin/flapjack_configurator +82 -0
  9. data/example.yml +121 -0
  10. data/flapjack_configurator.gemspec +29 -0
  11. data/lib/flapjack_configurator.rb +32 -0
  12. data/lib/flapjack_configurator/entity_mapper.rb +70 -0
  13. data/lib/flapjack_configurator/flapjack_config.rb +72 -0
  14. data/lib/flapjack_configurator/flapjack_contact.rb +156 -0
  15. data/lib/flapjack_configurator/flapjack_media.rb +23 -0
  16. data/lib/flapjack_configurator/flapjack_notification_rule.rb +39 -0
  17. data/lib/flapjack_configurator/flapjack_object_base.rb +86 -0
  18. data/lib/flapjack_configurator/flapjack_pagerduty.rb +28 -0
  19. data/lib/flapjack_configurator/flapjack_sub_object_base.rb +33 -0
  20. data/lib/flapjack_configurator/user_configuration.rb +107 -0
  21. data/lib/flapjack_configurator/version.rb +6 -0
  22. data/spec/docker_test_wrapper.rb +52 -0
  23. data/spec/functional/all_entity_spec.rb +19 -0
  24. data/spec/functional/config_test_common.rb +58 -0
  25. data/spec/functional/configuration_contact_attributes_spec.rb +18 -0
  26. data/spec/functional/configuration_contact_entities_spec.rb +116 -0
  27. data/spec/functional/configuration_contact_notification_media_spec.rb +73 -0
  28. data/spec/functional/configuration_contact_notification_rules_spec.rb +58 -0
  29. data/spec/functional/configuration_contact_removal_spec.rb +83 -0
  30. data/spec/functional/test_configs/changes/attributes.yaml +24 -0
  31. data/spec/functional/test_configs/changes/notification_media.yaml +155 -0
  32. data/spec/functional/test_configs/changes/notification_rules.yaml +143 -0
  33. data/spec/functional/test_configs/entities.yaml +71 -0
  34. data/spec/functional/test_configs/initial/attributes.yaml +24 -0
  35. data/spec/functional/test_configs/initial/notification_media.yaml +155 -0
  36. data/spec/functional/test_configs/initial/notification_rules.yaml +143 -0
  37. data/spec/functional/test_configs/obj_removal_setup.yaml +106 -0
  38. data/spec/spec_helper.rb +9 -0
  39. 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,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Define the gem version
4
+ module FlapjackConfigurator
5
+ VERSION = '1.0.0'
6
+ 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