pager_judy 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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