pager_judy 0.2.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.
- checksums.yaml +7 -0
- data/.buildkite/pipeline.yml +17 -0
- data/.buildkite/upload-pipeline +3 -0
- data/.dockerignore +3 -0
- data/.gitignore +53 -0
- data/.rspec +2 -0
- data/.rubocop.yml +49 -0
- data/CHANGES.md +29 -0
- data/Dockerfile +22 -0
- data/Gemfile +20 -0
- data/Gemfile.lock +109 -0
- data/README.md +5 -0
- data/Rakefile +9 -0
- data/auto/build +7 -0
- data/auto/dev-environment +11 -0
- data/auto/test +3 -0
- data/docker-compose.yml +16 -0
- data/exe/pagerjudy +10 -0
- data/lib/pager_judy/api/client.rb +61 -0
- data/lib/pager_judy/api/collection.rb +95 -0
- data/lib/pager_judy/api/errors.rb +20 -0
- data/lib/pager_judy/api/fake_api_app.rb +171 -0
- data/lib/pager_judy/api/item.rb +56 -0
- data/lib/pager_judy/api/resource.rb +81 -0
- data/lib/pager_judy/cli/collection_behaviour.rb +49 -0
- data/lib/pager_judy/cli/data_display.rb +52 -0
- data/lib/pager_judy/cli/item_behaviour.rb +31 -0
- data/lib/pager_judy/cli/main_command.rb +321 -0
- data/lib/pager_judy/cli/time_filtering.rb +46 -0
- data/lib/pager_judy/cli.rb +1 -0
- data/lib/pager_judy/sync/config.rb +69 -0
- data/lib/pager_judy/sync/syncer.rb +23 -0
- data/lib/pager_judy/sync.rb +12 -0
- data/lib/pager_judy/version.rb +5 -0
- data/pager_judy.gemspec +26 -0
- metadata +93 -0
@@ -0,0 +1,171 @@
|
|
1
|
+
require "securerandom"
|
2
|
+
require "sinatra/base"
|
3
|
+
require "multi_json"
|
4
|
+
|
5
|
+
module PagerJudy
|
6
|
+
module API
|
7
|
+
|
8
|
+
# This little Rack app is designed to fake the PagerDuty REST API.
|
9
|
+
#
|
10
|
+
# It's pretty much just CRUD:
|
11
|
+
#
|
12
|
+
# GET /COLLECTION
|
13
|
+
# POST /COLLECTION
|
14
|
+
# GET /COLLECTION/ID
|
15
|
+
# PUT /COLLECTION/ID
|
16
|
+
#
|
17
|
+
class FakeApp < Sinatra::Base
|
18
|
+
|
19
|
+
def db
|
20
|
+
# The "database" is just a big Hash. We make it an instance variable,
|
21
|
+
# so it sticks around between requests.
|
22
|
+
@db ||= {}
|
23
|
+
end
|
24
|
+
|
25
|
+
attr_writer :db
|
26
|
+
|
27
|
+
def collection_type
|
28
|
+
params["collection_type"]
|
29
|
+
end
|
30
|
+
|
31
|
+
def collection
|
32
|
+
db[collection_type] ||= {}
|
33
|
+
end
|
34
|
+
|
35
|
+
def item_type
|
36
|
+
collection_type.chomp("s")
|
37
|
+
end
|
38
|
+
|
39
|
+
def item_id
|
40
|
+
@item_id ||= params["item_id"]
|
41
|
+
end
|
42
|
+
|
43
|
+
def item
|
44
|
+
return_error(404, "#{item_type} #{item_id} not found") unless collection.key?(item_id)
|
45
|
+
collection.fetch(item_id)
|
46
|
+
end
|
47
|
+
|
48
|
+
def item_data
|
49
|
+
item.merge("id" => item_id)
|
50
|
+
end
|
51
|
+
|
52
|
+
def sub_collection_type
|
53
|
+
params["sub_collection_type"]
|
54
|
+
end
|
55
|
+
|
56
|
+
def sub_collection
|
57
|
+
item[sub_collection_type] ||= {}
|
58
|
+
end
|
59
|
+
|
60
|
+
def sub_item_type
|
61
|
+
sub_collection_type.chomp("s")
|
62
|
+
end
|
63
|
+
|
64
|
+
def sub_item_id
|
65
|
+
@sub_item_id ||= params["sub_item_id"]
|
66
|
+
end
|
67
|
+
|
68
|
+
def sub_item_exists?
|
69
|
+
sub_collection.key?(sub_item_id)
|
70
|
+
end
|
71
|
+
|
72
|
+
def sub_item
|
73
|
+
return_error(404, "#{sub_item_type} #{sub_item_id} not found") unless sub_collection.key?(sub_item_id)
|
74
|
+
sub_collection.fetch(sub_item_id)
|
75
|
+
end
|
76
|
+
|
77
|
+
def sub_item_data
|
78
|
+
sub_collection.fetch(sub_item_id).merge("id" => sub_item_id)
|
79
|
+
end
|
80
|
+
|
81
|
+
# List a collection
|
82
|
+
#
|
83
|
+
get "/:collection_type" do
|
84
|
+
result = {
|
85
|
+
collection_type => collection.map do |id, data|
|
86
|
+
data.merge("id" => id)
|
87
|
+
end,
|
88
|
+
"limit" => collection.size + 10,
|
89
|
+
"offset" => 0,
|
90
|
+
"more" => false
|
91
|
+
}
|
92
|
+
return_json(result)
|
93
|
+
end
|
94
|
+
|
95
|
+
# Show an item
|
96
|
+
#
|
97
|
+
get "/:collection_type/:item_id" do
|
98
|
+
return_json(item_type => item_data)
|
99
|
+
end
|
100
|
+
|
101
|
+
# Create an item
|
102
|
+
#
|
103
|
+
post "/:collection_type" do
|
104
|
+
data = json_body.fetch(item_type)
|
105
|
+
data.delete("id")
|
106
|
+
@item_id = SecureRandom.hex(4)
|
107
|
+
collection[@item_id] = data
|
108
|
+
return_json({ item_type => item_data }, 201)
|
109
|
+
end
|
110
|
+
|
111
|
+
# Update an item
|
112
|
+
#
|
113
|
+
put "/:collection_type/:item_id" do
|
114
|
+
data = json_body.fetch(item_type)
|
115
|
+
data.delete("id")
|
116
|
+
data.delete("type")
|
117
|
+
item.merge!(data)
|
118
|
+
return_json({ item_type => item_data }, 200)
|
119
|
+
end
|
120
|
+
|
121
|
+
# Create a sub-item
|
122
|
+
#
|
123
|
+
post "/:collection_type/:item_id/:sub_collection_type" do
|
124
|
+
data = json_body.fetch(sub_item_type)
|
125
|
+
data.delete("id")
|
126
|
+
@sub_item_id = SecureRandom.hex(4)
|
127
|
+
sub_collection[@sub_item_id] = data
|
128
|
+
return_json({ sub_item_type => sub_item_data }, 201)
|
129
|
+
end
|
130
|
+
|
131
|
+
# Update a sub-item
|
132
|
+
#
|
133
|
+
put "/:collection_type/:item_id/:sub_collection_type/:sub_item_id" do
|
134
|
+
data = json_body.fetch(sub_item_type)
|
135
|
+
data.delete("id")
|
136
|
+
data.delete("type")
|
137
|
+
sub_item.merge!(data)
|
138
|
+
return_json({ sub_item_type => sub_item_data }, 200)
|
139
|
+
end
|
140
|
+
|
141
|
+
not_found do
|
142
|
+
return_error 404, "Not found"
|
143
|
+
end
|
144
|
+
|
145
|
+
private
|
146
|
+
|
147
|
+
def json_body
|
148
|
+
MultiJson.load(request.body)
|
149
|
+
rescue MultiJson::ParseError
|
150
|
+
{}
|
151
|
+
end
|
152
|
+
|
153
|
+
def return_json(data, status = 200)
|
154
|
+
content_type "application/vnd.pagerduty+json;version=2"
|
155
|
+
[status, MultiJson.dump(data)]
|
156
|
+
end
|
157
|
+
|
158
|
+
def return_error(status, message)
|
159
|
+
data = {
|
160
|
+
"error" => {
|
161
|
+
"message" => message
|
162
|
+
}
|
163
|
+
}
|
164
|
+
content_type "application/json"
|
165
|
+
halt status, MultiJson.dump(data)
|
166
|
+
end
|
167
|
+
|
168
|
+
end
|
169
|
+
|
170
|
+
end
|
171
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module PagerJudy
|
2
|
+
module API
|
3
|
+
|
4
|
+
# Represents an item, e.g. a service, a user, ...
|
5
|
+
#
|
6
|
+
class Item
|
7
|
+
|
8
|
+
def initialize(resource, type, id, criteria = {})
|
9
|
+
@resource = resource
|
10
|
+
@type = type
|
11
|
+
@id = id
|
12
|
+
@criteria = criteria
|
13
|
+
end
|
14
|
+
|
15
|
+
attr_reader :resource
|
16
|
+
attr_reader :type
|
17
|
+
attr_reader :id
|
18
|
+
attr_reader :criteria
|
19
|
+
|
20
|
+
def with(more_criteria)
|
21
|
+
more_criteria = Hash[more_criteria.select { |_, v| v }]
|
22
|
+
self.class.new(resource, type, id, criteria.merge(more_criteria))
|
23
|
+
end
|
24
|
+
|
25
|
+
def read
|
26
|
+
resource.get(criteria).fetch(type)
|
27
|
+
end
|
28
|
+
|
29
|
+
def update(data)
|
30
|
+
if dry_run?
|
31
|
+
result = data
|
32
|
+
else
|
33
|
+
result = resource.put(type => data).fetch(type)
|
34
|
+
end
|
35
|
+
name = result.fetch("name")
|
36
|
+
logger.info { "updated #{type} #{name.inspect} [#{id}]" }
|
37
|
+
end
|
38
|
+
|
39
|
+
def to_h
|
40
|
+
read
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def logger
|
46
|
+
resource.logger
|
47
|
+
end
|
48
|
+
|
49
|
+
def dry_run?
|
50
|
+
resource.dry_run?
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require "httpi"
|
2
|
+
require "multi_json"
|
3
|
+
require "pager_judy/api/errors"
|
4
|
+
require "uri"
|
5
|
+
|
6
|
+
module PagerJudy
|
7
|
+
module API
|
8
|
+
|
9
|
+
# Represents an API endpoint.
|
10
|
+
#
|
11
|
+
class Resource
|
12
|
+
|
13
|
+
def initialize(api_key:, uri:, logger: nil, dry_run: false)
|
14
|
+
@api_key = api_key
|
15
|
+
@uri = URI(uri.to_s.chomp("/"))
|
16
|
+
@type = @uri.to_s.split("/").last
|
17
|
+
@logger = logger || Logger.new(nil)
|
18
|
+
@dry_run = dry_run
|
19
|
+
end
|
20
|
+
|
21
|
+
attr_reader :api_key
|
22
|
+
attr_reader :uri
|
23
|
+
attr_reader :type
|
24
|
+
attr_reader :logger
|
25
|
+
|
26
|
+
def dry_run?
|
27
|
+
@dry_run
|
28
|
+
end
|
29
|
+
|
30
|
+
def subresource(path)
|
31
|
+
Resource.new(api_key: api_key, uri: "#{uri}/#{path}", logger: logger, dry_run: dry_run?)
|
32
|
+
end
|
33
|
+
|
34
|
+
def get(query = nil)
|
35
|
+
request = new_request
|
36
|
+
request.query = query if query
|
37
|
+
debug("GET", request.url)
|
38
|
+
response = HTTPI.get(request)
|
39
|
+
raise HttpError.new(request, response) if response.error?
|
40
|
+
MultiJson.load(response.body)
|
41
|
+
end
|
42
|
+
|
43
|
+
def post(data)
|
44
|
+
request = new_request
|
45
|
+
request.body = MultiJson.dump(data)
|
46
|
+
debug("POST", request.url, data)
|
47
|
+
response = HTTPI.post(request)
|
48
|
+
raise HttpError.new(request, response) if response.error?
|
49
|
+
MultiJson.load(response.body)
|
50
|
+
end
|
51
|
+
|
52
|
+
def put(data)
|
53
|
+
request = new_request
|
54
|
+
request.body = MultiJson.dump(data)
|
55
|
+
debug("PUT", request.url, data)
|
56
|
+
response = HTTPI.put(request)
|
57
|
+
raise HttpError.new(request, response) if response.error?
|
58
|
+
MultiJson.load(response.body)
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def debug(operation, url, data = nil)
|
64
|
+
logger.debug do
|
65
|
+
data_dump = MultiJson.dump(data, pretty: true) if data
|
66
|
+
[operation, url, data_dump].join(" ")
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def new_request
|
71
|
+
HTTPI::Request.new(uri.to_s).tap do |req|
|
72
|
+
req.headers["Accept"] = "application/vnd.pagerduty+json;version=2"
|
73
|
+
req.headers["Authorization"] = "Token token=#{api_key}"
|
74
|
+
req.headers["Content-Type"] = "application/json"
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require "pager_judy/cli/data_display"
|
2
|
+
|
3
|
+
module PagerJudy
|
4
|
+
module CLI
|
5
|
+
|
6
|
+
# Common behaviour for sub-commands that operate on collections.
|
7
|
+
#
|
8
|
+
module CollectionBehaviour
|
9
|
+
|
10
|
+
def self.included(target)
|
11
|
+
target.default_subcommand = "summary"
|
12
|
+
|
13
|
+
target.subcommand ["summary", "s"], "One-line summary" do
|
14
|
+
include SummarySubcommand
|
15
|
+
end
|
16
|
+
|
17
|
+
target.subcommand ["data", "d"], "Full details" do
|
18
|
+
include DataSubcommand
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
module SummarySubcommand
|
23
|
+
|
24
|
+
def execute
|
25
|
+
collection.each do |item|
|
26
|
+
puts "#{item.fetch('id')}: #{item.fetch('summary')}"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
module DataSubcommand
|
33
|
+
|
34
|
+
include DataDisplay
|
35
|
+
|
36
|
+
def self.included(target)
|
37
|
+
target.parameter "[EXPR]", "JMESPath expression"
|
38
|
+
end
|
39
|
+
|
40
|
+
def execute
|
41
|
+
puts display_data(collection.to_a, expr)
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require "clamp"
|
2
|
+
require "jmespath"
|
3
|
+
require "multi_json"
|
4
|
+
require "yaml"
|
5
|
+
|
6
|
+
module PagerJudy
|
7
|
+
module CLI
|
8
|
+
|
9
|
+
module DataDisplay
|
10
|
+
|
11
|
+
extend Clamp::Option::Declaration
|
12
|
+
|
13
|
+
option %w[-f --format], "FORMAT", "format for data output",
|
14
|
+
attribute_name: :output_format,
|
15
|
+
default: "YAML"
|
16
|
+
|
17
|
+
def output_format=(arg)
|
18
|
+
arg = arg.upcase
|
19
|
+
unless %w[JSON YAML].member?(arg)
|
20
|
+
raise ArgumentError, "unrecognised data format: #{arg.inspect}"
|
21
|
+
end
|
22
|
+
@output_format = arg
|
23
|
+
end
|
24
|
+
|
25
|
+
protected
|
26
|
+
|
27
|
+
def format_data(data)
|
28
|
+
case output_format
|
29
|
+
when "JSON"
|
30
|
+
MultiJson.dump(data, pretty: true)
|
31
|
+
when "YAML"
|
32
|
+
YAML.dump(data)
|
33
|
+
else
|
34
|
+
raise "bad output format: #{output_format}"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def select_data(data, jmespath_expression = nil)
|
39
|
+
return data if jmespath_expression.nil?
|
40
|
+
JMESPath.search(jmespath_expression, data)
|
41
|
+
rescue JMESPath::Errors::SyntaxError
|
42
|
+
signal_error("invalid JMESPath expression")
|
43
|
+
end
|
44
|
+
|
45
|
+
def display_data(data, jmespath_expression = nil)
|
46
|
+
format_data(select_data(data, jmespath_expression))
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require "pager_judy/cli/data_display"
|
2
|
+
|
3
|
+
module PagerJudy
|
4
|
+
module CLI
|
5
|
+
|
6
|
+
# Common behaviour for sub-commands that operate on collection members.
|
7
|
+
#
|
8
|
+
module ItemBehaviour
|
9
|
+
|
10
|
+
def self.included(target)
|
11
|
+
target.default_subcommand = "data"
|
12
|
+
|
13
|
+
target.subcommand ["data", "d"], "Full details" do
|
14
|
+
include DataSubcommand
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
module DataSubcommand
|
19
|
+
|
20
|
+
include DataDisplay
|
21
|
+
|
22
|
+
def execute
|
23
|
+
puts display_data(item.to_h)
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|