xapixctl 1.1.0 → 1.2.2
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/.github/workflows/cd.yaml +17 -0
- data/.rubocop.yml +5 -1
- data/.ruby-version +1 -1
- data/Gemfile.lock +57 -40
- data/Rakefile +1 -1
- data/lib/xapixctl.rb +0 -3
- data/lib/xapixctl/base_cli.rb +75 -0
- data/lib/xapixctl/cli.rb +59 -159
- data/lib/xapixctl/connector_cli.rb +49 -0
- data/lib/xapixctl/phoenix_client.rb +38 -247
- data/lib/xapixctl/phoenix_client/connection.rb +50 -0
- data/lib/xapixctl/phoenix_client/organization_connection.rb +61 -0
- data/lib/xapixctl/phoenix_client/project_connection.rb +184 -0
- data/lib/xapixctl/phoenix_client/result_handler.rb +35 -0
- data/lib/xapixctl/preview_cli.rb +54 -0
- data/lib/xapixctl/sync_cli.rb +166 -0
- data/lib/xapixctl/titan_cli.rb +281 -0
- data/lib/xapixctl/util.rb +42 -0
- data/lib/xapixctl/version.rb +3 -1
- data/xapixctl.gemspec +13 -6
- metadata +67 -14
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'xapixctl/base_cli'
|
4
|
+
|
5
|
+
module Xapixctl
|
6
|
+
class ConnectorCli < BaseCli
|
7
|
+
option :schema_import, desc: "Resource id of an existing Schema import"
|
8
|
+
desc "import SPECFILE", "Create HTTP Connectors from Swagger / OpenAPI or SOAP WSDL files"
|
9
|
+
long_desc <<-LONGDESC
|
10
|
+
`xapixctl connectors import SPECFILE` will create HTTP Connectors from the given Swagger / OpenAPI or SOAP WSDL file.
|
11
|
+
|
12
|
+
Examples:
|
13
|
+
\x5> $ xapixctl connectors import ./swagger.json -p xapix/some-project
|
14
|
+
\x5> $ xapixctl connectors import ./swagger.json -p xapix/some-project --schema-import=existing-schema
|
15
|
+
LONGDESC
|
16
|
+
def import(spec_filename)
|
17
|
+
path = Pathname.new(spec_filename)
|
18
|
+
unless path.file? && path.readable?
|
19
|
+
warn "Cannot read #{path}, please check file exists and is readable"
|
20
|
+
exit 1
|
21
|
+
end
|
22
|
+
if options[:schema_import]
|
23
|
+
puts "uploading to update schema import '#{options[:schema_import]}': #{spec_filename}..."
|
24
|
+
result = prj_connection.update_schema_import(options[:schema_import], spec_filename)
|
25
|
+
puts "updated #{result.dig('resource', 'kind')} #{result.dig('resource', 'id')}"
|
26
|
+
else
|
27
|
+
puts "uploading as new import: #{spec_filename}..."
|
28
|
+
result = prj_connection.add_schema_import(spec_filename)
|
29
|
+
puts "created #{result.dig('resource', 'kind')} #{result.dig('resource', 'id')}"
|
30
|
+
end
|
31
|
+
|
32
|
+
[['issues', 'import'], ['validation_issues', 'validation']].each do |key, name|
|
33
|
+
issues = result.dig('schema_import', 'report', key)
|
34
|
+
if issues.any?
|
35
|
+
puts "\n#{name} issues:"
|
36
|
+
issues.each { |issue| puts " - #{issue}" }
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
updated_resources = result.dig('schema_import', 'updated_resources')
|
41
|
+
if updated_resources.any?
|
42
|
+
puts "\nconnectors:"
|
43
|
+
updated_resources.each { |resource| puts " - #{resource['kind']} #{resource['id']}" }
|
44
|
+
else
|
45
|
+
puts "\nno connectors created/updated."
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -1,45 +1,45 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'json'
|
4
|
+
require 'psych'
|
5
|
+
require 'rest_client'
|
6
|
+
require 'xapixctl/phoenix_client/result_handler'
|
7
|
+
require 'xapixctl/phoenix_client/organization_connection'
|
8
|
+
require 'xapixctl/phoenix_client/project_connection'
|
9
|
+
require 'xapixctl/phoenix_client/connection'
|
10
|
+
|
3
11
|
module Xapixctl
|
4
12
|
module PhoenixClient
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
@success_handler.call(res)
|
28
|
-
rescue RestClient::Exception => err
|
29
|
-
response = JSON.parse(err.response) rescue {}
|
30
|
-
@error_handler.call(err, response)
|
31
|
-
rescue SocketError, Errno::ECONNREFUSED => err
|
32
|
-
@error_handler.call(err, nil)
|
33
|
-
end
|
34
|
-
end
|
13
|
+
# sorting is intentional to reflect dependencies when exporting
|
14
|
+
SUPPORTED_RESOURCE_TYPES = %w[
|
15
|
+
Project
|
16
|
+
Ambassador
|
17
|
+
AuthScheme
|
18
|
+
Credential
|
19
|
+
Proxy
|
20
|
+
CacheConnection
|
21
|
+
Schema
|
22
|
+
DataSource
|
23
|
+
Pipeline
|
24
|
+
Service
|
25
|
+
ServiceInstall
|
26
|
+
EndpointGroup
|
27
|
+
Endpoint
|
28
|
+
StreamGroup
|
29
|
+
Stream
|
30
|
+
StreamProcessor
|
31
|
+
Scheduler
|
32
|
+
ApiPublishing
|
33
|
+
ApiPublishingRole
|
34
|
+
].freeze
|
35
35
|
|
36
36
|
TEXT_FORMATTERS = {
|
37
|
-
all: ->(data) { "id :
|
37
|
+
all: ->(data) { "id : %<id>s\nkind: %<kind>s\nname: %<name>s\n\n" % { id: data.dig('metadata', 'id'), kind: data['kind'], name: data.dig('definition', 'name') } }
|
38
38
|
}.freeze
|
39
39
|
|
40
40
|
FORMATTERS = {
|
41
41
|
json: ->(data) { JSON.pretty_generate(data) },
|
42
|
-
yaml: ->(data) { Psych.dump(data
|
42
|
+
yaml: ->(data) { Psych.dump(data) },
|
43
43
|
text: ->(data) { (TEXT_FORMATTERS[data.dig('metadata', 'type')] || TEXT_FORMATTERS[:all]).call(data) }
|
44
44
|
}.freeze
|
45
45
|
|
@@ -65,224 +65,15 @@ module Xapixctl
|
|
65
65
|
end
|
66
66
|
}.freeze
|
67
67
|
|
68
|
-
|
69
|
-
|
70
|
-
DEFAULT_ERROR_HANDLER = ->(err, _response) { warn "Could not get data: #{err}" }
|
71
|
-
|
72
|
-
# sorting is intentional to reflect dependencies when exporting
|
73
|
-
SUPPORTED_RESOURCE_TYPES = %w[
|
74
|
-
Project
|
75
|
-
Ambassador
|
76
|
-
AuthScheme
|
77
|
-
Credential
|
78
|
-
Proxy
|
79
|
-
CacheConnection
|
80
|
-
Schema
|
81
|
-
DataSource
|
82
|
-
Pipeline
|
83
|
-
EndpointGroup
|
84
|
-
Endpoint
|
85
|
-
StreamGroup
|
86
|
-
Stream
|
87
|
-
ApiPublishing
|
88
|
-
ApiPublishingRole
|
89
|
-
].freeze
|
90
|
-
|
91
|
-
def initialize(url, token)
|
92
|
-
@client = RestClient::Resource.new(File.join(url, 'api/v1'), verify_ssl: false, accept: :json, content_type: :json, headers: { Authorization: "Bearer #{token}" })
|
93
|
-
@default_success_handler = DEFAULT_SUCCESS_HANDLER
|
94
|
-
@default_error_handler = DEFAULT_ERROR_HANDLER
|
95
|
-
end
|
96
|
-
|
97
|
-
def on_success(&block); @default_success_handler = block; self; end
|
98
|
-
|
99
|
-
def on_error(&block); @default_error_handler = block; self; end
|
100
|
-
|
101
|
-
def resource(resource_type, resource_id, org:, project: nil, format: :hash, &block)
|
102
|
-
result_handler(block).
|
103
|
-
formatter(FORMATTERS[format]).
|
104
|
-
run { @client[resource_path(org, project, resource_type, resource_id)].get }
|
105
|
-
end
|
106
|
-
|
107
|
-
def resource_ids(resource_type, org:, project: nil, &block)
|
108
|
-
result_handler(block).
|
109
|
-
prepare_data(->(data) { data['resource_ids'] }).
|
110
|
-
run { @client[resources_path(org, project, resource_type)].get }
|
111
|
-
end
|
112
|
-
|
113
|
-
def apply(resource_description, org:, project: nil, &block)
|
114
|
-
result_handler(block).
|
115
|
-
run { @client[generic_resource_path(org, project)].put(resource_description.to_json) }
|
116
|
-
end
|
117
|
-
|
118
|
-
def delete(resource_type, resource_id, org:, project: nil, &block)
|
119
|
-
result_handler(block).
|
120
|
-
run { @client[resource_path(org, project, resource_type, resource_id)].delete }
|
121
|
-
end
|
122
|
-
|
123
|
-
def pipeline_preview(pipeline_id, org:, project:, format: :hash, &block)
|
124
|
-
result_handler(block).
|
125
|
-
prepare_data(->(data) { data['pipeline_preview'] }).
|
126
|
-
formatter(PREVIEW_FORMATTERS[format]).
|
127
|
-
run { @client[pipeline_preview_path(org, project, pipeline_id)].get }
|
128
|
-
end
|
129
|
-
|
130
|
-
def endpoint_preview(endpoint_id, org:, project:, format: :hash, &block)
|
131
|
-
result_handler(block).
|
132
|
-
prepare_data(->(data) { data['endpoint_preview'] }).
|
133
|
-
formatter(PREVIEW_FORMATTERS[format]).
|
134
|
-
run { @client[endpoint_preview_path(org, project, endpoint_id)].get }
|
135
|
-
end
|
136
|
-
|
137
|
-
def publish(org:, project:, &block)
|
138
|
-
result_handler(block).
|
139
|
-
run { @client[project_publications_path(org, project)].post('') }
|
140
|
-
end
|
141
|
-
|
142
|
-
def logs(correlation_id, org:, project:, &block)
|
143
|
-
result_handler(block).
|
144
|
-
run { @client[project_logss_path(org, project, correlation_id)].get }
|
145
|
-
end
|
146
|
-
|
147
|
-
def available_resource_types(&block)
|
148
|
-
result_handler(block).
|
149
|
-
prepare_data(->(data) { data['resource_types'] }).
|
150
|
-
run { @client[resource_types_path].get }
|
151
|
-
end
|
152
|
-
|
153
|
-
def resource_types_for_export
|
154
|
-
@resource_types_for_export ||=
|
155
|
-
available_resource_types do |res|
|
156
|
-
res.on_success { |available_types| SUPPORTED_RESOURCE_TYPES & available_types.map { |desc| desc['type'] } }
|
157
|
-
res.on_error { |err, _response| raise err }
|
158
|
-
end
|
159
|
-
end
|
160
|
-
|
161
|
-
def onboarding(org:, project:)
|
162
|
-
OnboardingConnection.new(@client, @default_success_handler, @default_error_handler, org, project)
|
163
|
-
end
|
164
|
-
|
165
|
-
private
|
166
|
-
|
167
|
-
def result_handler(block)
|
168
|
-
ResultHandler.new(default_success_handler: @default_success_handler, default_error_handler: @default_error_handler, &block)
|
169
|
-
end
|
170
|
-
|
171
|
-
def resource_path(org, project, type, id)
|
172
|
-
type = translate_type(type)
|
173
|
-
project ? "/projects/#{org}/#{project}/#{type}/#{id}" : "/orgs/#{org}/#{type}/#{id}"
|
174
|
-
end
|
175
|
-
|
176
|
-
def resources_path(org, project, type)
|
177
|
-
type = translate_type(type)
|
178
|
-
project ? "/projects/#{org}/#{project}/#{type}" : "/orgs/#{org}/#{type}"
|
179
|
-
end
|
180
|
-
|
181
|
-
def generic_resource_path(org, project)
|
182
|
-
project ? "projects/#{org}/#{project}/resource" : "orgs/#{org}/resource"
|
183
|
-
end
|
184
|
-
|
185
|
-
def pipeline_preview_path(org, project, pipeline)
|
186
|
-
"/projects/#{org}/#{project}/pipelines/#{pipeline}/preview"
|
187
|
-
end
|
188
|
-
|
189
|
-
def endpoint_preview_path(org, project, endpoint)
|
190
|
-
"/projects/#{org}/#{project}/endpoints/#{endpoint}/preview"
|
191
|
-
end
|
192
|
-
|
193
|
-
def project_publications_path(org, project)
|
194
|
-
"/projects/#{org}/#{project}/publications"
|
195
|
-
end
|
196
|
-
|
197
|
-
def project_logss_path(org, project, correlation_id)
|
198
|
-
"/projects/#{org}/#{project}/logs/#{correlation_id}"
|
199
|
-
end
|
68
|
+
DEFAULT_SUCCESS_HANDLER = ->(result) { result }
|
69
|
+
DEFAULT_ERROR_HANDLER = ->(err, _response) { warn "Could not get data: #{err}" }
|
200
70
|
|
201
|
-
|
202
|
-
|
203
|
-
end
|
204
|
-
|
205
|
-
def translate_type(resource_type)
|
206
|
-
return 'ApiPublishingRole' if resource_type == 'ApiPublishing/Role'
|
207
|
-
resource_type.sub(%r[/.*], '') # cut off everything after first slash
|
208
|
-
end
|
71
|
+
def self.connection(url, token, default_success_handler: DEFAULT_SUCCESS_HANDLER, default_error_handler: DEFAULT_ERROR_HANDLER, logging: nil)
|
72
|
+
Connection.new(url, token, default_success_handler, default_error_handler, logging)
|
209
73
|
end
|
210
74
|
|
211
|
-
|
212
|
-
|
213
|
-
@client = client
|
214
|
-
@default_success_handler = default_success_handler
|
215
|
-
@default_error_handler = default_error_handler
|
216
|
-
@org = org
|
217
|
-
@project = project
|
218
|
-
end
|
219
|
-
|
220
|
-
# Notes on parameters:
|
221
|
-
# - Query parameters should be part of the URL
|
222
|
-
# - Path parameters should be marked with `{name}` in the URL, and values should be given in path_params hash
|
223
|
-
# - Headers should be given in headers hash
|
224
|
-
# - Cookies should be given in cookies hash
|
225
|
-
# - The body has to be given as a string
|
226
|
-
# - The required authentication schemes should be listed, referring to previously created schemes
|
227
|
-
#
|
228
|
-
# This returns a hash like the following:
|
229
|
-
# "data_source" => { "id" => id, "resource_description" => resource_description }
|
230
|
-
#
|
231
|
-
# To successfully onboard a DB using the API, the following steps are needed:
|
232
|
-
# 1. setup the data source using add_rest_data_source.
|
233
|
-
# 2. retrieve a preview using preview_data_source using the id returned by previous step
|
234
|
-
# 3. confirm preview
|
235
|
-
# 4. call accept_data_source_preview to complete onboarding
|
236
|
-
#
|
237
|
-
def add_rest_data_source(http_method:, url:, path_params: {}, headers: {}, cookies: {}, body: nil, auth_schemes: [], &block)
|
238
|
-
data_source_details = {
|
239
|
-
http_method: http_method, url: url,
|
240
|
-
parameters: { path: path_params.to_query, header: headers.to_query, cookies: cookies.to_query, body: body },
|
241
|
-
auth_schemes: auth_schemes
|
242
|
-
}
|
243
|
-
result_handler(block).
|
244
|
-
run { @client[rest_data_source_path].post(data_source: data_source_details) }
|
245
|
-
end
|
246
|
-
|
247
|
-
# Notes on parameters:
|
248
|
-
# - To call a data source which requires authentication, provide a hash with each required auth scheme as key and
|
249
|
-
# as the value a reference to a previously created credential.
|
250
|
-
# Example: { scheme_ref1 => credential_ref1, scheme_ref2 => credential_ref2 }
|
251
|
-
#
|
252
|
-
# This returns a hashified preview like the following:
|
253
|
-
# { "preview" => {
|
254
|
-
# "sample" => { "status" => integer, "body" => { ... }, "headers" => { ... }, "cookies" => { ... } },
|
255
|
-
# "fetched_at" => Timestamp },
|
256
|
-
# "data_source" => { "id" => id, "resource_description" => resource_description } }
|
257
|
-
#
|
258
|
-
def preview_data_source(data_source_id, authentications: {}, &block)
|
259
|
-
preview_data = {
|
260
|
-
authentications: authentications.map { |scheme, cred| { auth_scheme_id: scheme, auth_credential_id: cred } }
|
261
|
-
}
|
262
|
-
result_handler(block).
|
263
|
-
run { @client[data_source_preview_path(data_source_id)].post(preview_data) }
|
264
|
-
end
|
265
|
-
|
266
|
-
# This returns a hashified preview like the following:
|
267
|
-
|
268
|
-
def accept_data_source_preview(data_source_id, &block)
|
269
|
-
result_handler(block).
|
270
|
-
run { @client[data_source_preview_path(data_source_id)].patch('') }
|
271
|
-
end
|
272
|
-
|
273
|
-
private
|
274
|
-
|
275
|
-
def result_handler(block)
|
276
|
-
ResultHandler.new(default_success_handler: @default_success_handler, default_error_handler: @default_error_handler, &block)
|
277
|
-
end
|
278
|
-
|
279
|
-
def rest_data_source_path
|
280
|
-
"/projects/#{@org}/#{@project}/onboarding/data_sources/rest"
|
281
|
-
end
|
282
|
-
|
283
|
-
def data_source_preview_path(id)
|
284
|
-
"/projects/#{@org}/#{@project}/onboarding/data_sources/#{id}/preview"
|
285
|
-
end
|
75
|
+
def self.supported_type?(type)
|
76
|
+
SUPPORTED_RESOURCE_TYPES.include?(type)
|
286
77
|
end
|
287
78
|
end
|
288
79
|
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Xapixctl
|
4
|
+
module PhoenixClient
|
5
|
+
class Connection
|
6
|
+
DEFAULT_CLIENT_OPTS = { verify_ssl: false, headers: { accept: :json } }.freeze
|
7
|
+
|
8
|
+
attr_reader :xapix_url, :client
|
9
|
+
|
10
|
+
def initialize(url, token, default_success_handler, default_error_handler, logging)
|
11
|
+
@xapix_url = url
|
12
|
+
client_opts = DEFAULT_CLIENT_OPTS.deep_merge(headers: { Authorization: "Bearer #{token}" })
|
13
|
+
client_opts.merge!(log: RestClient.create_log(logging)) if logging
|
14
|
+
@client = RestClient::Resource.new(File.join(url, 'api/v1'), client_opts)
|
15
|
+
@default_success_handler = default_success_handler
|
16
|
+
@default_error_handler = default_error_handler
|
17
|
+
end
|
18
|
+
|
19
|
+
def on_success(&block); @default_success_handler = block; self; end
|
20
|
+
|
21
|
+
def on_error(&block); @default_error_handler = block; self; end
|
22
|
+
|
23
|
+
def available_resource_types(&block)
|
24
|
+
@available_resource_types ||= begin
|
25
|
+
result_handler(block).
|
26
|
+
prepare_data(->(data) { data['resource_types'].freeze }).
|
27
|
+
run { @client[resource_types_path].get }
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def organization(org)
|
32
|
+
OrganizationConnection.new(self, org)
|
33
|
+
end
|
34
|
+
|
35
|
+
def project(org:, project:)
|
36
|
+
ProjectConnection.new(self, org, project)
|
37
|
+
end
|
38
|
+
|
39
|
+
def result_handler(block)
|
40
|
+
ResultHandler.new(default_success_handler: @default_success_handler, default_error_handler: @default_error_handler, &block)
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def resource_types_path
|
46
|
+
"/resource_types"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Xapixctl
|
4
|
+
module PhoenixClient
|
5
|
+
class OrganizationConnection
|
6
|
+
attr_reader :org
|
7
|
+
|
8
|
+
def initialize(connection, org)
|
9
|
+
@connection = connection
|
10
|
+
@client = connection.client
|
11
|
+
@org = org
|
12
|
+
end
|
13
|
+
|
14
|
+
def resource(resource_type, resource_id, format: :hash, &block)
|
15
|
+
result_handler(block).
|
16
|
+
formatter(FORMATTERS[format]).
|
17
|
+
run { @client[resource_path(resource_type, resource_id)].get }
|
18
|
+
end
|
19
|
+
|
20
|
+
def resource_ids(resource_type, &block)
|
21
|
+
result_handler(block).
|
22
|
+
prepare_data(->(data) { data['resource_ids'] }).
|
23
|
+
run { @client[resources_path(resource_type)].get }
|
24
|
+
end
|
25
|
+
|
26
|
+
def apply(resource_description, &block)
|
27
|
+
result_handler(block).
|
28
|
+
prepare_data(->(data) { data['resource_ids'] }).
|
29
|
+
run { @client[generic_resource_path].put(resource_description.to_json, content_type: :json) }
|
30
|
+
end
|
31
|
+
|
32
|
+
def delete(resource_type, resource_id, &block)
|
33
|
+
result_handler(block).
|
34
|
+
run { @client[resource_path(resource_type, resource_id)].delete }
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def result_handler(block)
|
40
|
+
@connection.result_handler(block)
|
41
|
+
end
|
42
|
+
|
43
|
+
def resource_path(type, id)
|
44
|
+
"/orgs/#{@org}/#{translate_type(type)}/#{id}"
|
45
|
+
end
|
46
|
+
|
47
|
+
def resources_path(type)
|
48
|
+
"/orgs/#{@org}/#{translate_type(type)}"
|
49
|
+
end
|
50
|
+
|
51
|
+
def generic_resource_path
|
52
|
+
"orgs/#{@org}/resource"
|
53
|
+
end
|
54
|
+
|
55
|
+
def translate_type(resource_type)
|
56
|
+
return 'ApiPublishingRole' if resource_type == 'ApiPublishing/Role'
|
57
|
+
resource_type.sub(%r[/.*], '') # cut off everything after first slash
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|