hula 0.7.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.
@@ -0,0 +1,308 @@
1
+ # Copyright (c) 2014-2015 Pivotal Software, Inc.
2
+ # All rights reserved.
3
+ # THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
4
+ # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
5
+ # PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
6
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
7
+ # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
8
+ # USE OR OTHER DEALINGS IN THE SOFTWARE.
9
+ #
10
+
11
+ require 'tmpdir'
12
+ require 'json'
13
+ require 'open3'
14
+ require 'tempfile'
15
+
16
+ require 'hula/command_runner'
17
+ require 'hula/cloud_foundry/service_broker'
18
+
19
+ module Hula
20
+ class CloudFoundry
21
+ attr_reader :current_organization, :current_space, :domain, :api_url
22
+
23
+ def initialize(args)
24
+ @domain = args.fetch(:domain)
25
+ @api_url = args.fetch(:api_url)
26
+ @logger = args.fetch(:logger, default_logger)
27
+ @command_runner = args.fetch(:command_runner, default_command_runner)
28
+
29
+ target_and_login = args.fetch(:target_and_login, true)
30
+ if target_and_login
31
+ target(api_url)
32
+ login(args.fetch(:username), args.fetch(:password))
33
+ end
34
+ end
35
+
36
+ def url_for_app(app_name)
37
+ "https://#{app_name}.#{domain}"
38
+ end
39
+
40
+ def target(cloud_controller_url)
41
+ cf("api #{cloud_controller_url} --skip-ssl-validation")
42
+ end
43
+
44
+ def app_vcap_services(app_name)
45
+ app_environment(app_name)["VCAP_SERVICES"]
46
+ end
47
+
48
+ def login(username, password, allow_failure = true)
49
+ cf("auth #{username} #{password}", allow_failure: allow_failure)
50
+ end
51
+
52
+ def service_brokers
53
+ output = cf('service-brokers')
54
+
55
+ if output.include?('No service brokers found')
56
+ []
57
+ else
58
+ output.split("\n").drop(3).map do |row|
59
+ name, url = row.split(/\s+/)
60
+ ServiceBroker.new(name: name, url: url)
61
+ end
62
+ end
63
+ end
64
+
65
+ def create_and_target_org(name)
66
+ create_org(name)
67
+ sleep 1
68
+ target_org(name)
69
+ end
70
+
71
+ def create_org(name)
72
+ cf("create-org #{name}")
73
+ end
74
+
75
+ def target_org(name)
76
+ cf("target -o #{name}")
77
+ @current_organization = name
78
+ end
79
+
80
+ def create_and_target_space(name)
81
+ create_space(name)
82
+ target_space(name)
83
+ end
84
+
85
+ def create_space(name)
86
+ cf("create-space #{name}")
87
+ end
88
+
89
+ def target_space(name)
90
+ cf("target -s #{name}")
91
+ @current_space = name
92
+ end
93
+
94
+ def space_exists?(name)
95
+ spaces = cf('spaces').lines[3..-1]
96
+ spaces.map(&:strip).include?(name)
97
+ end
98
+
99
+ def org_exists?(name)
100
+ orgs = cf('orgs').lines[3..-1]
101
+ orgs.map(&:strip).include?(name)
102
+ end
103
+
104
+ def setup_permissive_security_group(org, space)
105
+ rules = [{
106
+ 'destination' => '0.0.0.0-255.255.255.255',
107
+ 'protocol' => 'all'
108
+ }]
109
+
110
+ rule_file = Tempfile.new('default_security_group.json')
111
+ rule_file.write(rules.to_json)
112
+ rule_file.close
113
+
114
+ cf("create-security-group prof-test #{rule_file.path}")
115
+ cf("bind-security-group prof-test #{org} #{space}")
116
+ cf('bind-staging-security-group prof-test')
117
+ cf('bind-running-security-group prof-test')
118
+
119
+ rule_file.unlink
120
+ end
121
+
122
+ def delete_space(name, options = {})
123
+ allow_failure = options.fetch(:allow_failure, true)
124
+ cf("delete-space #{name} -f", allow_failure: allow_failure)
125
+ end
126
+
127
+ def delete_org(name, options = {})
128
+ allow_failure = options.fetch(:allow_failure, true)
129
+ cf("delete-org #{name} -f", allow_failure: allow_failure)
130
+ end
131
+
132
+ alias_method :reset!, :delete_org
133
+
134
+ def add_public_service_broker(service_name, _service_label, url, username, password)
135
+ cf("create-service-broker #{service_name} #{username} #{password} #{url}")
136
+
137
+ service_plans = JSON.parse(cf('curl /v2/service_plans -X GET'))
138
+ guids = service_plans['resources'].map do |resource|
139
+ resource['metadata']['guid']
140
+ end
141
+
142
+ guids.each do |guid|
143
+ cf(%(curl /v2/service_plans/#{guid} -X PUT -d '{"public":true}'))
144
+ end
145
+ end
146
+
147
+ def remove_service_broker(service_name, options = {})
148
+ allow_failure = options.fetch(:allow_failure, true)
149
+ cf("delete-service-broker #{service_name} -f", allow_failure: allow_failure)
150
+ end
151
+
152
+ def assert_broker_is_in_marketplace(type)
153
+ output = marketplace
154
+ unless output.include?(type)
155
+ fail "Broker #{type} not found in marketplace"
156
+ end
157
+ end
158
+
159
+ def marketplace
160
+ cf('marketplace')
161
+ end
162
+
163
+ def create_service_instance(type, name, plan)
164
+ cf("create-service #{type} #{plan} #{name}")
165
+ end
166
+
167
+ def delete_service_instance_and_unbind(name, options = {})
168
+ allow_failure = options.fetch(:allow_failure, true)
169
+ cf("delete-service -f #{name}", allow_failure: allow_failure)
170
+ end
171
+
172
+ def assert_instance_is_in_services_list(service_name)
173
+ output = cf('services')
174
+ unless output.include?(service_name)
175
+ fail "Instance #{service_name} not found in services list"
176
+ end
177
+ end
178
+
179
+ def push_app_and_start(app_path, name)
180
+ push_app(app_path, name)
181
+ start_app(name)
182
+ end
183
+
184
+ def push_app(app_path, name)
185
+ cf("push #{name} -p #{app_path} -n #{name} -d #{domain} --no-start")
186
+ end
187
+
188
+ def enable_diego_for_app(name)
189
+ cf("enable-diego #{name}")
190
+ end
191
+
192
+ def delete_app(name, options = {})
193
+ allow_failure = options.fetch(:allow_failure, true)
194
+ cf("delete #{name} -f", allow_failure: allow_failure)
195
+ end
196
+
197
+ def bind_app_to_service(app_name, service_name)
198
+ cf("bind-service #{app_name} #{service_name}")
199
+ end
200
+
201
+ def unbind_app_from_service(app_name, service_name)
202
+ cf("unbind-service #{app_name} #{service_name}")
203
+ end
204
+
205
+ def list_service_keys(service_instance_name)
206
+ cf("service-keys #{service_instance_name}")
207
+ end
208
+
209
+ def create_service_key(service_instance_name, key_name)
210
+ cf("create-service-key #{service_instance_name} #{key_name}")
211
+ end
212
+
213
+ def delete_service_key(service_instance_name, key_name)
214
+ cf("delete-service-key #{service_instance_name} #{key_name} -f")
215
+ end
216
+
217
+ def service_key(service_instance_name, key_name)
218
+ cf("service-key #{service_instance_name} #{key_name}")
219
+ end
220
+
221
+ def restart_app(name)
222
+ stop_app(name)
223
+ start_app(name)
224
+ end
225
+
226
+ def start_app(name)
227
+ cf("start #{name}")
228
+ rescue => start_exception
229
+ begin
230
+ cf("logs --recent #{name}")
231
+ ensure
232
+ raise start_exception
233
+ end
234
+ end
235
+
236
+ def stop_app(name)
237
+ cf("stop #{name}")
238
+ end
239
+
240
+ def app_env(app_name)
241
+ cf("env #{app_name}")
242
+ end
243
+
244
+ def create_user(username, password)
245
+ cf("create-user #{username} #{password}")
246
+ end
247
+
248
+ def delete_user(username)
249
+ cf("delete-user -f #{username}")
250
+ end
251
+
252
+ def user_exists?(username, org)
253
+ output = cf("org-users #{org}")
254
+ output.lines.select { |l| l.start_with? ' ' }.map(&:strip).uniq.include?(username)
255
+ end
256
+
257
+ def set_org_role(username, org, role)
258
+ cf("set-org-role #{username} #{org} #{role}")
259
+ end
260
+
261
+ def version
262
+ cf('-v')
263
+ end
264
+
265
+ private
266
+
267
+ attr_reader :logger, :command_runner
268
+
269
+ def app_environment(app_name)
270
+ env_output = cf("env #{app_name}")
271
+ response = env_output[/^\{.*}$/m].split(/^\n/)
272
+ response = response.map { |json| JSON.parse(json) }
273
+ response.inject({}) { |result, current| result.merge(current) }
274
+ end
275
+
276
+ def default_logger
277
+ @default_logger ||= begin
278
+ STDOUT.sync = true
279
+ require 'logger'
280
+ Logger.new(STDOUT)
281
+ end
282
+ end
283
+
284
+ def default_command_runner
285
+ @default_command_runner ||= CommandRunner.new(environment: env)
286
+ end
287
+
288
+ def cf(command, options = {})
289
+ allow_failure = options.fetch(:allow_failure, false)
290
+ cf_command = "cf #{command}"
291
+
292
+ logger.info(cf_command)
293
+
294
+ command_runner.run(cf_command, allow_failure: allow_failure)
295
+ end
296
+
297
+ def env
298
+ @env ||= ENV.to_hash.merge(
299
+ 'PATH' => clean_path,
300
+ 'CF_HOME' => Dir.mktmpdir('cf-home')
301
+ )
302
+ end
303
+
304
+ def clean_path
305
+ '/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/bin/:sbin'
306
+ end
307
+ end
308
+ end
@@ -0,0 +1,38 @@
1
+ # Copyright (c) 2014-2015 Pivotal Software, Inc.
2
+ # All rights reserved.
3
+ # THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
4
+ # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
5
+ # PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
6
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
7
+ # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
8
+ # USE OR OTHER DEALINGS IN THE SOFTWARE.
9
+ #
10
+
11
+ require 'open3'
12
+
13
+ module Hula
14
+ class CommandFailedError < StandardError; end
15
+
16
+ class CommandRunner
17
+ def initialize(environment: ENV)
18
+ @environment = environment
19
+ end
20
+
21
+ def run(command, allow_failure: false)
22
+ stdout_and_stderr, status = Open3.capture2e(environment, command)
23
+
24
+ if !allow_failure && !status.success?
25
+ message = "Command failed! - #{command}\n\n#{stdout_and_stderr}\n\nexit status: #{status.exitstatus}"
26
+ fail CommandFailedError, message
27
+ end
28
+
29
+ stdout_and_stderr
30
+ rescue => e
31
+ raise CommandFailedError, e
32
+ end
33
+
34
+ private
35
+
36
+ attr_reader :environment
37
+ end
38
+ end
@@ -0,0 +1,44 @@
1
+ # Copyright (c) 2014-2015 Pivotal Software, Inc.
2
+ # All rights reserved.
3
+ # THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
4
+ # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
5
+ # PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
6
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
7
+ # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
8
+ # USE OR OTHER DEALINGS IN THE SOFTWARE.
9
+ #
10
+
11
+ require 'hula/helpers/timeout_tools'
12
+
13
+ require 'socket'
14
+
15
+ module Hula
16
+ module Helpers
17
+ module SocketTools
18
+ module_function def wait_for_port(host:, port:, timeout_seconds: 20)
19
+ error = "Failed to connect to #{host}:#{port} within #{timeout_seconds} seconds"
20
+ TimeoutTools.wait_for(error: error, timeout_seconds: timeout_seconds) do
21
+ port_open?(host: host, port: port)
22
+ end
23
+ end
24
+
25
+ module_function def port_open?(host:, port:)
26
+ socket = TCPSocket.new(host, port)
27
+ socket.close unless socket.nil?
28
+ true
29
+ rescue Errno::ECONNREFUSED
30
+ false
31
+ end
32
+
33
+ module_function def free_port
34
+ socket = Socket.new(:INET, :STREAM, 0)
35
+ socket.bind(Addrinfo.tcp('127.0.0.1', 0))
36
+ socket.local_address.ip_port
37
+ ensure
38
+ socket.close
39
+ end
40
+ end
41
+ end
42
+ end
43
+
44
+
@@ -0,0 +1,27 @@
1
+ # Copyright (c) 2014-2015 Pivotal Software, Inc.
2
+ # All rights reserved.
3
+ # THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
4
+ # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
5
+ # PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
6
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
7
+ # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
8
+ # USE OR OTHER DEALINGS IN THE SOFTWARE.
9
+ #
10
+
11
+ require 'timeout'
12
+
13
+ module Hula
14
+ module Helpers
15
+ module TimeoutTools
16
+ module_function def wait_for(error: nil, timeout_seconds:, &condition_block)
17
+ Timeout::timeout(timeout_seconds) do
18
+ until condition_block.call do
19
+ sleep 0.1
20
+ end
21
+ end
22
+ rescue Timeout::Error => e
23
+ error ? raise(error) : raise(e)
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,72 @@
1
+ # Copyright (c) 2014-2015 Pivotal Software, Inc.
2
+ # All rights reserved.
3
+ # THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
4
+ # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
5
+ # PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
6
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
7
+ # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
8
+ # USE OR OTHER DEALINGS IN THE SOFTWARE.
9
+ #
10
+
11
+ require 'hula/helpers/socket_tools'
12
+
13
+ module Hula
14
+ class HttpProxyUpstreamSocks
15
+ include Helpers::SocketTools
16
+
17
+ def initialize(
18
+ polipo_bin: 'polipo',
19
+ socks_proxy:,
20
+ http_host: 'localhost',
21
+ http_port: free_port
22
+ )
23
+ @socks_proxy_host = socks_proxy.socks_host
24
+ @socks_proxy_port = socks_proxy.socks_port
25
+ @http_host = http_host
26
+ @http_port = http_port
27
+ @polipo_bin = polipo_bin
28
+
29
+ check_polipo_bin!
30
+ end
31
+
32
+ attr_reader :http_host, :http_port
33
+
34
+ def start
35
+ @process ||= start_polipo_process
36
+ end
37
+
38
+ def stop
39
+ return unless @process
40
+
41
+ Process.kill('TERM', @process) rescue Errno::ESRCH
42
+ Process.wait(@process) rescue Errno::ECHILD
43
+ @process = nil
44
+ end
45
+
46
+ private
47
+
48
+ attr_reader :socks_proxy_host, :socks_proxy_port, :polipo_bin
49
+
50
+
51
+ def start_polipo_process
52
+ pid = Process.spawn(polipo_command)
53
+ at_exit { stop }
54
+ wait_for_port(host: http_host, port: http_port)
55
+ Process.detach(pid)
56
+ pid
57
+ end
58
+
59
+ def polipo_command
60
+ "#{polipo_bin} diskCacheRoot='' \
61
+ proxyPort=#{http_port} \
62
+ socksParentProxy=#{socks_proxy_host}:#{socks_proxy_port} \
63
+ socksProxyType=socks4a"
64
+ end
65
+
66
+ def check_polipo_bin!
67
+ unless system("which #{polipo_bin} > /dev/null 2>&1")
68
+ raise "Could not run polipo (#{polipo_bin}). Please install, or put in PATH"
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,123 @@
1
+ # Copyright (c) 2014-2015 Pivotal Software, Inc.
2
+ # All rights reserved.
3
+ # THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
4
+ # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
5
+ # PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
6
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
7
+ # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
8
+ # USE OR OTHER DEALINGS IN THE SOFTWARE.
9
+ #
10
+
11
+ require 'forwardable'
12
+ require 'hula/service_broker/catalog'
13
+ require 'hula/service_broker/instance_binding'
14
+ require 'hula/service_broker/http_json_client'
15
+ require 'hula/service_broker/service_instance'
16
+
17
+ require 'securerandom'
18
+
19
+ module Hula
20
+ module ServiceBroker
21
+ class Api
22
+ extend Forwardable
23
+ def_delegators :catalog, :service_plan
24
+
25
+ def initialize(url:, username:, password:, http_client: HttpJsonClient.new)
26
+ @http_client = http_client
27
+
28
+ @url = URI(url)
29
+ @username = username
30
+ @password = password
31
+ end
32
+
33
+ attr_reader :url
34
+
35
+ def catalog
36
+ json = http_client.get(url_for('/v2/catalog'), auth: { username: username, password: password })
37
+ Catalog.new(json)
38
+ end
39
+
40
+ def provision_instance(plan, service_instance_id: SecureRandom.uuid)
41
+ http_provision_instance(
42
+ service_id: plan.service_id,
43
+ plan_id: plan.id,
44
+ service_instance_id: service_instance_id
45
+ )
46
+
47
+ ServiceInstance.new(id: service_instance_id)
48
+ end
49
+
50
+ def deprovision_instance(service_instance)
51
+ http_deprovision_service(service_instance_id: service_instance.id)
52
+ end
53
+
54
+ def bind_instance(service_instance, binding_id: SecureRandom.uuid)
55
+ result = http_bind_instance(
56
+ service_instance_id: service_instance.id,
57
+ binding_id: binding_id
58
+ )
59
+
60
+ InstanceBinding.new(
61
+ id: binding_id,
62
+ credentials: result.fetch(:credentials),
63
+ service_instance: service_instance
64
+ )
65
+ end
66
+
67
+ def unbind_instance(instance_binding)
68
+ http_unbind_instance(
69
+ service_instance_id: instance_binding.service_instance.id,
70
+ binding_id: instance_binding.id
71
+ )
72
+ end
73
+
74
+ def debug
75
+ http_client.get(url_for('/debug'), auth: { username: username, password: password })
76
+ end
77
+
78
+ private
79
+
80
+ def http_provision_instance(service_instance_id:, service_id:, plan_id:)
81
+ http_client.put(
82
+ url_for("/v2/service_instances/#{service_instance_id}"),
83
+ body: {
84
+ service_id: service_id,
85
+ plan_id: plan_id,
86
+ },
87
+ auth: { username: username, password: password }
88
+ )
89
+ end
90
+
91
+ def http_deprovision_service(service_instance_id:)
92
+ http_client.delete(
93
+ url_for("/v2/service_instances/#{service_instance_id}"),
94
+ auth: {
95
+ username: username,
96
+ password: password
97
+ }
98
+ )
99
+ end
100
+
101
+ def http_bind_instance(service_instance_id:, binding_id:)
102
+ http_client.put(
103
+ url_for("/v2/service_instances/#{service_instance_id}/service_bindings/#{binding_id}"),
104
+ body: {},
105
+ auth: { username: username, password: password }
106
+ )
107
+ end
108
+
109
+ def http_unbind_instance(service_instance_id:, binding_id:)
110
+ http_client.delete(
111
+ url_for("/v2/service_instances/#{service_instance_id}/service_bindings/#{binding_id}"),
112
+ auth: { username: username, password: password }
113
+ )
114
+ end
115
+
116
+ def url_for(path)
117
+ url.dup.tap { |uri| uri.path += path }
118
+ end
119
+
120
+ attr_reader :http_client, :username, :password
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,43 @@
1
+ # Copyright (c) 2014-2015 Pivotal Software, Inc.
2
+ # All rights reserved.
3
+ # THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
4
+ # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
5
+ # PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
6
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
7
+ # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
8
+ # USE OR OTHER DEALINGS IN THE SOFTWARE.
9
+ #
10
+
11
+ require 'hula/service_broker/errors'
12
+ require 'hula/service_broker/service'
13
+
14
+ module Hula
15
+ module ServiceBroker
16
+
17
+ class Catalog
18
+ attr_reader :services
19
+
20
+ def initialize(args = {})
21
+ @services = args.fetch(:services).map { |s| Service.new(s) }
22
+ end
23
+
24
+ def ==(other)
25
+ is_a?(other.class) &&
26
+ services == other.services
27
+ end
28
+
29
+ def service(service_name)
30
+ services.find { |s| s.name == service_name } or
31
+ fail(ServiceNotFoundError, [
32
+ %{Unknown service with name: #{service_name.inspect}},
33
+ " Known service names: #{services.map(&:name).inspect}"
34
+ ].join("\n")
35
+ )
36
+ end
37
+
38
+ def service_plan(service_name, plan_name)
39
+ service(service_name).plan(plan_name)
40
+ end
41
+ end
42
+ end
43
+ end