xapixctl 1.0.0 → 1.2.1
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 +63 -41
- data/README.md +8 -0
- data/Rakefile +1 -1
- data/lib/xapixctl.rb +0 -3
- data/lib/xapixctl/base_cli.rb +75 -0
- data/lib/xapixctl/cli.rb +68 -136
- data/lib/xapixctl/phoenix_client.rb +58 -142
- 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 +164 -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/util.rb +42 -0
- data/lib/xapixctl/version.rb +3 -1
- data/xapixctl.gemspec +16 -9
- metadata +93 -24
@@ -1,163 +1,79 @@
|
|
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
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
Pipeline
|
61
|
-
EndpointGroup
|
62
|
-
Endpoint
|
63
|
-
StreamGroup
|
64
|
-
Stream
|
65
|
-
ApiPublishing
|
66
|
-
ApiPublishingRole
|
67
|
-
].freeze
|
68
|
-
|
69
|
-
def initialize(url, token)
|
70
|
-
@client = RestClient::Resource.new(File.join(url, 'api/v1'), verify_ssl: false, accept: :json, content_type: :json, headers: { Authorization: "Bearer #{token}" })
|
71
|
-
@default_success_handler = DEFAULT_SUCCESS_HANDLER
|
72
|
-
@default_error_handler = DEFAULT_ERROR_HANDLER
|
73
|
-
end
|
74
|
-
|
75
|
-
def on_success(&block); @default_success_handler = block; self; end
|
76
|
-
|
77
|
-
def on_error(&block); @default_error_handler = block; self; end
|
78
|
-
|
79
|
-
def resource(resource_type, resource_id, org:, project: nil, format: :hash, &block)
|
80
|
-
result_handler(block).
|
81
|
-
formatter(FORMATTERS[format]).
|
82
|
-
run { @client[resource_path(org, project, resource_type, resource_id)].get }
|
83
|
-
end
|
84
|
-
|
85
|
-
def resource_ids(resource_type, org:, project: nil, &block)
|
86
|
-
result_handler(block).
|
87
|
-
prepare_data(->(data) { data['resource_ids'] }).
|
88
|
-
run { @client[resources_path(org, project, resource_type)].get }
|
89
|
-
end
|
90
|
-
|
91
|
-
def apply(resource_description, org:, project: nil, &block)
|
92
|
-
result_handler(block).
|
93
|
-
run { @client[generic_resource_path(org, project)].put(resource_description.to_json) }
|
94
|
-
end
|
95
|
-
|
96
|
-
def delete(resource_type, resource_id, org:, project: nil, &block)
|
97
|
-
result_handler(block).
|
98
|
-
run { @client[resource_path(org, project, resource_type, resource_id)].delete }
|
99
|
-
end
|
100
|
-
|
101
|
-
def publish(org:, project:, &block)
|
102
|
-
result_handler(block).
|
103
|
-
run { @client[project_publications_path(org, project)].post('') }
|
104
|
-
end
|
105
|
-
|
106
|
-
def logs(correlation_id, org:, project:, &block)
|
107
|
-
result_handler(block).
|
108
|
-
run { @client[project_logss_path(org, project, correlation_id)].get }
|
109
|
-
end
|
110
|
-
|
111
|
-
def available_resource_types(&block)
|
112
|
-
result_handler(block).
|
113
|
-
prepare_data(->(data) { data['resource_types'] }).
|
114
|
-
run { @client[resource_types_path].get }
|
115
|
-
end
|
116
|
-
|
117
|
-
def resource_types_for_export
|
118
|
-
@resource_types_for_export ||=
|
119
|
-
available_resource_types do |res|
|
120
|
-
res.on_success { |available_types| SUPPORTED_RESOURCE_TYPES & available_types.map { |desc| desc['type'] } }
|
121
|
-
res.on_error { |err, _response| raise err }
|
46
|
+
PREVIEW_FORMATTERS = {
|
47
|
+
json: ->(data) { JSON.pretty_generate(data) },
|
48
|
+
yaml: ->(data) { Psych.dump(data) },
|
49
|
+
text: ->(data) do
|
50
|
+
preview = data['preview']
|
51
|
+
if ['RestJson', 'SoapXml'].include?(data['content_type'])
|
52
|
+
res = StringIO.new
|
53
|
+
if preview.is_a?(Hash)
|
54
|
+
res.puts "HTTP #{preview['status']}"
|
55
|
+
preview['headers']&.each { |h, v| res.puts "#{h}: #{v}" }
|
56
|
+
res.puts
|
57
|
+
res.puts preview['body']
|
58
|
+
else
|
59
|
+
res.puts preview
|
122
60
|
end
|
61
|
+
res.string
|
62
|
+
else
|
63
|
+
Psych.dump(preview)
|
64
|
+
end
|
123
65
|
end
|
66
|
+
}.freeze
|
124
67
|
|
125
|
-
|
126
|
-
|
127
|
-
def result_handler(block)
|
128
|
-
ResultHandler.new(default_success_handler: @default_success_handler, default_error_handler: @default_error_handler, &block)
|
129
|
-
end
|
130
|
-
|
131
|
-
def resource_path(org, project, type, id)
|
132
|
-
type = translate_type(type)
|
133
|
-
project ? "/projects/#{org}/#{project}/#{type}/#{id}" : "/orgs/#{org}/#{type}/#{id}"
|
134
|
-
end
|
135
|
-
|
136
|
-
def resources_path(org, project, type)
|
137
|
-
type = translate_type(type)
|
138
|
-
project ? "/projects/#{org}/#{project}/#{type}" : "/orgs/#{org}/#{type}"
|
139
|
-
end
|
140
|
-
|
141
|
-
def generic_resource_path(org, project)
|
142
|
-
project ? "projects/#{org}/#{project}/resource" : "orgs/#{org}/resource"
|
143
|
-
end
|
144
|
-
|
145
|
-
def project_publications_path(org, project)
|
146
|
-
"/projects/#{org}/#{project}/publications"
|
147
|
-
end
|
148
|
-
|
149
|
-
def project_logss_path(org, project, correlation_id)
|
150
|
-
"/projects/#{org}/#{project}/logs/#{correlation_id}"
|
151
|
-
end
|
68
|
+
DEFAULT_SUCCESS_HANDLER = ->(result) { result }
|
69
|
+
DEFAULT_ERROR_HANDLER = ->(err, _response) { warn "Could not get data: #{err}" }
|
152
70
|
|
153
|
-
|
154
|
-
|
155
|
-
|
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)
|
73
|
+
end
|
156
74
|
|
157
|
-
|
158
|
-
|
159
|
-
resource_type.sub(%r[/.*], '') # cut off everything after first slash
|
160
|
-
end
|
75
|
+
def self.supported_type?(type)
|
76
|
+
SUPPORTED_RESOURCE_TYPES.include?(type)
|
161
77
|
end
|
162
78
|
end
|
163
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
|
@@ -0,0 +1,164 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Xapixctl
|
4
|
+
module PhoenixClient
|
5
|
+
class ProjectConnection < OrganizationConnection
|
6
|
+
attr_reader :project
|
7
|
+
|
8
|
+
def initialize(connection, org, project)
|
9
|
+
super(connection, org)
|
10
|
+
@project = project
|
11
|
+
end
|
12
|
+
|
13
|
+
def project_resource(format: :hash, &block)
|
14
|
+
organization.resource('Project', @project, format: format, &block)
|
15
|
+
end
|
16
|
+
|
17
|
+
def organization
|
18
|
+
OrganizationConnection.new(@connection, @org)
|
19
|
+
end
|
20
|
+
|
21
|
+
def resource_types_for_export
|
22
|
+
@resource_types_for_export ||=
|
23
|
+
@connection.available_resource_types do |res|
|
24
|
+
res.on_success do |available_types|
|
25
|
+
prj_types = available_types.select { |desc| desc['context'] == 'Project' }
|
26
|
+
SUPPORTED_RESOURCE_TYPES & prj_types.map { |desc| desc['type'] }
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Notes on parameters:
|
32
|
+
# - Query parameters should be part of the URL
|
33
|
+
# - Path parameters should be marked with `{name}` in the URL, and values should be given in path_params hash
|
34
|
+
# - Headers should be given in headers hash
|
35
|
+
# - Cookies should be given in cookies hash
|
36
|
+
# - The body has to be given as a string
|
37
|
+
# - The required authentication schemes should be listed, referring to previously created schemes
|
38
|
+
#
|
39
|
+
# This returns a hash like the following:
|
40
|
+
# "data_source" => { "id" => id, "resource_description" => resource_description }
|
41
|
+
#
|
42
|
+
# To successfully onboard a DB using the API, the following steps are needed:
|
43
|
+
# 1. setup the data source using add_rest_data_source.
|
44
|
+
# 2. retrieve a preview using preview_data_source using the id returned by previous step
|
45
|
+
# 3. confirm preview
|
46
|
+
# 4. call accept_data_source_preview to complete onboarding
|
47
|
+
#
|
48
|
+
def add_rest_data_source(http_method:, url:, path_params: {}, headers: {}, cookies: {}, body: nil, auth_schemes: [], &block)
|
49
|
+
data_source_details = {
|
50
|
+
data_source: {
|
51
|
+
http_method: http_method, url: url,
|
52
|
+
parameters: { path: path_params.to_query, header: headers.to_query, cookies: cookies.to_query, body: body },
|
53
|
+
auth_schemes: auth_schemes
|
54
|
+
}
|
55
|
+
}
|
56
|
+
result_handler(block).
|
57
|
+
run { @client[rest_data_source_path].post(data_source_details.to_json, content_type: :json) }
|
58
|
+
end
|
59
|
+
|
60
|
+
# Notes on parameters:
|
61
|
+
# - To call a data source which requires authentication, provide a hash with each required auth scheme as key and
|
62
|
+
# as the value a reference to a previously created credential.
|
63
|
+
# Example: { scheme_ref1 => credential_ref1, scheme_ref2 => credential_ref2 }
|
64
|
+
#
|
65
|
+
# This returns a hashified preview like the following:
|
66
|
+
# { "preview" => {
|
67
|
+
# "sample" => { "status" => integer, "body" => { ... }, "headers" => { ... }, "cookies" => { ... } },
|
68
|
+
# "fetched_at" => Timestamp },
|
69
|
+
# "data_source" => { "id" => id, "resource_description" => resource_description } }
|
70
|
+
#
|
71
|
+
def data_source_preview(data_source_id, authentications: {}, &block)
|
72
|
+
preview_data = {
|
73
|
+
authentications: authentications.map { |scheme, cred| { auth_scheme_id: scheme, auth_credential_id: cred } }
|
74
|
+
}
|
75
|
+
result_handler(block).
|
76
|
+
run { @client[data_source_preview_path(data_source_id)].post(preview_data.to_json, content_type: :json) }
|
77
|
+
end
|
78
|
+
|
79
|
+
def pipeline_preview(pipeline_id, format: :hash, &block)
|
80
|
+
result_handler(block).
|
81
|
+
prepare_data(->(data) { data['pipeline_preview'] }).
|
82
|
+
formatter(PREVIEW_FORMATTERS[format]).
|
83
|
+
run { @client[pipeline_preview_path(pipeline_id)].get }
|
84
|
+
end
|
85
|
+
|
86
|
+
def endpoint_preview(endpoint_id, format: :hash, &block)
|
87
|
+
result_handler(block).
|
88
|
+
prepare_data(->(data) { data['endpoint_preview'] }).
|
89
|
+
formatter(PREVIEW_FORMATTERS[format]).
|
90
|
+
run { @client[endpoint_preview_path(endpoint_id)].get }
|
91
|
+
end
|
92
|
+
|
93
|
+
def stream_processor_preview(stream_processor_id, format: :hash, &block)
|
94
|
+
result_handler(block).
|
95
|
+
prepare_data(->(data) { data['stream_processor_preview'] }).
|
96
|
+
formatter(PREVIEW_FORMATTERS[format]).
|
97
|
+
run { @client[stream_processor_preview_path(stream_processor_id)].get }
|
98
|
+
end
|
99
|
+
|
100
|
+
def publish(&block)
|
101
|
+
result_handler(block).
|
102
|
+
run { @client[project_publications_path].post('') }
|
103
|
+
end
|
104
|
+
|
105
|
+
def logs(correlation_id, &block)
|
106
|
+
result_handler(block).
|
107
|
+
run { @client[project_logss_path(correlation_id)].get }
|
108
|
+
end
|
109
|
+
|
110
|
+
# This returns a hashified preview like the following:
|
111
|
+
|
112
|
+
def accept_data_source_preview(data_source_id, &block)
|
113
|
+
result_handler(block).
|
114
|
+
run { @client[data_source_preview_path(data_source_id)].patch('') }
|
115
|
+
end
|
116
|
+
|
117
|
+
def public_project_url
|
118
|
+
File.join(@connection.xapix_url, @org, @project)
|
119
|
+
end
|
120
|
+
|
121
|
+
private
|
122
|
+
|
123
|
+
def rest_data_source_path
|
124
|
+
"/projects/#{@org}/#{@project}/onboarding/data_sources/rest"
|
125
|
+
end
|
126
|
+
|
127
|
+
def data_source_preview_path(id)
|
128
|
+
"/projects/#{@org}/#{@project}/onboarding/data_sources/#{id}/preview"
|
129
|
+
end
|
130
|
+
|
131
|
+
def resource_path(type, id)
|
132
|
+
"/projects/#{@org}/#{@project}/#{translate_type(type)}/#{id}"
|
133
|
+
end
|
134
|
+
|
135
|
+
def resources_path(type)
|
136
|
+
"/projects/#{@org}/#{@project}/#{translate_type(type)}"
|
137
|
+
end
|
138
|
+
|
139
|
+
def generic_resource_path
|
140
|
+
"projects/#{@org}/#{@project}/resource"
|
141
|
+
end
|
142
|
+
|
143
|
+
def pipeline_preview_path(pipeline)
|
144
|
+
"/projects/#{@org}/#{@project}/pipelines/#{pipeline}/preview"
|
145
|
+
end
|
146
|
+
|
147
|
+
def endpoint_preview_path(endpoint)
|
148
|
+
"/projects/#{@org}/#{@project}/endpoints/#{endpoint}/preview"
|
149
|
+
end
|
150
|
+
|
151
|
+
def stream_processor_preview_path(stream_processor)
|
152
|
+
"/projects/#{@org}/#{@project}/stream_processors/#{stream_processor}/preview"
|
153
|
+
end
|
154
|
+
|
155
|
+
def project_publications_path
|
156
|
+
"/projects/#{@org}/#{@project}/publications"
|
157
|
+
end
|
158
|
+
|
159
|
+
def project_logss_path(correlation_id)
|
160
|
+
"/projects/#{@org}/#{@project}/logs/#{correlation_id}"
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|