receptor_controller-client 0.0.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.
@@ -0,0 +1,5 @@
1
+ module ReceptorController
2
+ class Client
3
+ VERSION = "0.0.2".freeze
4
+ end
5
+ end
@@ -0,0 +1,41 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ =begin
4
+ # Receptor-Controller Client
5
+ =end
6
+
7
+ $:.push File.expand_path("../lib", __FILE__)
8
+ require "receptor_controller/client/version"
9
+
10
+ Gem::Specification.new do |s|
11
+ s.name = "receptor_controller-client"
12
+ s.version = ReceptorController::Client::VERSION
13
+ s.platform = Gem::Platform::RUBY
14
+ s.authors = ["Martin Slemr"]
15
+ s.email = ["mslemr@redhat.com"]
16
+ s.homepage = "https://github.com/RedHatInsights/receptor_controller-client-ruby"
17
+ s.summary = "Client for communication with Platform Receptor Controller - Gem"
18
+ s.description = "Client for communication with Platform Receptor Controller"
19
+ s.license = "Apache-2.0"
20
+ s.required_ruby_version = ">= 2.5"
21
+
22
+ s.add_runtime_dependency 'activesupport', '~> 5.2.4.3'
23
+ s.add_runtime_dependency 'concurrent-ruby', '~> 1.1', '>= 1.1.6'
24
+ s.add_runtime_dependency 'faraday', '~> 1.0'
25
+ s.add_runtime_dependency 'json', '~> 2.3', '>= 2.3.0'
26
+ s.add_runtime_dependency 'manageiq-loggers', '~> 0.5.0'
27
+ s.add_runtime_dependency 'manageiq-messaging', '~> 0.1.5'
28
+
29
+ s.add_development_dependency 'bundler', '~> 2.0'
30
+ s.add_development_dependency 'rake', '>= 12.3.3'
31
+ s.add_development_dependency 'rspec', '~> 3.6', '>= 3.6.0'
32
+ s.add_development_dependency 'rubocop', '~>0.69.0'
33
+ s.add_development_dependency 'rubocop-performance', '~>1.3'
34
+ s.add_development_dependency 'simplecov', '~> 0.17.1'
35
+ s.add_development_dependency 'webmock'
36
+
37
+ s.files = `find *`.split("\n").uniq.sort.select { |f| !f.empty? }
38
+ s.test_files = `find spec/*`.split("\n")
39
+ s.executables = []
40
+ s.require_paths = ["lib"]
41
+ end
@@ -0,0 +1,87 @@
1
+ require "receptor_controller/client"
2
+
3
+ RSpec.describe ReceptorController::Client do
4
+ let(:external_tenant) { '0000001' }
5
+ let(:organization_id) { '000001' }
6
+ let(:identity) do
7
+ {"x-rh-identity" => Base64.strict_encode64({"identity" => {"account_number" => external_tenant, "user" => {"is_org_admin" => true}, "internal" => {"org_id" => organization_id}}}.to_json)}
8
+ end
9
+ let(:headers) do
10
+ {"Content-Type" => "application/json",
11
+ "Accept" => "*/*",
12
+ "Accept-Encoding" => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3'}.merge(identity)
13
+ end
14
+ let(:receptor_scheme) { 'http' }
15
+ let(:receptor_host) { 'localhost:9090' }
16
+ let(:receptor_node) { 'testing-receptor' }
17
+ let(:receptor_config) do
18
+ ReceptorController::Client::Configuration.new do |config|
19
+ config.controller_scheme = receptor_scheme
20
+ config.controller_host = receptor_host
21
+ end
22
+ end
23
+ let(:satellite_uid) { '1234567890' }
24
+
25
+ subject { described_class.new(:config => receptor_config) }
26
+
27
+ before do
28
+ subject.identity_header = identity
29
+ end
30
+
31
+ describe "#connection_status" do
32
+ it "makes a POST request to receptor and returns status key-value if successful" do
33
+ response = {"status" => "connected"}
34
+ stub_request(:post, "#{receptor_scheme}://#{receptor_host}/connection/status")
35
+ .with(:body => {"account" => external_tenant, "node_id" => receptor_node}.to_json,
36
+ :headers => headers)
37
+ .to_return(:status => 200, :body => response.to_json, :headers => {})
38
+
39
+ expect(subject.connection_status(external_tenant, receptor_node)).to eq(response)
40
+ end
41
+
42
+ it "makes a POST request to receptor and returns disconnected status in case of error" do
43
+ stub_request(:post, "#{receptor_scheme}://#{receptor_host}/connection/status")
44
+ .with(:body => {"account" => external_tenant, "node_id" => receptor_node}.to_json,
45
+ :headers => headers)
46
+ .to_return(:status => 401, :body => {"errors" => [{"status" => 401, "detail" => "Unauthorized"}]}.to_json, :headers => {})
47
+
48
+ expect(subject.connection_status(external_tenant, receptor_node)).to eq(described_class::STATUS_DISCONNECTED)
49
+ end
50
+
51
+ it "makes a POST request and returns disconnected if receptor unavailable" do
52
+ allow(Faraday).to receive(:post).and_raise(Faraday::ConnectionFailed, "Failed to open TCP connection to #{receptor_host}")
53
+
54
+ expect(subject.connection_status(external_tenant, receptor_node)).to eq(described_class::STATUS_DISCONNECTED)
55
+ end
56
+ end
57
+
58
+ describe "#directive" do
59
+ it "creates blocking or non-blocking directive" do
60
+ %i[blocking non_blocking].each do |type|
61
+ directive = subject.directive(nil,
62
+ nil,
63
+ :payload => nil,
64
+ :directive => 'xxx',
65
+ :type => type)
66
+ klass = type == :blocking ? ReceptorController::Client::DirectiveBlocking : ReceptorController::Client::DirectiveNonBlocking
67
+ expect(directive).to be_a_kind_of(klass)
68
+ end
69
+ end
70
+ end
71
+
72
+ describe "#headers" do
73
+ let(:pre_shared_key) { '123456789' }
74
+
75
+ it "uses identity_header if PSK is not provided" do
76
+ expect(subject.headers).to eq({"Content-Type" => "application/json"}.merge(identity))
77
+ end
78
+
79
+ it "uses pre-shared key if provided" do
80
+ subject.config.configure do |config|
81
+ config.pre_shared_key = pre_shared_key
82
+ end
83
+
84
+ expect(subject.headers("000001")).to eq("Content-Type" => "application/json", "x-rh-receptor-controller-account" => "000001", "x-rh-receptor-controller-client-id" => "topological-inventory", "x-rh-receptor-controller-psk" => "123456789")
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,197 @@
1
+ require "receptor_controller/client/directive_blocking"
2
+
3
+ RSpec.describe ReceptorController::Client::DirectiveBlocking do
4
+ # TODO: definitions below contain the same like non-blocking spec
5
+ let(:external_tenant) { '0000001' }
6
+ let(:organization_id) { '000001' }
7
+ let(:identity) do
8
+ {"x-rh-identity" => Base64.strict_encode64({"identity" => {"account_number" => external_tenant, "user" => {"is_org_admin" => true}, "internal" => {"org_id" => organization_id}}}.to_json)}
9
+ end
10
+ let(:headers) do
11
+ {"Content-Type" => "application/json",
12
+ "Accept" => "*/*",
13
+ "Accept-Encoding" => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3'}.merge(identity)
14
+ end
15
+ let(:receptor_scheme) { 'http' }
16
+ let(:receptor_host) { 'localhost:9090' }
17
+ let(:receptor_node) { 'testing-receptor' }
18
+ let(:receptor_config) do
19
+ ReceptorController::Client::Configuration.new do |config|
20
+ config.controller_scheme = receptor_scheme
21
+ config.controller_host = receptor_host
22
+ end
23
+ end
24
+ let(:receptor_client) do
25
+ client = ReceptorController::Client.new(:config => receptor_config)
26
+ client.identity_header = identity
27
+ client
28
+ end
29
+
30
+ let(:payload) { {'method' => :get, 'url' => 'tower.example.com', 'headers' => {}, 'ssl' => false}.to_json }
31
+ let(:directive) { 'receptor_http:execute' }
32
+
33
+ subject { described_class.new(:name => directive, :account => external_tenant, :node_id => receptor_node, :payload => payload, :client => receptor_client) }
34
+
35
+ describe "#call" do
36
+ it "makes POST /job request to receptor and registers received message ID" do
37
+ allow(subject).to receive(:wait_for_response)
38
+
39
+ response = {"id" => '1234'}
40
+
41
+ stub_request(:post, "#{receptor_scheme}://#{receptor_host}/job")
42
+ .with(:body => subject.default_body.to_json,
43
+ :headers => headers)
44
+ .to_return(:status => 200, :body => response.to_json, :headers => {})
45
+
46
+ expect(subject.response_worker).to receive(:register_message).with(response['id'], subject)
47
+
48
+ subject.call
49
+ end
50
+
51
+ it "raises ControllerResponseError if POST /job returns error" do
52
+ stub_request(:post, "#{receptor_scheme}://#{receptor_host}/job")
53
+ .with(:body => subject.default_body.to_json,
54
+ :headers => headers)
55
+ .to_return(:status => 401, :body => {"errors" => [{"status" => 401, "detail" => "Unauthorized"}]}.to_json, :headers => {})
56
+
57
+ expect(subject.response_worker).not_to receive(:register_message)
58
+
59
+ expect { subject.call }.to raise_error(ReceptorController::Client::ControllerResponseError)
60
+ end
61
+
62
+ context "waiting for callbacks" do
63
+ let(:http_response) { {"id" => '1234'} }
64
+ let(:kafka_client) { double("Kafka client") }
65
+ let(:kafka_response) { double("Kafka response") }
66
+
67
+ before do
68
+ stub_request(:post, "#{receptor_scheme}://#{receptor_host}/job")
69
+ .with(:body => subject.default_body.to_json,
70
+ :headers => headers)
71
+ .to_return(:status => 200, :body => http_response.to_json, :headers => {})
72
+
73
+ allow(kafka_response).to receive(:ack)
74
+
75
+ allow(ManageIQ::Messaging::Client).to receive(:open).and_return(kafka_client)
76
+ allow(kafka_client).to receive(:subscribe_topic).and_yield(kafka_response)
77
+ allow(kafka_client).to receive(:close)
78
+ end
79
+
80
+ it "waits for successful response and sets data" do
81
+ response_message = {'code' => 0,
82
+ 'in_response_to' => http_response['id'],
83
+ 'message_type' => subject.class::MESSAGE_TYPE_RESPONSE,
84
+ 'payload' => 'Test payload'}
85
+
86
+ allow(kafka_response).to receive(:payload).and_return(response_message.to_json)
87
+
88
+ expect(subject).to(receive(:response_success)
89
+ .with(response_message['in_response_to'],
90
+ response_message['message_type'],
91
+ response_message['payload'])
92
+ .and_wrap_original) do |m, *args|
93
+ m.call(*args) # Doesn't release lock, only EOF response can
94
+ subject.send(:response_waiting).signal
95
+ subject.response_worker.stop
96
+ end
97
+
98
+ subject.response_worker.start
99
+ subject.call
100
+
101
+ expect(subject.send(:response_data)).to eq(response_message['payload'])
102
+ expect(subject.send(:response_exception)).to be_nil
103
+ end
104
+
105
+ it "waits for EOF response" do
106
+ response_message = {'code' => 0,
107
+ 'in_response_to' => http_response['id'],
108
+ 'message_type' => subject.class::MESSAGE_TYPE_EOF,
109
+ 'payload' => 'Unimportant'}
110
+
111
+ allow(kafka_response).to receive(:payload).and_return(response_message.to_json)
112
+
113
+ expect(subject).to(receive(:response_success)
114
+ .with(response_message['in_response_to'],
115
+ response_message['message_type'],
116
+ response_message['payload'])
117
+ .and_wrap_original) do |m, *args|
118
+ m.call(*args) # original call releases lock
119
+ subject.response_worker.stop
120
+ end
121
+
122
+ subject.response_worker.start
123
+ subject.call
124
+
125
+ expect(subject.send(:response_data)).to be_nil
126
+ expect(subject.send(:response_exception)).to be_nil
127
+ end
128
+
129
+ it "raises UnknownResponseTypeError if unknown successful response received" do
130
+ response_message = {'code' => 0,
131
+ 'in_response_to' => http_response['id'],
132
+ 'message_type' => 'unknown',
133
+ 'payload' => 'Unimportant'}
134
+
135
+ allow(kafka_response).to receive(:payload).and_return(response_message.to_json)
136
+
137
+ expect(subject).to(receive(:response_success)
138
+ .with(response_message['in_response_to'],
139
+ response_message['message_type'],
140
+ response_message['payload'])
141
+ .and_wrap_original) do |m, *args|
142
+ m.call(*args) # original call releases lock
143
+ subject.response_worker.stop
144
+ end
145
+
146
+ subject.response_worker.start
147
+ expect { subject.call }.to raise_error(ReceptorController::Client::UnknownResponseTypeError)
148
+
149
+ expect(subject.send(:response_data)).to be_nil
150
+ expect(subject.send(:response_exception)).to be_kind_of(ReceptorController::Client::UnknownResponseTypeError)
151
+ end
152
+
153
+ it "raises ResponseError if error response received" do
154
+ response_message = {'code' => 1,
155
+ 'in_response_to' => http_response['id'],
156
+ 'payload' => 'Some error message'}
157
+
158
+ allow(kafka_response).to receive(:payload).and_return(response_message.to_json)
159
+
160
+ expect(subject).to(receive(:response_error)
161
+ .with(response_message['in_response_to'],
162
+ response_message['code'],
163
+ response_message['payload'])
164
+ .and_wrap_original) do |m, *args|
165
+ m.call(*args) # original calls releases lock
166
+ subject.response_worker.stop
167
+ end
168
+
169
+ subject.response_worker.start
170
+ expect { subject.call }.to raise_error(ReceptorController::Client::ResponseError)
171
+
172
+ expect(subject.send(:response_data)).to be_nil
173
+ expect(subject.send(:response_exception)).to be_kind_of(ReceptorController::Client::ResponseError)
174
+ end
175
+
176
+ it "raises ResponseTimeoutError if timeout response received" do
177
+ receptor_client.config.response_timeout = 1.second
178
+ receptor_client.config.response_timeout_poll_time = 0
179
+
180
+ allow(kafka_client).to receive(:subscribe_topic).and_return(nil)
181
+
182
+ expect(subject).to(receive(:response_timeout)
183
+ .with(http_response['id'])
184
+ .and_wrap_original) do |m, *args|
185
+ m.call(*args) # original call releases lock
186
+ subject.response_worker.stop
187
+ end
188
+
189
+ subject.response_worker.start
190
+ expect { subject.call }.to raise_error(ReceptorController::Client::ResponseTimeoutError)
191
+
192
+ expect(subject.send(:response_data)).to be_nil
193
+ expect(subject.send(:response_exception)).to be_kind_of(ReceptorController::Client::ResponseTimeoutError)
194
+ end
195
+ end
196
+ end
197
+ end
@@ -0,0 +1,160 @@
1
+ require "receptor_controller/client/directive_non_blocking"
2
+
3
+ RSpec.describe ReceptorController::Client::DirectiveNonBlocking do
4
+ let(:external_tenant) { '0000001' }
5
+ let(:organization_id) { '000001' }
6
+ let(:identity) do
7
+ {"x-rh-identity" => Base64.strict_encode64({"identity" => {"account_number" => external_tenant, "user" => {"is_org_admin" => true}, "internal" => {"org_id" => organization_id}}}.to_json)}
8
+ end
9
+ let(:headers) do
10
+ {"Content-Type" => "application/json",
11
+ "Accept" => "*/*",
12
+ "Accept-Encoding" => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3'}.merge(identity)
13
+ end
14
+ let(:receptor_scheme) { 'http' }
15
+ let(:receptor_host) { 'localhost:9090' }
16
+ let(:receptor_node) { 'testing-receptor' }
17
+ let(:receptor_config) do
18
+ ReceptorController::Client::Configuration.new do |config|
19
+ config.controller_scheme = receptor_scheme
20
+ config.controller_host = receptor_host
21
+ end
22
+ end
23
+ let(:receptor_client) do
24
+ client = ReceptorController::Client.new
25
+ client.identity_header = identity
26
+ client
27
+ end
28
+
29
+ let(:satellite_uid) { '1234567890' }
30
+ let(:payload) { {'satellite_instance_id' => satellite_uid.to_s}.to_json }
31
+ let(:directive) { 'receptor_satellite:health_check' }
32
+
33
+ subject { described_class.new(:name => directive, :account => external_tenant, :node_id => receptor_node, :payload => payload, :client => receptor_client) }
34
+
35
+ describe "#call" do
36
+ it "makes POST /job request to receptor, registers received message ID and returns it" do
37
+ response = {"id" => '1234'}
38
+
39
+ stub_request(:post, "#{receptor_scheme}://#{receptor_host}/job")
40
+ .with(:body => subject.default_body.to_json,
41
+ :headers => headers)
42
+ .to_return(:status => 200, :body => response.to_json, :headers => {})
43
+
44
+ expect(subject.response_worker).to receive(:register_message).with(response['id'], subject)
45
+
46
+ expect(subject.call).to eq(response['id'])
47
+ end
48
+
49
+ it "makes POST /job request to receptor, doesn't register message and returns nil in case of error" do
50
+ stub_request(:post, "#{receptor_scheme}://#{receptor_host}/job")
51
+ .with(:body => subject.default_body.to_json,
52
+ :headers => headers)
53
+ .to_return(:status => 401, :body => {"errors" => [{"status" => 401, "detail" => "Unauthorized"}]}.to_json, :headers => {})
54
+
55
+ expect(subject.response_worker).not_to receive(:register_message)
56
+
57
+ expect(subject.call).to be_nil
58
+ end
59
+
60
+ it "makes a POST request and returns disconnected if receptor unavailable" do
61
+ allow(Faraday).to receive(:post).and_raise(Faraday::ConnectionFailed, "Failed to open TCP connection to #{receptor_host}")
62
+
63
+ expect(subject.response_worker).not_to receive(:register_message)
64
+
65
+ expect(subject.call).to be_nil
66
+ end
67
+ end
68
+
69
+ context "callbacks" do
70
+ let(:response_id) { '1234' }
71
+ let(:response) { double('Kafka response', :ack => nil) }
72
+
73
+ before do
74
+ # HTTP request
75
+ stub_request(:post, "#{receptor_scheme}://#{receptor_host}/job")
76
+ .with(:body => subject.default_body.to_json,
77
+ :headers => headers)
78
+ .to_return(:status => 200, :body => {'id' => response_id}.to_json, :headers => {})
79
+
80
+ subject.call
81
+ end
82
+
83
+ it "calls success/eof blocks" do
84
+ mutex = Mutex.new
85
+ cv = ConditionVariable.new
86
+
87
+ # Callbacks
88
+ result = {}
89
+ subject
90
+ .on_success do |msg_id, payload|
91
+ result[:first] = {:id => msg_id, :payload => payload}
92
+ end
93
+ .on_success do |msg_id, payload|
94
+ result[:second] = {:id => msg_id, :payload => payload}
95
+ end
96
+ .on_eof do |msg_id|
97
+ result[:eof] = {:id => msg_id}
98
+ mutex.synchronize { cv.signal }
99
+ end
100
+
101
+ # Kafka response - 'response' type
102
+ expect(subject).to receive(:response_success).twice.and_call_original
103
+ message_type, payload = 'response', 'Testing payload'
104
+ response_payload = {'code' => 0, 'in_response_to' => response_id, 'message_type' => message_type, 'payload' => payload}
105
+ allow(response).to receive(:payload).and_return(response_payload.to_json)
106
+ subject.response_worker.send(:process_message, response)
107
+
108
+ # Kafka response - 'eof' type
109
+ message_type = 'eof'
110
+ response_payload = {'code' => 0, 'in_response_to' => response_id, 'message_type' => message_type, 'payload' => nil}
111
+ allow(response).to receive(:payload).and_return(response_payload.to_json)
112
+ subject.response_worker.send(:process_message, response)
113
+
114
+ mutex.synchronize { cv.wait(mutex) }
115
+
116
+ # Result containing both "on_success" blocks
117
+ block_result = {:id => response_id, :payload => payload}
118
+ expect(result).to eq(:first => block_result,
119
+ :second => block_result,
120
+ :eof => {:id => response_id})
121
+ end
122
+
123
+ it "processes eof blocks always after all success blocks" do
124
+ success_cnt = Concurrent::AtomicFixnum.new(0)
125
+
126
+ success_calls = 3
127
+
128
+ mutex = Mutex.new
129
+ cv = ConditionVariable.new
130
+
131
+ # Callbacks
132
+ subject
133
+ .on_success do |_msg_id, _payload|
134
+ success_cnt.increment
135
+ end
136
+ .on_eof do |_msg_id|
137
+ # Tests
138
+ expect(success_cnt.value).to eq(success_calls)
139
+
140
+ mutex.synchronize { cv.signal }
141
+ end
142
+
143
+ # Kafka response - 'response' type - success
144
+ message_type, payload = 'response', 'Testing payload'
145
+ response_payload = {'code' => 0, 'in_response_to' => response_id, 'message_type' => message_type, 'payload' => payload}
146
+ allow(response).to receive(:payload).and_return(response_payload.to_json)
147
+ success_calls.times do
148
+ subject.response_worker.send(:process_message, response)
149
+ end
150
+
151
+ # Kafka response - 'eof' type
152
+ message_type = 'eof'
153
+ response_payload = {'code' => 0, 'in_response_to' => response_id, 'message_type' => message_type, 'payload' => nil}
154
+ allow(response).to receive(:payload).and_return(response_payload.to_json)
155
+ subject.response_worker.send(:process_message, response)
156
+
157
+ mutex.synchronize { cv.wait(mutex) }
158
+ end
159
+ end
160
+ end