codeclimate-collector-manager 0.0.2

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.
@@ -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: []