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.
@@ -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