codeclimate-collector-manager 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: a12a56ef4ce5f3dcec9de3baa06a263ed8bcf7e759114d9b70aa6ef2776b4e0b
4
+ data.tar.gz: a2a4cc6d0c504e3b1e544f3e5964a39865e7c71e5927df9aee60211d420141a3
5
+ SHA512:
6
+ metadata.gz: 9464ecc990f434fac802f5ae75d8626dfcb0e68928632188c74d779fdd584a409ed2f3244ef5032e32b2a2a083d91562035c7db6cab2a2421dfbbd69f629424d
7
+ data.tar.gz: 24940ac19c4e2178d0ffa0dea265d8bc1351f41fab475a97157c45a11bab57a8836785b7c112dd9207c90bd367ff12f83ffef10645cdc2c1badba80d4b29aea7
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # Run a collector and log what it does for testing.
4
+ #
5
+ # Usage: bundle exec run-collector collector-slug collector-method config-file
6
+ #
7
+ # This script will `require "codeclimate-collector-#{collector-slug}"`,
8
+ # instantiate a client (class name derived from the require name), providing a
9
+ # configuration object constructed from the contents of `config-file`. It will
10
+ # then ask the client to handle the request parsed from `request-file`.
11
+ #
12
+ # E.g. `bundle exec run-collector pagerduty sync config.json` will
13
+ # `require "codeclimate-collector-pagerduty"`, construct a config from
14
+ # `config.json`, and call
15
+ # `Codeclimate::Collectors::Pagerduty::Client.sync(
16
+ # config,
17
+ # earliest_date_cutoff: 6.months.ago)`.
18
+
19
+ require "codeclimate-collector-manager"
20
+
21
+ class Runner
22
+ class MessagesHandler
23
+ def initialize(logger)
24
+ @logger = logger
25
+ end
26
+
27
+ def send_message(message)
28
+ logger.info("[MESSAGE RECEIVED] #{message.to_json}")
29
+ end
30
+
31
+ private
32
+
33
+ attr_reader :logger
34
+ end
35
+
36
+ def initialize(slug: , client_method:, config_path:)
37
+ require "codeclimate-collector-#{slug}"
38
+ @client_klass = "Codeclimate::Collectors::#{slug.camelcase}::Client".constantize
39
+ @client_method = client_method
40
+ @client_config = build_config(JSON.parse(File.read(config_path)))
41
+ end
42
+
43
+ def run
44
+ case client_method
45
+ when "sync"
46
+ client_klass.sync(
47
+ configuration: client_config,
48
+ manager: manager,
49
+ earliest_data_cutoff: Time.now.utc - (6 * 30 * 24 * 60 * 60), # ~6 months
50
+ )
51
+ when "validate_configuration"
52
+ client_klass.validate_configuration(
53
+ configuration: client_config,
54
+ manager: manager,
55
+ )
56
+ else
57
+ raise ArgumentError, "don't know how to process '#{client_method}'"
58
+ end
59
+
60
+ logger.info("Done!")
61
+ end
62
+
63
+ private
64
+
65
+ attr_reader :client_klass, :client_method, :client_config
66
+
67
+ def build_client
68
+ client_klass.new(configuration: client_config.dup, manager: manager)
69
+ end
70
+
71
+ def build_config(json)
72
+ Codeclimate::Collectors::Configuration.new(json)
73
+ end
74
+
75
+ def manager
76
+ @manager ||= Codeclimate::Collectors::Manager.new(
77
+ messages: Codeclimate::Collectors::MessagesFacade.new(
78
+ implementation: MessagesHandler.new(logger),
79
+ ),
80
+ )
81
+ end
82
+
83
+ def logger
84
+ @logger ||= Logger.new($stdout).tap do |l|
85
+ l.level = Logger::INFO
86
+ end
87
+ end
88
+ end
89
+
90
+ Runner.new(
91
+ slug: ARGV[0],
92
+ client_method: ARGV[1],
93
+ config_path: ARGV[2],
94
+ ).run
@@ -0,0 +1,4 @@
1
+ require "active_model"
2
+ require "active_support/json"
3
+
4
+ require "codeclimate/collectors"
@@ -0,0 +1,5 @@
1
+ require "codeclimate/collectors/configuration"
2
+ require "codeclimate/collectors/manager"
3
+ require "codeclimate/collectors/validations/type_validator"
4
+ require "codeclimate/collectors/messages"
5
+ require "codeclimate/collectors/messages_facade"
@@ -0,0 +1,34 @@
1
+ module Codeclimate
2
+ module Collectors
3
+ # The configuration for a collector client.
4
+ # Used like a +Hash+. Treats all keys as symbols.
5
+ class Configuration
6
+ def initialize(attrs = {})
7
+ @storage = attrs.deep_symbolize_keys
8
+ end
9
+
10
+ def [](key)
11
+ storage[key.to_sym]
12
+ end
13
+
14
+ def []=(key, val)
15
+ storage[key.to_sym] = val
16
+ end
17
+
18
+ def fetch(*args)
19
+ case args.count
20
+ when 1
21
+ storage.fetch(args[0].to_sym)
22
+ when 2
23
+ storage.fetch(args[0].to_sym, args[1])
24
+ else
25
+ raise ArgumentError, "expected 1 or 2 arguments"
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ attr_reader :storage
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,14 @@
1
+ module Codeclimate
2
+ module Collectors
3
+ class Manager
4
+ VERSION = File.read(File.expand_path("../../../../VERSION", __FILE__)).strip
5
+
6
+ attr_reader :messages
7
+
8
+ # - +messages+ is an instance of MessagesFacade
9
+ def initialize(messages:)
10
+ @messages = messages
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,12 @@
1
+ require "codeclimate/collectors/messages/message"
2
+
3
+ require "codeclimate/collectors/messages/configuration_verification"
4
+ require "codeclimate/collectors/messages/incident"
5
+
6
+ module Codeclimate
7
+ module Collectors
8
+ module Messages
9
+ InvalidMessage = Class.new(StandardError)
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,38 @@
1
+ module Codeclimate
2
+ module Collectors
3
+ module Messages
4
+ # Used to emit the result of processing a +Requests::VerifyConfiguration+
5
+ # request
6
+ class ConfigurationVerification < Message
7
+ STATES = [
8
+ SUCCESS = "success".freeze,
9
+ ERROR = "error".freeze,
10
+ ].freeze
11
+
12
+ attribute :state, String
13
+ attribute :error_messages, Array, optional: true
14
+
15
+ validates :state, inclusion: { in: STATES }
16
+ validate :validate_error_messages_match_state
17
+ validate :validate_error_messages_are_strings
18
+
19
+ private
20
+
21
+ def validate_error_messages_are_strings
22
+ all_strs = (error_messages || []).all? { |m| m.is_a?(String) }
23
+ unless all_strs
24
+ errors.add(:error_message, "must be an array of strings")
25
+ end
26
+ end
27
+
28
+ def validate_error_messages_match_state
29
+ if state == ERROR && error_messages.blank?
30
+ errors.add(:error_messages, "are required if the state is 'error'")
31
+ elsif state == SUCCESS && error_messages.present?
32
+ errors.add(:error_messages, "should not be provided if config is valid")
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,41 @@
1
+ module Codeclimate
2
+ module Collectors
3
+ module Messages
4
+ # Represents an incident from an Incident Response Platform, such as
5
+ # PagerDuty.
6
+ class Incident < Message
7
+ STATUSES = [
8
+ TRIGGERED = "triggered".freeze,
9
+ ACKNOWLEDGED = "acknowledged".freeze,
10
+ RESOLVED = "resolved".freeze,
11
+ ].freeze
12
+
13
+ # The unique identifier of this incident within the IRP. Often this is
14
+ # an +id+ key in API responses.
15
+ attribute :external_id, String
16
+
17
+ # The human-readable title of the incident.
18
+ attribute :title, String
19
+
20
+ # The URL a human would visit in a brower to see details of the
21
+ # incident.
22
+ attribute :url, String
23
+
24
+ # The incrementing numerical number of the incident.
25
+ attribute :number, Integer
26
+
27
+ # The current status of the incident.
28
+ attribute :status, String
29
+
30
+ # The time the incident began.
31
+ attribute :created_at, Time
32
+
33
+ validates :status, inclusion: { in: STATUSES }
34
+
35
+ def self.ordering_keys
36
+ [:external_id]
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,91 @@
1
+ module Codeclimate
2
+ module Collectors
3
+ module Messages
4
+ class Message
5
+ include ActiveModel::Model
6
+ include ActiveModel::Validations::HelperMethods
7
+ include Collectors::Validations
8
+
9
+ def self.attribute_metadata
10
+ @attribute_metadata ||= {}
11
+ end
12
+
13
+ # Declare an attribute on a message type. This will declare an
14
+ # +attr_accessor+, a validation that the attribute is of the specified
15
+ # +type+, and include it in +#attributes+. If +optional+ is false a
16
+ # validation for +presence+ is also added.
17
+ def self.attribute(name, type, optional: false)
18
+ name = name.to_sym
19
+
20
+ attribute_metadata[name] = { type: type, optional: optional }
21
+
22
+ attr_accessor name
23
+
24
+ validates name, presence: !optional, type: { type: type }
25
+ end
26
+
27
+ # Calculate the type string to use in the JSON representation to
28
+ # identify this message type.
29
+ def self.json_type
30
+ to_s.gsub(/^.+::(\w+)$/, "\\1").underscore
31
+ end
32
+
33
+ # Messages can have ordering dependencies, e.g. a collector could emit
34
+ # an Incident message, followed by several IncidentEvent messages.
35
+ # IncidentEvent messages identify their associated incident via their
36
+ # +incident_external_id+ attribute.
37
+ #
38
+ # Our backend distributes messages amongst parallel workers for
39
+ # ingestion. To ensure messages with an ordering dependency are
40
+ # processed in order by the same worker, we can return an appropriate
41
+ # +ordering_keys+ array for the classes: e.g. Incident returns
42
+ # +[:external_id]+ and IncidentEvent returns +[incident_external_id]+.
43
+ #
44
+ # Collectors are also responsible for emitting messages in the
45
+ # appropriate order.
46
+ def self.ordering_keys
47
+ []
48
+ end
49
+
50
+ def ==(other)
51
+ other.class == self.class && other.attributes == attributes
52
+ end
53
+
54
+ validate :validate_not_base_class
55
+
56
+ # Return all the attributes of a message as a +Hash+
57
+ def attributes
58
+ Hash[
59
+ self.class.attribute_metadata.keys.each.map do |name|
60
+ [name, public_send(name)]
61
+ end
62
+ ]
63
+ end
64
+
65
+ # A +Hash+ for serializing the message as JSON.
66
+ # Includes a +type+ key and an +attributes+ key.
67
+ def as_json(*_opts)
68
+ {
69
+ type: self.class.json_type,
70
+ attributes: json_attributes,
71
+ }
72
+ end
73
+
74
+ protected
75
+
76
+ def json_attributes
77
+ Hash[attributes.map { |name, val| [name, val.as_json] }]
78
+ end
79
+
80
+ private
81
+
82
+ def validate_not_base_class
83
+ if instance_of?(Messages::Message)
84
+ errors.add(:base, "base Message class is abstract")
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
91
+
@@ -0,0 +1,28 @@
1
+ module Codeclimate
2
+ module Collectors
3
+ class MessagesFacade
4
+ # Wraps functionality for sending messages. Takes care of some basic
5
+ # validation and such so that individual implementations don't need to.
6
+ #
7
+ # +implementation+ should respond to +#send_message+.
8
+ def initialize(implementation:)
9
+ @implementation = implementation
10
+ end
11
+
12
+ def send_message(message)
13
+ if !message.valid?
14
+ raise Messages::InvalidMessage, message.errors.full_messages.to_sentence
15
+ end
16
+ implementation.send_message(message)
17
+ end
18
+
19
+ def <<(message)
20
+ send_message(message)
21
+ end
22
+
23
+ private
24
+
25
+ attr_reader :implementation
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,17 @@
1
+ require "codeclimate/collectors/testing/stub_messages_facade"
2
+
3
+ module Codeclimate
4
+ module Collectors
5
+ module Testing
6
+ def stub_manager
7
+ Codeclimate::Collectors::Manager.new(
8
+ messages: stub_messages_facade,
9
+ )
10
+ end
11
+
12
+ def stub_messages_facade
13
+ Codeclimate::Collectors::Testing::StubMessagesFacade.new
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,25 @@
1
+ module Codeclimate
2
+ module Collectors
3
+ module Testing
4
+ class StubMessagesFacade < MessagesFacade
5
+ attr_reader :received_messages
6
+
7
+ class NullMessagesImplementation
8
+ def send_message(_msg); end
9
+ end
10
+
11
+ def initialize(implementation: NullMessagesImplementation.new)
12
+ super
13
+
14
+ @received_messages = []
15
+ end
16
+
17
+ def send_message(message)
18
+ super
19
+ received_messages << message
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+
@@ -0,0 +1,14 @@
1
+ module Codeclimate
2
+ module Collectors
3
+ module Validations
4
+ class TypeValidator < ::ActiveModel::EachValidator
5
+ def validate_each(record, attr_name, value)
6
+ type = options.fetch(:type)
7
+ if !value.nil? && !value.is_a?(type)
8
+ record.errors.add(attr_name, "must be a #{type}")
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
metadata ADDED
@@ -0,0 +1,99 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: codeclimate-collector-manager
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ platform: ruby
6
+ authors:
7
+ - Code Climate
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-02-18 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activemodel
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 5.1.6
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 5.1.6
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: pry
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ description: Interfaces for collectors to use
56
+ email:
57
+ - hello@codeclimate.com
58
+ executables:
59
+ - run-collector
60
+ extensions: []
61
+ extra_rdoc_files: []
62
+ files:
63
+ - bin/run-collector
64
+ - lib/codeclimate-collector-manager.rb
65
+ - lib/codeclimate/collectors.rb
66
+ - lib/codeclimate/collectors/configuration.rb
67
+ - lib/codeclimate/collectors/manager.rb
68
+ - lib/codeclimate/collectors/messages.rb
69
+ - lib/codeclimate/collectors/messages/configuration_verification.rb
70
+ - lib/codeclimate/collectors/messages/incident.rb
71
+ - lib/codeclimate/collectors/messages/message.rb
72
+ - lib/codeclimate/collectors/messages_facade.rb
73
+ - lib/codeclimate/collectors/testing.rb
74
+ - lib/codeclimate/collectors/testing/stub_messages_facade.rb
75
+ - lib/codeclimate/collectors/validations/type_validator.rb
76
+ homepage: https://codeclimate.com
77
+ licenses:
78
+ - Nonstandard
79
+ metadata: {}
80
+ post_install_message:
81
+ rdoc_options: []
82
+ require_paths:
83
+ - lib
84
+ required_ruby_version: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ required_rubygems_version: !ruby/object:Gem::Requirement
90
+ requirements:
91
+ - - ">="
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ requirements: []
95
+ rubygems_version: 3.1.2
96
+ signing_key:
97
+ specification_version: 4
98
+ summary: Code Climate Collector Manager
99
+ test_files: []