ankusa-sdk 0.3.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/CHANGELOG.md +41 -0
- data/LICENSE +190 -0
- data/README.md +256 -0
- data/lib/ankusa/admin.rb +141 -0
- data/lib/ankusa/claim_check.rb +155 -0
- data/lib/ankusa/connection.rb +63 -0
- data/lib/ankusa/error.rb +10 -0
- data/lib/ankusa/routes.rb +187 -0
- data/lib/ankusa/sdk/version.rb +7 -0
- data/lib/ankusa/sdk.rb +11 -0
- data/lib/ankusa/sources.rb +298 -0
- data/lib/ankusa/transport.rb +72 -0
- data/lib/ankusa/webhook.rb +38 -0
- metadata +58 -0
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ankusa
|
|
4
|
+
# Every failure the source-management client can raise.
|
|
5
|
+
#
|
|
6
|
+
# Every subclass carries the HTTP `status` and decoded `body` of the response
|
|
7
|
+
# that produced it (both nil when no response was involved, e.g. a transport
|
|
8
|
+
# failure). Unlike the other families this one has no `retryable?`, matching
|
|
9
|
+
# the reference SDK.
|
|
10
|
+
class SourcesError < Error
|
|
11
|
+
attr_reader :status, :body
|
|
12
|
+
|
|
13
|
+
def initialize(message = nil, status: nil, body: nil)
|
|
14
|
+
super(message)
|
|
15
|
+
@status = status
|
|
16
|
+
@body = body
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# 404: no such source for this tenant.
|
|
21
|
+
class SourceNotFoundError < SourcesError
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# 409 source_exists: a source with that name already exists.
|
|
25
|
+
class SourceConflictError < SourcesError
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# 409 source_store_read_only: the deployment's source store is a static seed,
|
|
29
|
+
# so writes are impossible.
|
|
30
|
+
class SourceStoreReadOnlyError < SourcesError
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# 400: bad tenant, bad source name, or a spec the server rejected.
|
|
34
|
+
#
|
|
35
|
+
# `message` is the server's own `message` (for a bad spec) or the `error` code
|
|
36
|
+
# (for a bad tenant/name, which has no message).
|
|
37
|
+
class SourceInvalidError < SourcesError
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# The admin API is unreachable, timed out, or answered 5xx. Safe to retry.
|
|
41
|
+
class SourcesUnavailableError < SourcesError
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# GET /health reported a version other than `expected_version`.
|
|
45
|
+
class VersionMismatchError < SourcesError
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# The writable fields of a source, as submitted to POST/PUT.
|
|
49
|
+
#
|
|
50
|
+
# `verify` is optional (absent means `{"type" => "none"}` on the server);
|
|
51
|
+
# `sinks` is required and must be non-empty -- the server validates all of
|
|
52
|
+
# this exactly as the YAML config does.
|
|
53
|
+
SourceSpec = Data.define(:sinks, :verify, :on_verify_failure) do
|
|
54
|
+
def initialize(sinks:, verify: nil, on_verify_failure: nil) = super
|
|
55
|
+
|
|
56
|
+
# The JSON body for a create/update, omitting unset (nil) fields.
|
|
57
|
+
def to_request_body
|
|
58
|
+
body = {"sinks" => sinks}
|
|
59
|
+
body["verify"] = verify unless verify.nil?
|
|
60
|
+
body["on_verify_failure"] = on_verify_failure unless on_verify_failure.nil?
|
|
61
|
+
body
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# A stored source as the admin API reports it: redacted spec plus the derived
|
|
66
|
+
# identity fields.
|
|
67
|
+
Source = Data.define(:tenant, :name, :source_id, :ingest_path, :verify, :on_verify_failure, :sinks) do
|
|
68
|
+
def self.from_h(data)
|
|
69
|
+
new(
|
|
70
|
+
tenant: data.fetch("tenant"),
|
|
71
|
+
name: data.fetch("name"),
|
|
72
|
+
source_id: data.fetch("source_id"),
|
|
73
|
+
ingest_path: data.fetch("ingest_path"),
|
|
74
|
+
verify: data["verify"] || {"type" => "none"},
|
|
75
|
+
on_verify_failure: data["on_verify_failure"],
|
|
76
|
+
sinks: data["sinks"] || []
|
|
77
|
+
)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Manage a deployment's tenant-scoped sources via `Ankusa.Admin.Router` (the
|
|
82
|
+
# same `admin.port` as the operator API).
|
|
83
|
+
#
|
|
84
|
+
# The router does no authentication (by design, like the rest of this port),
|
|
85
|
+
# and every source it returns has its secrets redacted. So a `Source` read
|
|
86
|
+
# back here is never useful for editing: resending its `verify` map is not the
|
|
87
|
+
# same as resending the stored secret. Callers that hold the secret supply it
|
|
88
|
+
# through `SourceSpec`.
|
|
89
|
+
#
|
|
90
|
+
# Only `[A-Za-z0-9_-]{1,64}` tenants and names are accepted, and both are
|
|
91
|
+
# checked before any path is built, so a caller-supplied name cannot escape
|
|
92
|
+
# its tenant through URL normalization.
|
|
93
|
+
#
|
|
94
|
+
# `expected_version` is an optional safety latch: when set, the first API call
|
|
95
|
+
# fetches GET /health once, compares its `"version"` field against the
|
|
96
|
+
# expected value, caches the fetched version, and raises
|
|
97
|
+
# `VersionMismatchError` on any mismatch. Every subsequent call re-checks the
|
|
98
|
+
# cached value without another request.
|
|
99
|
+
class SourcesClient
|
|
100
|
+
# The same rule `Ankusa::CLAIM_REF_PATTERN` uses for its tenant. Anything
|
|
101
|
+
# outside it is rejected before a path is built: URL parsers normalize dot
|
|
102
|
+
# segments, so an unvalidated "../" would escape the tenant scope before the
|
|
103
|
+
# server sees it.
|
|
104
|
+
SAFE_ID = /\A[A-Za-z0-9_-]{1,64}\z/
|
|
105
|
+
|
|
106
|
+
def initialize(base_url, expected_version: nil, timeout: 10.0, transport: nil)
|
|
107
|
+
@connection = Connection.new(base_url, timeout: timeout, transport: transport)
|
|
108
|
+
@expected_version = expected_version
|
|
109
|
+
@server_version = nil
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# The deployment's Ankusa version: GET /health ["version"].
|
|
113
|
+
#
|
|
114
|
+
# Fetched once and cached; when `expected_version` was set this also
|
|
115
|
+
# enforces it, so a mismatched deployment raises `VersionMismatchError` here
|
|
116
|
+
# too.
|
|
117
|
+
def server_version
|
|
118
|
+
@server_version = fetch_version if @server_version.nil?
|
|
119
|
+
check_version
|
|
120
|
+
@server_version
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# List a tenant's sources: GET /v1/tenants/<tenant>/sources.
|
|
124
|
+
def list_sources(tenant)
|
|
125
|
+
validate_tenant(tenant)
|
|
126
|
+
ensure_version
|
|
127
|
+
response = request("GET", "/v1/tenants/#{tenant}/sources")
|
|
128
|
+
raise_for_status(response)
|
|
129
|
+
parse_json(response).fetch("entries").map { |entry| Source.from_h(entry) }
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Fetch one source: GET /v1/tenants/<tenant>/sources/<name>.
|
|
133
|
+
def get_source(tenant, name)
|
|
134
|
+
validate_tenant(tenant)
|
|
135
|
+
validate_name(name)
|
|
136
|
+
ensure_version
|
|
137
|
+
response = request("GET", "/v1/tenants/#{tenant}/sources/#{name}")
|
|
138
|
+
raise_for_status(response)
|
|
139
|
+
Source.from_h(parse_json(response))
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Create a source: POST /v1/tenants/<tenant>/sources.
|
|
143
|
+
#
|
|
144
|
+
# The source name travels in the body (plus `name`); the tenant comes from
|
|
145
|
+
# the URL and wins over any `"tenant"` key inside `spec`.
|
|
146
|
+
def create_source(tenant, name, spec)
|
|
147
|
+
validate_tenant(tenant)
|
|
148
|
+
validate_name(name)
|
|
149
|
+
ensure_version
|
|
150
|
+
body = spec.to_request_body.merge("name" => name)
|
|
151
|
+
response = request("POST", "/v1/tenants/#{tenant}/sources", json: body)
|
|
152
|
+
raise_for_status(response)
|
|
153
|
+
Source.from_h(parse_json(response))
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Replace a source: PUT /v1/tenants/<tenant>/sources/<name>.
|
|
157
|
+
#
|
|
158
|
+
# The name comes from the URL; a `"name"` key inside `spec` is never sent
|
|
159
|
+
# (`SourceSpec` has no such field).
|
|
160
|
+
def update_source(tenant, name, spec)
|
|
161
|
+
validate_tenant(tenant)
|
|
162
|
+
validate_name(name)
|
|
163
|
+
ensure_version
|
|
164
|
+
response = request("PUT", "/v1/tenants/#{tenant}/sources/#{name}", json: spec.to_request_body)
|
|
165
|
+
raise_for_status(response)
|
|
166
|
+
Source.from_h(parse_json(response))
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Delete a source: DELETE /v1/tenants/<tenant>/sources/<name>.
|
|
170
|
+
#
|
|
171
|
+
# Succeeds with no return value (the server answers 204 with an empty body);
|
|
172
|
+
# a missing source raises `SourceNotFoundError`.
|
|
173
|
+
def delete_source(tenant, name)
|
|
174
|
+
validate_tenant(tenant)
|
|
175
|
+
validate_name(name)
|
|
176
|
+
ensure_version
|
|
177
|
+
response = request("DELETE", "/v1/tenants/#{tenant}/sources/#{name}")
|
|
178
|
+
raise_for_status(response)
|
|
179
|
+
nil
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
private
|
|
183
|
+
|
|
184
|
+
# The optional version latch: only when `expected_version` is set does the
|
|
185
|
+
# first API call fetch /health once, cache the version, and enforce it.
|
|
186
|
+
def ensure_version
|
|
187
|
+
return if @expected_version.nil?
|
|
188
|
+
|
|
189
|
+
@server_version = fetch_version if @server_version.nil?
|
|
190
|
+
check_version
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def check_version
|
|
194
|
+
return if @expected_version.nil? || @server_version == @expected_version
|
|
195
|
+
|
|
196
|
+
raise VersionMismatchError,
|
|
197
|
+
"expected Ankusa version #{@expected_version.inspect}, server reports #{@server_version.inspect}"
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def fetch_version
|
|
201
|
+
response = request("GET", "/health")
|
|
202
|
+
status = response.status
|
|
203
|
+
if status != 200
|
|
204
|
+
body = Connection.error_body(response)
|
|
205
|
+
raise SourcesUnavailableError.new(
|
|
206
|
+
"ankusa admin API health check failed (#{status}): #{body.inspect}",
|
|
207
|
+
status: status,
|
|
208
|
+
body: body
|
|
209
|
+
)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
begin
|
|
213
|
+
data = Connection.parse_json(response.body)
|
|
214
|
+
rescue JSON::ParserError
|
|
215
|
+
raise SourcesUnavailableError, "ankusa admin API health check returned a non-JSON body (#{status})"
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
version = data.is_a?(Hash) ? data["version"] : nil
|
|
219
|
+
unless version.is_a?(String)
|
|
220
|
+
raise SourcesUnavailableError,
|
|
221
|
+
"ankusa admin API health check returned no version (#{status}): #{data.inspect}"
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
version
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def request(http_method, path, json: nil)
|
|
228
|
+
@connection.request(http_method, path, json: json)
|
|
229
|
+
rescue Transport::Error => e
|
|
230
|
+
raise SourcesUnavailableError, "ankusa admin API unreachable: #{e.message}"
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def parse_json(response)
|
|
234
|
+
Connection.parse_json(response.body)
|
|
235
|
+
rescue JSON::ParserError
|
|
236
|
+
# Deliberate deviation from the reference SDK, which lets its decoder's
|
|
237
|
+
# ValueError escape here: every other failure this client can produce is a
|
|
238
|
+
# SourcesError, so a 2xx that isn't JSON is one too.
|
|
239
|
+
raise SourcesUnavailableError.new(
|
|
240
|
+
"ankusa admin API returned a non-JSON body (#{response.status})",
|
|
241
|
+
status: response.status
|
|
242
|
+
)
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def raise_for_status(response)
|
|
246
|
+
status = response.status
|
|
247
|
+
return if status >= 200 && status < 300
|
|
248
|
+
|
|
249
|
+
body = Connection.error_body(response)
|
|
250
|
+
if status == 404
|
|
251
|
+
raise SourceNotFoundError.new("source not found (#{status}): #{body.inspect}", status: status, body: body)
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
if status == 400
|
|
255
|
+
raise SourceInvalidError.new(invalid_message(body, status), status: status, body: body)
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
if status == 409
|
|
259
|
+
if body.is_a?(Hash) && body["error"] == "source_store_read_only"
|
|
260
|
+
raise SourceStoreReadOnlyError.new(
|
|
261
|
+
"source store is read-only (#{status}): #{body.inspect}",
|
|
262
|
+
status: status,
|
|
263
|
+
body: body
|
|
264
|
+
)
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
raise SourceConflictError.new("source already exists (#{status}): #{body.inspect}", status: status, body: body)
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
raise SourcesUnavailableError.new("ankusa admin API error (#{status}): #{body.inspect}", status: status, body: body)
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def invalid_message(body, status)
|
|
274
|
+
if body.is_a?(Hash)
|
|
275
|
+
message = body["message"] || body["error"]
|
|
276
|
+
return message if message.is_a?(String)
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
"invalid source (#{status}): #{body.inspect}"
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# Reject a tenant that is not `[A-Za-z0-9_-]{1,64}` before any path is
|
|
283
|
+
# built.
|
|
284
|
+
def validate_tenant(tenant)
|
|
285
|
+
return if tenant.is_a?(String) && SAFE_ID.match?(tenant)
|
|
286
|
+
|
|
287
|
+
raise SourceInvalidError, "invalid tenant: #{tenant.inspect}"
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# Reject a source name that is not `[A-Za-z0-9_-]{1,64}` before any path is
|
|
291
|
+
# built.
|
|
292
|
+
def validate_name(name)
|
|
293
|
+
return if name.is_a?(String) && SAFE_ID.match?(name)
|
|
294
|
+
|
|
295
|
+
raise SourceInvalidError, "invalid source name: #{name.inspect}"
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "openssl"
|
|
5
|
+
require "uri"
|
|
6
|
+
require "zlib"
|
|
7
|
+
|
|
8
|
+
module Ankusa
|
|
9
|
+
# The injectable HTTP hook every client sends through.
|
|
10
|
+
#
|
|
11
|
+
# A transport is any object that responds to `call(Request) -> Response`,
|
|
12
|
+
# including a lambda. `Transport::NetHTTP` is the default, used when a client
|
|
13
|
+
# is built without one; tests inject a lambda and never touch the network.
|
|
14
|
+
module Transport
|
|
15
|
+
# One HTTP request. `http_method` is an upcased String -- not `method`,
|
|
16
|
+
# which would shadow `Object#method` -- and `url` is a full `URI::HTTP`.
|
|
17
|
+
Request = Data.define(:http_method, :url, :headers, :body)
|
|
18
|
+
|
|
19
|
+
# One HTTP response. `headers` has lowercase String keys; `body` is a binary
|
|
20
|
+
# String, `""` when the response carried no body.
|
|
21
|
+
Response = Data.define(:status, :headers, :body)
|
|
22
|
+
|
|
23
|
+
# Raised for any transport failure: connection refused, DNS failure, a
|
|
24
|
+
# timeout, a TLS error, a truncated response. An injected transport raises
|
|
25
|
+
# it too, to signal unreachable without a network.
|
|
26
|
+
class Error < Ankusa::Error
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# The default transport: one `Net::HTTP` request per call, no redirects, no
|
|
30
|
+
# retries, and one timeout for connecting, reading, and writing.
|
|
31
|
+
class NetHTTP
|
|
32
|
+
def initialize(timeout:)
|
|
33
|
+
@timeout = timeout
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def call(request)
|
|
37
|
+
url = request.url
|
|
38
|
+
# hostname, not host: for an IPv6 literal host is "[::1]" with the
|
|
39
|
+
# brackets still on, and Net::HTTP.addr_port adds them back from the
|
|
40
|
+
# bare address, so "[::1]" would go to DNS as written.
|
|
41
|
+
http = Net::HTTP.new(url.hostname, url.port)
|
|
42
|
+
http.use_ssl = url.scheme == "https"
|
|
43
|
+
http.open_timeout = @timeout
|
|
44
|
+
http.read_timeout = @timeout
|
|
45
|
+
http.write_timeout = @timeout
|
|
46
|
+
# Net::HTTP retries idempotent requests (GET, PUT, DELETE, ...) once by
|
|
47
|
+
# default, which would send -- and so in the conformance vectors,
|
|
48
|
+
# record -- a second request.
|
|
49
|
+
http.max_retries = 0
|
|
50
|
+
|
|
51
|
+
http_request = Net::HTTPGenericRequest.new(
|
|
52
|
+
request.http_method,
|
|
53
|
+
!request.body.nil?,
|
|
54
|
+
request.http_method != "HEAD",
|
|
55
|
+
url.request_uri,
|
|
56
|
+
request.headers
|
|
57
|
+
)
|
|
58
|
+
http_request.body = request.body unless request.body.nil?
|
|
59
|
+
|
|
60
|
+
response = http.request(http_request)
|
|
61
|
+
Response.new(
|
|
62
|
+
status: response.code.to_i,
|
|
63
|
+
headers: response.each_header.to_h,
|
|
64
|
+
body: (response.body || "").b
|
|
65
|
+
)
|
|
66
|
+
rescue SystemCallError, IOError, SocketError, Timeout::Error, OpenSSL::SSL::SSLError,
|
|
67
|
+
Net::ProtocolError, Net::HTTPBadResponse, Zlib::Error => e
|
|
68
|
+
raise Error, "#{e.class}: #{e.message}"
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ankusa
|
|
4
|
+
# The identity of one HTTP-sink delivery: the headers Ankusa's HTTP sink
|
|
5
|
+
# attaches to every delivery.
|
|
6
|
+
#
|
|
7
|
+
# `content_type` and `tenant` are nil when the delivery had no such header
|
|
8
|
+
# (a tenant only travels when the source has one).
|
|
9
|
+
HookHeaders = Data.define(:id, :source, :tenant, :content_type)
|
|
10
|
+
|
|
11
|
+
# Raised by `Ankusa.parse_headers` when `x-ankusa-id` is absent or empty.
|
|
12
|
+
#
|
|
13
|
+
# Every other Ankusa header is optional; this one is the identity a receiver
|
|
14
|
+
# dedupes on, so a delivery without it is a framework bug, not a malformed but
|
|
15
|
+
# tolerable request.
|
|
16
|
+
class MissingHookIdError < Error
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Parses the `x-ankusa-*` headers of one delivery, whatever the mapping's own
|
|
20
|
+
# case behavior: lookup here is always case-insensitive.
|
|
21
|
+
#
|
|
22
|
+
# See "HTTP handoff" in docs/integrations.md for the delivery contract this
|
|
23
|
+
# mirrors. A receiver must dedupe on `x-ankusa-id`: delivery is at-least-once,
|
|
24
|
+
# so the same hook can arrive twice after a retry.
|
|
25
|
+
def self.parse_headers(headers)
|
|
26
|
+
lowered = headers.to_h { |key, value| [key.to_s.downcase, value] }
|
|
27
|
+
|
|
28
|
+
hook_id = lowered["x-ankusa-id"]
|
|
29
|
+
raise MissingHookIdError, "missing x-ankusa-id header" if hook_id.nil? || hook_id.empty?
|
|
30
|
+
|
|
31
|
+
HookHeaders.new(
|
|
32
|
+
id: hook_id,
|
|
33
|
+
source: lowered.fetch("x-ankusa-source", ""),
|
|
34
|
+
tenant: lowered["x-ankusa-tenant"],
|
|
35
|
+
content_type: lowered["content-type"]
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: ankusa-sdk
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.3.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- James Carr
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: Client SDK for Ankusa deployments. Bundles the claim-check gateway client,
|
|
13
|
+
the route-management and operator (admin) clients, the source-management client,
|
|
14
|
+
and a webhook-receiving header helper; more clients (ingest) land here as they're
|
|
15
|
+
built.
|
|
16
|
+
executables: []
|
|
17
|
+
extensions: []
|
|
18
|
+
extra_rdoc_files: []
|
|
19
|
+
files:
|
|
20
|
+
- CHANGELOG.md
|
|
21
|
+
- LICENSE
|
|
22
|
+
- README.md
|
|
23
|
+
- lib/ankusa/admin.rb
|
|
24
|
+
- lib/ankusa/claim_check.rb
|
|
25
|
+
- lib/ankusa/connection.rb
|
|
26
|
+
- lib/ankusa/error.rb
|
|
27
|
+
- lib/ankusa/routes.rb
|
|
28
|
+
- lib/ankusa/sdk.rb
|
|
29
|
+
- lib/ankusa/sdk/version.rb
|
|
30
|
+
- lib/ankusa/sources.rb
|
|
31
|
+
- lib/ankusa/transport.rb
|
|
32
|
+
- lib/ankusa/webhook.rb
|
|
33
|
+
homepage: https://github.com/jamescarr/ankusa/tree/main/packages/sdk-ruby
|
|
34
|
+
licenses:
|
|
35
|
+
- Apache-2.0
|
|
36
|
+
metadata:
|
|
37
|
+
homepage_uri: https://github.com/jamescarr/ankusa/tree/main/packages/sdk-ruby
|
|
38
|
+
source_code_uri: https://github.com/jamescarr/ankusa
|
|
39
|
+
bug_tracker_uri: https://github.com/jamescarr/ankusa/issues
|
|
40
|
+
changelog_uri: https://github.com/jamescarr/ankusa/blob/main/packages/sdk-ruby/CHANGELOG.md
|
|
41
|
+
rdoc_options: []
|
|
42
|
+
require_paths:
|
|
43
|
+
- lib
|
|
44
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
45
|
+
requirements:
|
|
46
|
+
- - ">="
|
|
47
|
+
- !ruby/object:Gem::Version
|
|
48
|
+
version: '3.3'
|
|
49
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0'
|
|
54
|
+
requirements: []
|
|
55
|
+
rubygems_version: 4.0.20
|
|
56
|
+
specification_version: 4
|
|
57
|
+
summary: Client SDK for Ankusa deployments.
|
|
58
|
+
test_files: []
|