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.
@@ -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
- class ResultHandler
7
- def initialize(default_success_handler:, default_error_handler:)
8
- @success_handler = default_success_handler
9
- @error_handler = default_error_handler
10
- @result_handler = nil
11
- yield self if block_given?
12
- end
13
-
14
- def on_success(&block); @success_handler = block; self; end
15
-
16
- def on_error(&block); @error_handler = block; self; end
17
-
18
- def prepare_data(proc); @result_handler = proc; self; end
19
-
20
- def formatter(proc); @formatter = proc; self; end
21
-
22
- def run
23
- res = yield
24
- res = res.present? ? JSON.parse(res) : res
25
- res = @result_handler ? @result_handler.call(res) : res
26
- res = @formatter ? @formatter.call(res) : res
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 : %{id}\nkind: %{kind}\nname: %{name}\n\n" % { id: data.dig('metadata', 'id'), kind: data.dig('kind'), name: data.dig('definition', 'name') } }
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.deep_transform_keys! { |k| k.to_s.camelize(:lower) }) },
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
- class Connection
47
- DEFAULT_SUCCESS_HANDLER = ->(result) { result }
48
- DEFAULT_ERROR_HANDLER = ->(err, _response) { warn "Could not get data: #{err}" }
49
-
50
- # sorting is intentional to reflect dependencies when exporting
51
- SUPPORTED_RESOURCE_TYPES = %w[
52
- Project
53
- Ambassador
54
- AuthScheme
55
- Credential
56
- Proxy
57
- CacheConnection
58
- Schema
59
- DataSource
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
- private
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
- def resource_types_path
154
- "/resource_types"
155
- 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)
73
+ end
156
74
 
157
- def translate_type(resource_type)
158
- return 'ApiPublishingRole' if resource_type == 'ApiPublishing/Role'
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