xapixctl 1.0.0 → 1.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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