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.
- checksums.yaml +7 -0
- data/bin/run-collector +94 -0
- data/lib/codeclimate-collector-manager.rb +4 -0
- data/lib/codeclimate/collectors.rb +5 -0
- data/lib/codeclimate/collectors/configuration.rb +34 -0
- data/lib/codeclimate/collectors/manager.rb +14 -0
- data/lib/codeclimate/collectors/messages.rb +12 -0
- data/lib/codeclimate/collectors/messages/configuration_verification.rb +38 -0
- data/lib/codeclimate/collectors/messages/incident.rb +41 -0
- data/lib/codeclimate/collectors/messages/message.rb +91 -0
- data/lib/codeclimate/collectors/messages_facade.rb +28 -0
- data/lib/codeclimate/collectors/testing.rb +17 -0
- data/lib/codeclimate/collectors/testing/stub_messages_facade.rb +25 -0
- data/lib/codeclimate/collectors/validations/type_validator.rb +14 -0
- metadata +99 -0
checksums.yaml
ADDED
@@ -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
|
data/bin/run-collector
ADDED
@@ -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,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: []
|