k9o 1.1.1-aarch64-linux → 1.5.0-aarch64-linux
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 +4 -4
- data/CHANGELOG.md +118 -0
- data/README.md +1573 -176
- data/k9o.gemspec +2 -2
- data/lib/k9o/_ffi/kibo_kms_rest_ffi.rb +928 -25
- data/lib/k9o/_ffi/libkibo_kms_rest_ffi.so +0 -0
- data/lib/k9o/ai.rb +15 -0
- data/lib/k9o/analytics.rb +403 -0
- data/lib/k9o/approvals.rb +253 -0
- data/lib/k9o/audit.rb +162 -14
- data/lib/k9o/client.rb +28 -9
- data/lib/k9o/compute.rb +32 -15
- data/lib/k9o/crypto.rb +471 -0
- data/lib/k9o/custody.rb +28 -0
- data/lib/k9o/data.rb +125 -5
- data/lib/k9o/ed25519.rb +182 -0
- data/lib/k9o/end_users.rb +93 -0
- data/lib/k9o/envelope.rb +20 -5
- data/lib/k9o/functions.rb +127 -9
- data/lib/k9o/gate.rb +52 -22
- data/lib/k9o/http_transport.rb +78 -16
- data/lib/k9o/jcs.rb +117 -0
- data/lib/k9o/kerror.rb +57 -5
- data/lib/k9o/keys.rb +131 -0
- data/lib/k9o/kibo.rb +3 -3
- data/lib/k9o/kibo_client_core.rb +385 -0
- data/lib/k9o/kibo_ffi_core.rb +178 -0
- data/lib/k9o/locate.rb +118 -0
- data/lib/k9o/me.rb +27 -87
- data/lib/k9o/metadata.rb +342 -0
- data/lib/k9o/moat.rb +443 -0
- data/lib/k9o/org.rb +81 -68
- data/lib/k9o/orgs.rb +1 -0
- data/lib/k9o/policies.rb +25 -6
- data/lib/k9o/projects.rb +43 -8
- data/lib/k9o/query.rb +174 -0
- data/lib/k9o/rampart.rb +273 -0
- data/lib/k9o/realtime.rb +261 -32
- data/lib/k9o/region.rb +20 -18
- data/lib/k9o/schedules.rb +109 -0
- data/lib/k9o/schema.rb +35 -0
- data/lib/k9o/streams.rb +94 -0
- data/lib/k9o/studio.rb +21 -11
- data/lib/k9o/url_grammar.rb +46 -20
- data/lib/k9o/vault.rb +79 -84
- data/lib/k9o/vectors.rb +124 -0
- data/lib/k9o/version.rb +1 -1
- data/lib/k9o.rb +17 -0
- data/sig/k9o/ai.rbs +4 -0
- data/sig/k9o/approvals.rbs +71 -0
- data/sig/k9o/datasets.rbs +125 -0
- data/sig/k9o/gate.rbs +9 -6
- data/sig/k9o/kerror.rbs +24 -0
- data/sig/k9o/kibo_client_core.rbs +25 -0
- data/sig/k9o/metadata.rbs +54 -0
- data/sig/k9o/moat.rbs +29 -0
- data/sig/k9o/query.rbs +26 -0
- data/sig/k9o/rampart.rbs +59 -0
- data/sig/k9o/realtime.rbs +34 -0
- data/sig/k9o/region.rbs +14 -3
- data/sig/k9o/workload.rbs +64 -0
- metadata +33 -4
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module K9o
|
|
4
|
+
# Schedules: k9o.schedules.* run a function on a timetable (cron). They need the project admin role to change.
|
|
5
|
+
#
|
|
6
|
+
# list(status:, function_name:, limit:, cursor:) GET /@<slug>/forge/schedules
|
|
7
|
+
# get(id) GET /@<slug>/forge/schedules/<id>
|
|
8
|
+
# create(function_name:, schedule_expression:, ...) POST /@<slug>/forge/schedules
|
|
9
|
+
# update(id, **fields) PATCH /@<slug>/forge/schedules/<id>
|
|
10
|
+
# delete(id) DELETE /@<slug>/forge/schedules/<id> (204, no body)
|
|
11
|
+
# runs(id, limit:, cursor:) GET /@<slug>/forge/schedules/<id>/runs
|
|
12
|
+
# run_now(id) POST /@<slug>/forge/schedules/<id>/run-now (202)
|
|
13
|
+
#
|
|
14
|
+
# Lists are cursor-paginated, newest first: pass +meta.cursor.next+ of one page as +cursor:+ of the next, until it
|
|
15
|
+
# is nil. A schedule interval under 5 minutes is rejected with 400 K9O_INVALID_FIELD_VALUE (field schedule_expression), on every plan.
|
|
16
|
+
class Schedules
|
|
17
|
+
CATCHUP_POLICIES = %w[skip run_once run_all].freeze
|
|
18
|
+
STATUSES = %w[active paused].freeze
|
|
19
|
+
UPDATE_FIELDS = %i[schedule_expression schedule_timezone catchup_policy status enabled log_ok_invocations].freeze
|
|
20
|
+
|
|
21
|
+
def initialize(config)
|
|
22
|
+
@config = config
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Schedules, newest first. +status+ is active or paused; +function_name+ keeps one function's schedules;
|
|
26
|
+
# +limit+ is 1 to 200 (the gateway defaults to 50).
|
|
27
|
+
def list(status: nil, function_name: nil, limit: nil, cursor: nil)
|
|
28
|
+
ApprovalChecks.check_limit(limit)
|
|
29
|
+
query = {}
|
|
30
|
+
query['status'] = ApprovalChecks.enum!(status, STATUSES, 'status') unless status.nil?
|
|
31
|
+
query['function_name'] = function_name unless function_name.nil?
|
|
32
|
+
query['limit'] = limit unless limit.nil?
|
|
33
|
+
query['cursor'] = cursor unless cursor.nil?
|
|
34
|
+
call('GET', path, query: query)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def get(id)
|
|
38
|
+
call('GET', path(one(id)))
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Create a schedule; answers 201 with the schedule. +schedule_expression+ is a five-field cron or one of @yearly,
|
|
42
|
+
# @annually, @monthly, @weekly, @daily, @midnight, @hourly (stored and returned expanded). +schedule_timezone+
|
|
43
|
+
# defaults to UTC; +catchup_policy+ to skip.
|
|
44
|
+
def create(function_name:, schedule_expression:, schedule_timezone: nil, catchup_policy: nil, collection: nil,
|
|
45
|
+
enabled: nil, log_ok_invocations: nil)
|
|
46
|
+
body = { 'functionName' => function_name, 'scheduleExpression' => schedule_expression }
|
|
47
|
+
body['scheduleTimezone'] = schedule_timezone unless schedule_timezone.nil?
|
|
48
|
+
body['catchupPolicy'] = ApprovalChecks.enum!(catchup_policy, CATCHUP_POLICIES, 'catchup_policy') unless catchup_policy.nil?
|
|
49
|
+
body['collection'] = collection unless collection.nil?
|
|
50
|
+
body['enabled'] = enabled unless enabled.nil?
|
|
51
|
+
body['logOkInvocations'] = log_ok_invocations unless log_ok_invocations.nil?
|
|
52
|
+
call('POST', path, body: body)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Change a schedule; send at least one of +schedule_expression+, +schedule_timezone+, +catchup_policy+,
|
|
56
|
+
# +status+ ('paused' stops it, 'active' resumes it), +enabled+, +log_ok_invocations+. The function cannot change.
|
|
57
|
+
def update(id, **fields)
|
|
58
|
+
unknown = fields.keys - UPDATE_FIELDS
|
|
59
|
+
raise ApprovalChecks.invalid("unknown field: #{unknown.join(', ')}") unless unknown.empty?
|
|
60
|
+
raise ApprovalChecks.invalid("patch needs at least one of: #{UPDATE_FIELDS.join(', ')}") if fields.empty?
|
|
61
|
+
|
|
62
|
+
seg = one(id)
|
|
63
|
+
body = {}
|
|
64
|
+
body['scheduleExpression'] = fields[:schedule_expression] if fields.key?(:schedule_expression)
|
|
65
|
+
body['scheduleTimezone'] = fields[:schedule_timezone] if fields.key?(:schedule_timezone)
|
|
66
|
+
if fields.key?(:catchup_policy)
|
|
67
|
+
body['catchupPolicy'] = ApprovalChecks.enum!(fields[:catchup_policy], CATCHUP_POLICIES, 'catchup_policy')
|
|
68
|
+
end
|
|
69
|
+
body['status'] = ApprovalChecks.enum!(fields[:status], STATUSES, 'status') if fields.key?(:status)
|
|
70
|
+
body['enabled'] = fields[:enabled] if fields.key?(:enabled)
|
|
71
|
+
body['logOkInvocations'] = fields[:log_ok_invocations] if fields.key?(:log_ok_invocations)
|
|
72
|
+
call('PATCH', path(seg), body: body)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Delete a schedule. Answers 204, so +data+ is nil.
|
|
76
|
+
def delete(id)
|
|
77
|
+
call('DELETE', path(one(id)))
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# The schedule's runs, newest first, one page at a time.
|
|
81
|
+
def runs(id, limit: nil, cursor: nil)
|
|
82
|
+
ApprovalChecks.check_limit(limit)
|
|
83
|
+
seg = one(id)
|
|
84
|
+
query = {}
|
|
85
|
+
query['limit'] = limit unless limit.nil?
|
|
86
|
+
query['cursor'] = cursor unless cursor.nil?
|
|
87
|
+
call('GET', path("#{seg}/runs"), query: query)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Start one run now, outside the timetable. Answers 202; +data+ is the body exactly as the gateway sent it.
|
|
91
|
+
def run_now(id)
|
|
92
|
+
call('POST', path("#{one(id)}/run-now"))
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
private
|
|
96
|
+
|
|
97
|
+
def one(id)
|
|
98
|
+
"/#{ApprovalChecks.id_segment(id, 'schedule')}"
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def call(method, route, body: nil, query: nil)
|
|
102
|
+
HttpTransport.request(config: @config, method: method, path: route, body: body, query: query)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def path(tail = '')
|
|
106
|
+
UrlGrammar.project_path(Studio.require_project_slug(@config, 'schedules'), "forge/schedules#{tail}")
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
data/lib/k9o/schema.rb
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module K9o
|
|
4
|
+
# Schema: k9o.schema.*
|
|
5
|
+
#
|
|
6
|
+
# collections.list(limit:, cursor:) GET /@<slug>/schema/collections
|
|
7
|
+
#
|
|
8
|
+
# The list is cursor-paginated and ordered by collection name: pass +meta.cursor.next+ of one page as +cursor+ of
|
|
9
|
+
# the next, until it is nil.
|
|
10
|
+
class Schema
|
|
11
|
+
attr_reader :collections
|
|
12
|
+
|
|
13
|
+
def initialize(config)
|
|
14
|
+
@collections = SchemaCollections.new(config)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
class SchemaCollections
|
|
19
|
+
def initialize(config)
|
|
20
|
+
@config = config
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# The project's collections by name, one page at a time. +limit+ is 1 to 200 (default 50).
|
|
24
|
+
def list(limit: nil, cursor: nil)
|
|
25
|
+
route = UrlGrammar.project_path(Studio.require_project_slug(@config, 'schema'), 'schema/collections')
|
|
26
|
+
ApprovalChecks.check_limit(limit)
|
|
27
|
+
raise ApprovalChecks.invalid('cursor must be a string', cursor: cursor) unless cursor.nil? || cursor.is_a?(String)
|
|
28
|
+
|
|
29
|
+
query = {}
|
|
30
|
+
query['limit'] = limit unless limit.nil?
|
|
31
|
+
query['cursor'] = cursor unless cursor.nil?
|
|
32
|
+
HttpTransport.request(config: @config, method: 'GET', path: route, query: query)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
data/lib/k9o/streams.rb
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module K9o
|
|
4
|
+
# User data streams namespace: k9o.streams.*.
|
|
5
|
+
#
|
|
6
|
+
# Routes under /@<slug>/data-streams/user. Publish needs project admin; subscribe and replay need admin or
|
|
7
|
+
# auditor. While the stream data plane is unavailable the gateway answers K9O_STREAM_UNAVAILABLE (503); a bad
|
|
8
|
+
# subject answers K9O_STREAM_INVALID. Both raise K9o::KError.
|
|
9
|
+
class Streams
|
|
10
|
+
@reconnect_delay = 0.25
|
|
11
|
+
|
|
12
|
+
class << self
|
|
13
|
+
# Seconds to wait before reconnecting after the server closes a subscribe connection.
|
|
14
|
+
attr_accessor :reconnect_delay
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def initialize(config)
|
|
18
|
+
@config = config
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# POST /@<slug>/data-streams/user — +max_bytes+ is the storage ceiling; the slug is derived from +display_name+.
|
|
22
|
+
def create(display_name, max_bytes, retention_seconds: nil)
|
|
23
|
+
body = { 'displayName' => display_name, 'maxBytes' => max_bytes }
|
|
24
|
+
body['retentionSeconds'] = retention_seconds unless retention_seconds.nil?
|
|
25
|
+
call('POST', path, body)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# GET /@<slug>/data-streams/user — the project's active user streams.
|
|
29
|
+
def list
|
|
30
|
+
call('GET', path)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# GET /@<slug>/data-streams/user/<slug>
|
|
34
|
+
def get(slug)
|
|
35
|
+
call('GET', path(slug))
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# DELETE /@<slug>/data-streams/user/<slug> — retire the stream.
|
|
39
|
+
def delete(slug)
|
|
40
|
+
call('DELETE', path(slug))
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# POST .../<slug>/publish — +data+ is any JSON value; +subject+ is an optional dot-separated routing suffix.
|
|
44
|
+
def publish(slug, data, subject: nil)
|
|
45
|
+
body = { 'data' => data }
|
|
46
|
+
body['subject'] = subject unless subject.nil?
|
|
47
|
+
call('POST', path(slug, 'publish'), body)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# POST .../<slug>/replay — a bounded range of past events: data['events'], data['nextCursor'], data['hasMore'].
|
|
51
|
+
def replay(slug, from_sequence: nil, until_sequence: nil, limit: nil)
|
|
52
|
+
body = {}
|
|
53
|
+
body['fromSequence'] = from_sequence unless from_sequence.nil?
|
|
54
|
+
body['untilSequence'] = until_sequence unless until_sequence.nil?
|
|
55
|
+
body['limit'] = limit unless limit.nil?
|
|
56
|
+
call('POST', path(slug, 'replay'), body)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# GET .../<slug>/subscribe — an Enumerator tailing live events { 'sequence', 'subject', 'data', 'publishedAt' }.
|
|
60
|
+
# The gateway bounds each connection and closes it; the enumerator reconnects from the last sequence it yielded.
|
|
61
|
+
# Stop by leaving the +each+ block (break) or by taking a finite number (+first(n)+).
|
|
62
|
+
def subscribe(slug, from_sequence: nil)
|
|
63
|
+
route = path(slug, 'subscribe')
|
|
64
|
+
Enumerator.new do |yielder|
|
|
65
|
+
cursor = from_sequence
|
|
66
|
+
loop do
|
|
67
|
+
query = cursor.nil? ? nil : { 'fromSequence' => cursor.to_s }
|
|
68
|
+
HttpTransport.stream_sse(config: @config, method: 'GET', path: route, query: query).each do |event|
|
|
69
|
+
next unless event.is_a?(Hash) && event.key?('sequence')
|
|
70
|
+
|
|
71
|
+
cursor = event['sequence']
|
|
72
|
+
yielder << event
|
|
73
|
+
end
|
|
74
|
+
delay = self.class.reconnect_delay
|
|
75
|
+
sleep(delay) if delay.positive?
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
def call(method, route, body = nil)
|
|
83
|
+
HttpTransport.request(config: @config, method: method, path: route, body: body)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def path(stream_slug = nil, suffix = nil)
|
|
87
|
+
project = Studio.require_project_slug(@config, 'streams')
|
|
88
|
+
sub = +'data-streams/user'
|
|
89
|
+
sub << "/#{::URI.encode_www_form_component(stream_slug)}" if stream_slug
|
|
90
|
+
sub << "/#{suffix}" if suffix
|
|
91
|
+
UrlGrammar.project_path(project, sub)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
data/lib/k9o/studio.rb
CHANGED
|
@@ -25,7 +25,7 @@ module K9o
|
|
|
25
25
|
slug
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
-
# k9o.studio.workflows
|
|
28
|
+
# k9o.studio.workflows.*
|
|
29
29
|
class Workflows
|
|
30
30
|
attr_reader :runs
|
|
31
31
|
|
|
@@ -34,17 +34,19 @@ module K9o
|
|
|
34
34
|
@runs = Runs.new(config)
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
# GET /@<slug>/workflows — the route filters by +status+ and pages with +limit+ / +cursor+.
|
|
38
|
+
def list(status: nil, limit: nil, cursor: nil)
|
|
38
39
|
slug = Studio.require_project_slug(@config, 'studio.workflows')
|
|
39
40
|
q = {}
|
|
40
41
|
q['status'] = status if status
|
|
41
|
-
q['triggerType'] = trigger_type if trigger_type
|
|
42
42
|
q['limit'] = limit if limit
|
|
43
43
|
q['cursor'] = cursor if cursor
|
|
44
44
|
HttpTransport.request(config: @config, method: 'GET',
|
|
45
45
|
path: UrlGrammar.project_path(slug, 'workflows'), query: q)
|
|
46
46
|
end
|
|
47
47
|
|
|
48
|
+
# POST /@<slug>/workflows. +workflow+ is the gateway's body: 'name', 'triggerType', 'triggerConfig',
|
|
49
|
+
# 'actions' (each { 'index', 'action_type', 'config', ... }), optional 'description', 'status', 'rbacBinding'.
|
|
48
50
|
def create(workflow)
|
|
49
51
|
slug = Studio.require_project_slug(@config, 'studio.workflows')
|
|
50
52
|
HttpTransport.request(config: @config, method: 'POST',
|
|
@@ -69,12 +71,12 @@ module K9o
|
|
|
69
71
|
path: UrlGrammar.project_path(slug, "workflows/#{encode(workflow_id)}"))
|
|
70
72
|
end
|
|
71
73
|
|
|
72
|
-
# Manual-trigger a run (
|
|
74
|
+
# Manual-trigger a run (manual workflows only): POST /@<slug>/workflows/<id>/runs { triggerContext }.
|
|
73
75
|
def trigger(workflow_id, trigger_context = {})
|
|
74
76
|
slug = Studio.require_project_slug(@config, 'studio.workflows')
|
|
75
77
|
HttpTransport.request(config: @config, method: 'POST',
|
|
76
78
|
path: UrlGrammar.project_path(slug, "workflows/#{encode(workflow_id)}/runs"),
|
|
77
|
-
body: trigger_context)
|
|
79
|
+
body: { 'triggerContext' => trigger_context })
|
|
78
80
|
end
|
|
79
81
|
|
|
80
82
|
private
|
|
@@ -83,15 +85,18 @@ module K9o
|
|
|
83
85
|
::URI.encode_www_form_component(s)
|
|
84
86
|
end
|
|
85
87
|
|
|
86
|
-
# k9o.studio.workflows.runs
|
|
88
|
+
# k9o.studio.workflows.runs.*
|
|
87
89
|
class Runs
|
|
88
90
|
def initialize(config)
|
|
89
91
|
@config = config
|
|
90
92
|
end
|
|
91
93
|
|
|
92
|
-
|
|
94
|
+
# GET /@<slug>/workflows/<id>/runs — pages with +limit+ / +cursor+ (the route has no status filter).
|
|
95
|
+
def list(workflow_id, limit: nil, cursor: nil)
|
|
93
96
|
slug = Studio.require_project_slug(@config, 'studio.workflows')
|
|
94
|
-
q =
|
|
97
|
+
q = {}
|
|
98
|
+
q['limit'] = limit if limit
|
|
99
|
+
q['cursor'] = cursor if cursor
|
|
95
100
|
HttpTransport.request(config: @config, method: 'GET',
|
|
96
101
|
path: UrlGrammar.project_path(slug, "workflows/#{enc(workflow_id)}/runs"), query: q)
|
|
97
102
|
end
|
|
@@ -116,15 +121,20 @@ module K9o
|
|
|
116
121
|
end
|
|
117
122
|
end
|
|
118
123
|
|
|
119
|
-
# k9o.studio.dashboards
|
|
124
|
+
# k9o.studio.dashboards.* — widget fields on the wire are snake_case ('widget_type', 'data_source', 'config').
|
|
120
125
|
class Dashboards
|
|
121
126
|
def initialize(config)
|
|
122
127
|
@config = config
|
|
123
128
|
end
|
|
124
129
|
|
|
125
|
-
|
|
130
|
+
# GET /@<slug>/dashboards — filters: +status+, +pinned_only+; pages with +limit+ / +cursor+.
|
|
131
|
+
def list(status: nil, pinned_only: nil, limit: nil, cursor: nil)
|
|
126
132
|
slug = Studio.require_project_slug(@config, 'studio.dashboards')
|
|
127
|
-
q =
|
|
133
|
+
q = {}
|
|
134
|
+
q['status'] = status if status
|
|
135
|
+
q['pinnedOnly'] = pinned_only.to_s unless pinned_only.nil?
|
|
136
|
+
q['limit'] = limit if limit
|
|
137
|
+
q['cursor'] = cursor if cursor
|
|
128
138
|
HttpTransport.request(config: @config, method: 'GET',
|
|
129
139
|
path: UrlGrammar.project_path(slug, 'dashboards'), query: q)
|
|
130
140
|
end
|
data/lib/k9o/url_grammar.rb
CHANGED
|
@@ -7,11 +7,18 @@ module K9o
|
|
|
7
7
|
module UrlGrammar
|
|
8
8
|
SLUG_RE = /\A[a-z][a-z0-9-]{1,62}\z/
|
|
9
9
|
|
|
10
|
-
#
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
#
|
|
14
|
-
|
|
10
|
+
# The convenience default when the caller passes no base_url. A default value only: never used to parse a host.
|
|
11
|
+
DEFAULT_BASE_URL = 'https://api.k9o.io'
|
|
12
|
+
|
|
13
|
+
# `api` or `api-staging`, optionally followed by `-<rgn>`: the label that marks a gateway host.
|
|
14
|
+
API_LABEL = /\A(api|api-staging)(?:-([a-z]{2}\d{2}))?\z/.freeze
|
|
15
|
+
|
|
16
|
+
# The structure of a gateway API host, read from the host itself.
|
|
17
|
+
# org_slug: first label of an org host (+acme+ in +acme.api.corp.internal+); nil for a platform host.
|
|
18
|
+
# family: +api+ or +api-staging+.
|
|
19
|
+
# region: 4-character region code when the host is regional (+api-de01+), else nil.
|
|
20
|
+
# apex: everything after the gateway label; never matched against a list.
|
|
21
|
+
ApiHost = Struct.new(:org_slug, :family, :region, :apex)
|
|
15
22
|
|
|
16
23
|
module_function
|
|
17
24
|
|
|
@@ -65,24 +72,43 @@ module K9o
|
|
|
65
72
|
"#{uri.scheme}://#{host_port}"
|
|
66
73
|
end
|
|
67
74
|
|
|
68
|
-
#
|
|
69
|
-
#
|
|
70
|
-
# (
|
|
71
|
-
#
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
75
|
+
# Read the org slug and region out of a gateway host without knowing its apex. The gateway label is +api+ or
|
|
76
|
+
# +api-staging+, optionally followed by +-<rgn>+; the label before it is the org slug and whatever follows it is
|
|
77
|
+
# the apex. Returns nil for any other shape (localhost, an IP, a single-label host, a host with no gateway
|
|
78
|
+
# label), which callers use exactly as given.
|
|
79
|
+
def parse_api_host(host)
|
|
80
|
+
labels = host.to_s.downcase.split('.', -1)
|
|
81
|
+
return nil if labels.empty? || labels.any?(&:empty?)
|
|
82
|
+
|
|
83
|
+
build = lambda do |gateway_label, org_slug, rest|
|
|
84
|
+
m = API_LABEL.match(gateway_label)
|
|
85
|
+
next nil if m.nil? || rest.empty?
|
|
86
|
+
|
|
87
|
+
ApiHost.new(org_slug, m[1], m[2], rest.join('.'))
|
|
88
|
+
end
|
|
89
|
+
# Org host first: <org>.<api[-rgn]>.<apex>.
|
|
90
|
+
if labels.length >= 3 && labels[0].match?(SLUG_RE) && !labels[0].end_with?('-') && API_LABEL.match?(labels[1])
|
|
91
|
+
return build.call(labels[1], labels[0], labels[2..-1])
|
|
92
|
+
end
|
|
93
|
+
# Platform host: api.<apex>.
|
|
94
|
+
return build.call(labels[0], nil, labels[1..-1]) if labels.length >= 2 && API_LABEL.match?(labels[0])
|
|
95
|
+
|
|
96
|
+
nil
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Resolve the effective base_url. base_url is OPTIONAL: omitted, it defaults to DEFAULT_BASE_URL (the public
|
|
100
|
+
# cloud, a convenience only). When the (validated) host is a platform host (+api.<apex>+ or +api-staging.<apex>+,
|
|
101
|
+
# any apex) and org_slug is supplied, the canonical org host +<org_slug>.<host>+ is composed. Any other host (an
|
|
102
|
+
# org host already, an internal or air-gapped host, localhost) passes through unchanged; org_slug is then only
|
|
103
|
+
# sent as a header.
|
|
80
104
|
def resolve_base_url(base_url, org_slug = nil)
|
|
81
|
-
validated = validate_base_url(base_url.nil? ?
|
|
105
|
+
validated = validate_base_url(base_url.nil? ? DEFAULT_BASE_URL : base_url)
|
|
82
106
|
uri = URI.parse(validated)
|
|
83
|
-
|
|
107
|
+
host = parse_api_host(uri.host)
|
|
108
|
+
if !host.nil? && host.org_slug.nil? && host.region.nil? && org_slug
|
|
84
109
|
validate_slug(org_slug, 'org')
|
|
85
|
-
|
|
110
|
+
host_port = uri.port && uri.port != uri.default_port ? "#{uri.host}:#{uri.port}" : uri.host
|
|
111
|
+
return "#{uri.scheme}://#{org_slug}.#{host_port}"
|
|
86
112
|
end
|
|
87
113
|
validated
|
|
88
114
|
end
|