codeclimate-collector-manager 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|