vcap_common 1.0.10
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.
- data/lib/json_message.rb +139 -0
- data/lib/json_schema.rb +84 -0
- data/lib/services/api/async_requests.rb +43 -0
- data/lib/services/api/clients/service_gateway_client.rb +107 -0
- data/lib/services/api/const.rb +9 -0
- data/lib/services/api/messages.rb +153 -0
- data/lib/services/api/util.rb +17 -0
- data/lib/services/api.rb +6 -0
- data/lib/vcap/common.rb +236 -0
- data/lib/vcap/component.rb +172 -0
- data/lib/vcap/config.rb +32 -0
- data/lib/vcap/fiber_tracing.rb +45 -0
- data/lib/vcap/json_schema.rb +202 -0
- data/lib/vcap/priority_queue.rb +164 -0
- data/lib/vcap/process_utils.rb +43 -0
- data/lib/vcap/quota.rb +152 -0
- data/lib/vcap/rolling_metric.rb +74 -0
- data/lib/vcap/spec/em.rb +32 -0
- data/lib/vcap/spec/forked_component/base.rb +87 -0
- data/lib/vcap/spec/forked_component/nats_server.rb +28 -0
- data/lib/vcap/spec/forked_component.rb +2 -0
- data/lib/vcap/subprocess.rb +211 -0
- data/lib/vcap/user_pools/user_ops.rb +47 -0
- data/lib/vcap/user_pools/user_pool.rb +45 -0
- data/lib/vcap/user_pools/user_pool_util.rb +107 -0
- metadata +166 -0
data/lib/json_message.rb
ADDED
@@ -0,0 +1,139 @@
|
|
1
|
+
# Copyright (c) 2009-2011 VMware, Inc.
|
2
|
+
require 'rubygems'
|
3
|
+
|
4
|
+
require 'yajl'
|
5
|
+
|
6
|
+
require 'json_schema'
|
7
|
+
|
8
|
+
class JsonMessage
|
9
|
+
# Base error class that all other JsonMessage related errors should inherit from
|
10
|
+
class Error < StandardError
|
11
|
+
end
|
12
|
+
|
13
|
+
# Failed to parse json during +decode+
|
14
|
+
class ParseError < Error
|
15
|
+
end
|
16
|
+
|
17
|
+
# One or more field's values didn't match their schema
|
18
|
+
class ValidationError < Error
|
19
|
+
def initialize(field_errs)
|
20
|
+
@field_errs = field_errs
|
21
|
+
end
|
22
|
+
|
23
|
+
def to_s
|
24
|
+
err_strs = @field_errs.map{|f, e| "Field: #{f}, Error: #{e}"}
|
25
|
+
err_strs.join(', ')
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class Field
|
30
|
+
attr_reader :name, :schema, :required
|
31
|
+
|
32
|
+
def initialize(name, schema, required=true)
|
33
|
+
@name = name
|
34
|
+
@schema = schema.is_a?(JsonSchema) ? schema : JsonSchema.new(schema)
|
35
|
+
@required = required
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
class << self
|
40
|
+
attr_reader :fields
|
41
|
+
|
42
|
+
def schema(&blk)
|
43
|
+
instance_eval &blk
|
44
|
+
end
|
45
|
+
|
46
|
+
def decode(json)
|
47
|
+
begin
|
48
|
+
dec_json = Yajl::Parser.parse(json)
|
49
|
+
rescue => e
|
50
|
+
raise ParseError, e.to_s
|
51
|
+
end
|
52
|
+
|
53
|
+
from_decoded_json(dec_json)
|
54
|
+
end
|
55
|
+
|
56
|
+
def from_decoded_json(dec_json)
|
57
|
+
raise ParseError, "Decoded JSON cannot be nil" unless dec_json
|
58
|
+
|
59
|
+
errs = {}
|
60
|
+
|
61
|
+
# Treat null values as if the keys aren't present. This isn't as strict as one would like,
|
62
|
+
# but conforms to typical use cases.
|
63
|
+
dec_json.delete_if {|k, v| v == nil}
|
64
|
+
|
65
|
+
# Collect errors by field
|
66
|
+
@fields.each do |name, field|
|
67
|
+
err = nil
|
68
|
+
name_s = name.to_s
|
69
|
+
if dec_json.has_key?(name_s)
|
70
|
+
err = field.schema.validate(dec_json[name_s])
|
71
|
+
elsif field.required
|
72
|
+
err = "Missing field #{name}"
|
73
|
+
end
|
74
|
+
errs[name] = err if err
|
75
|
+
end
|
76
|
+
|
77
|
+
raise ValidationError.new(errs) unless errs.empty?
|
78
|
+
|
79
|
+
new(dec_json)
|
80
|
+
end
|
81
|
+
|
82
|
+
def required(field_name, schema=JsonSchema::WILDCARD)
|
83
|
+
define_field(field_name, schema, true)
|
84
|
+
end
|
85
|
+
|
86
|
+
def optional(field_name, schema=JsonSchema::WILDCARD)
|
87
|
+
define_field(field_name, schema, false)
|
88
|
+
end
|
89
|
+
|
90
|
+
protected
|
91
|
+
|
92
|
+
def define_field(name, schema, required)
|
93
|
+
name = name.to_sym
|
94
|
+
|
95
|
+
@fields ||= {}
|
96
|
+
@fields[name] = Field.new(name, schema, required)
|
97
|
+
|
98
|
+
define_method name.to_sym do
|
99
|
+
@msg[name]
|
100
|
+
end
|
101
|
+
|
102
|
+
define_method "#{name}=".to_sym do |value|
|
103
|
+
set_field(name, value)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def initialize(fields={})
|
109
|
+
@msg = {}
|
110
|
+
fields.each {|k, v| set_field(k, v)}
|
111
|
+
end
|
112
|
+
|
113
|
+
def encode
|
114
|
+
if self.class.fields
|
115
|
+
missing_fields = {}
|
116
|
+
self.class.fields.each do |name, field|
|
117
|
+
missing_fields[name] = "Missing field #{name}" unless (!field.required || @msg.has_key?(name))
|
118
|
+
end
|
119
|
+
raise ValidationError.new(missing_fields) unless missing_fields.empty?
|
120
|
+
end
|
121
|
+
|
122
|
+
Yajl::Encoder.encode(@msg)
|
123
|
+
end
|
124
|
+
|
125
|
+
def extract
|
126
|
+
@msg.dup.freeze
|
127
|
+
end
|
128
|
+
|
129
|
+
protected
|
130
|
+
|
131
|
+
def set_field(field, value)
|
132
|
+
field = field.to_sym
|
133
|
+
raise ValidationError.new({field => "Unknown field #{field}"}) unless self.class.fields.has_key?(field)
|
134
|
+
|
135
|
+
errs = self.class.fields[field].schema.validate(value)
|
136
|
+
raise ValidationError.new({field => errs}) if errs
|
137
|
+
@msg[field] = value
|
138
|
+
end
|
139
|
+
end
|
data/lib/json_schema.rb
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
# Copyright (c) 2009-2011 VMware, Inc.
|
2
|
+
# This class provides dead simple declarative validation for decoded json using
|
3
|
+
# a fairly intuitive DSL like syntax.
|
4
|
+
#
|
5
|
+
# For example, the following is a sample schema that exercises all functionality
|
6
|
+
#
|
7
|
+
# {'foo' => [String], # 'foo' must be a list of strings
|
8
|
+
# 'bar' => {'baz' => Fixnum, # 'bar' must be a hash where
|
9
|
+
# 'jaz' => /foo/, # 'baz' is a Fixnum, and
|
10
|
+
# } # 'jaz' matches the regex /foo/
|
11
|
+
# }
|
12
|
+
#
|
13
|
+
class JsonSchema
|
14
|
+
WILDCARD = Object
|
15
|
+
|
16
|
+
# TODO(mjp): validate that schema is syntatically correct
|
17
|
+
|
18
|
+
def initialize(schema)
|
19
|
+
@schema = schema
|
20
|
+
end
|
21
|
+
|
22
|
+
def validate(json)
|
23
|
+
_validate(json, @schema)
|
24
|
+
end
|
25
|
+
|
26
|
+
protected
|
27
|
+
|
28
|
+
def _validate(json, schema)
|
29
|
+
case schema
|
30
|
+
when Class
|
31
|
+
# Terminal case, type check
|
32
|
+
klass = json.class
|
33
|
+
if json.is_a? schema
|
34
|
+
nil
|
35
|
+
else
|
36
|
+
"Type mismatch (expected #{schema}, got #{klass})"
|
37
|
+
end
|
38
|
+
|
39
|
+
when Hash
|
40
|
+
# Recursive case, check for required params, recursively check them against the supplied schema
|
41
|
+
missing_keys = schema.keys.select {|k| !json.has_key?(k)} if json.is_a? Hash
|
42
|
+
|
43
|
+
if !(json.is_a? Hash)
|
44
|
+
"Type mismatch (expected hash, got #{json.class})"
|
45
|
+
elsif missing_keys.length > 0
|
46
|
+
"Missing params: '#{missing_keys.join(', ')}'"
|
47
|
+
else
|
48
|
+
errs = nil
|
49
|
+
schema.each_key do |k|
|
50
|
+
sub_errs = _validate(json[k], schema[k])
|
51
|
+
if sub_errs
|
52
|
+
errs ||= {}
|
53
|
+
errs[k] = sub_errs
|
54
|
+
end
|
55
|
+
end
|
56
|
+
errs
|
57
|
+
end
|
58
|
+
|
59
|
+
when Array
|
60
|
+
# Recursive case, check that array isn't empty, recursively check array against supplied schema
|
61
|
+
if !(json.is_a? Array)
|
62
|
+
"Type mismatch (expected array, got #{json.class}"
|
63
|
+
else
|
64
|
+
errs = nil
|
65
|
+
json.each do |v|
|
66
|
+
errs = _validate(v, schema[0])
|
67
|
+
break if errs
|
68
|
+
end
|
69
|
+
errs
|
70
|
+
end
|
71
|
+
|
72
|
+
when Regexp
|
73
|
+
if schema.match(json)
|
74
|
+
nil
|
75
|
+
else
|
76
|
+
"Invalid value (doesn't match '#{schema.source})"
|
77
|
+
end
|
78
|
+
|
79
|
+
else
|
80
|
+
# Terminal case, value check
|
81
|
+
"Value mismatch (expected '#{schema}', got #{json})" unless json == schema
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# Copyright (c) 2009-2011 VMware, Inc.
|
2
|
+
require 'eventmachine'
|
3
|
+
require 'em-http-request'
|
4
|
+
require 'fiber'
|
5
|
+
|
6
|
+
require 'services/api/const'
|
7
|
+
|
8
|
+
module VCAP
|
9
|
+
module Services
|
10
|
+
module Api
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
module VCAP::Services::Api
|
16
|
+
class AsyncHttpRequest
|
17
|
+
class << self
|
18
|
+
def new(url, token, verb, timeout, msg=VCAP::Services::Api::EMPTY_REQUEST)
|
19
|
+
|
20
|
+
req = {
|
21
|
+
:head => {
|
22
|
+
VCAP::Services::Api::GATEWAY_TOKEN_HEADER => token,
|
23
|
+
'Content-Type' => 'application/json',
|
24
|
+
},
|
25
|
+
:body => msg.encode,
|
26
|
+
}
|
27
|
+
if timeout
|
28
|
+
EM::HttpRequest.new(url, :inactivity_timeout => timeout).send(verb.to_sym, req)
|
29
|
+
else
|
30
|
+
EM::HttpRequest.new(url).send(verb.to_sym, req)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def fibered(url, token, verb, timeout, msg=VCAP::Services::Api::EMPTY_REQUEST)
|
35
|
+
req = new(url, token, verb, timeout, msg)
|
36
|
+
f = Fiber.current
|
37
|
+
req.callback { f.resume(req) }
|
38
|
+
req.errback { f.resume(req) }
|
39
|
+
Fiber.yield
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
# Copyright (c) 2009-2011 VMware, Inc.
|
2
|
+
require 'net/http'
|
3
|
+
|
4
|
+
require 'services/api/const'
|
5
|
+
require 'services/api/messages'
|
6
|
+
|
7
|
+
module VCAP
|
8
|
+
module Services
|
9
|
+
module Api
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class VCAP::Services::Api::ServiceGatewayClient
|
15
|
+
|
16
|
+
class UnexpectedResponse < StandardError
|
17
|
+
attr_reader :response
|
18
|
+
|
19
|
+
def initialize(resp)
|
20
|
+
@response = resp
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
attr_reader :host, :port, :token
|
25
|
+
|
26
|
+
def initialize(host, token, port=80)
|
27
|
+
@host = host
|
28
|
+
@port = port
|
29
|
+
@token = token
|
30
|
+
@hdrs = {
|
31
|
+
'Content-Type' => 'application/json',
|
32
|
+
VCAP::Services::Api::GATEWAY_TOKEN_HEADER => @token
|
33
|
+
}
|
34
|
+
end
|
35
|
+
|
36
|
+
def provision(args)
|
37
|
+
msg = VCAP::Services::Api::GatewayProvisionRequest.new(args)
|
38
|
+
resp = perform_request(Net::HTTP::Post, '/gateway/v1/configurations', msg)
|
39
|
+
VCAP::Services::Api::GatewayProvisionResponse.decode(resp.body)
|
40
|
+
end
|
41
|
+
|
42
|
+
def unprovision(args)
|
43
|
+
perform_request(Net::HTTP::Delete, "/gateway/v1/configurations/#{args[:service_id]}")
|
44
|
+
end
|
45
|
+
|
46
|
+
def create_snapshot(args)
|
47
|
+
resp = perform_request(Net::HTTP::Post, "/gateway/v1/configurations/#{args[:service_id]}/snapshots")
|
48
|
+
VCAP::Services::Api::Job.decode(resp.body)
|
49
|
+
end
|
50
|
+
|
51
|
+
def enum_snapshots(args)
|
52
|
+
resp = perform_request(Net::HTTP::Get, "/gateway/v1/configurations/#{args[:service_id]}/snapshots")
|
53
|
+
VCAP::Services::Api::SnapshotList.decode(resp.body)
|
54
|
+
end
|
55
|
+
|
56
|
+
def snapshot_details(args)
|
57
|
+
resp = perform_request(Net::HTTP::Get, "/gateway/v1/configurations/#{args[:service_id]}/snapshots/#{args[:snapshot_id]}")
|
58
|
+
VCAP::Services::Api::Snapshot.decode(resp.body)
|
59
|
+
end
|
60
|
+
|
61
|
+
def rollback_snapshot(args)
|
62
|
+
resp = perform_request(Net::HTTP::Put, "/gateway/v1/configurations/#{args[:service_id]}/snapshots/#{args[:snapshot_id]}")
|
63
|
+
VCAP::Services::Api::Job.decode(resp.body)
|
64
|
+
end
|
65
|
+
|
66
|
+
def serialized_url(args)
|
67
|
+
resp = perform_request(Net::HTTP::Get, "/gateway/v1/configurations/#{args[:service_id]}/serialized/url")
|
68
|
+
VCAP::Services::Api::Job.decode(resp.body)
|
69
|
+
end
|
70
|
+
|
71
|
+
def import_from_url(args)
|
72
|
+
resp = perform_request(Net::HTTP::Put, "/gateway/v1/configurations/#{args[:service_id]}/serialized/url", args[:msg])
|
73
|
+
VCAP::Services::Api::Job.decode(resp.body)
|
74
|
+
end
|
75
|
+
|
76
|
+
def import_from_data(args)
|
77
|
+
resp = perform_request(Net::HTTP::Put, "/gateway/v1/configurations/#{args[:service_id]}/serialized/data", args[:msg])
|
78
|
+
VCAP::Services::Api::Job.decode(resp.body)
|
79
|
+
end
|
80
|
+
|
81
|
+
def job_info(args)
|
82
|
+
resp = perform_request(Net::HTTP::Get, "/gateway/v1/configurations/#{args[:service_id]}/jobs/#{args[:job_id]}")
|
83
|
+
VCAP::Services::Api::Job.decode(resp.body)
|
84
|
+
end
|
85
|
+
|
86
|
+
def bind(args)
|
87
|
+
msg = VCAP::Services::Api::GatewayBindRequest.new(args)
|
88
|
+
resp = perform_request(Net::HTTP::Post, "/gateway/v1/configurations/#{msg.service_id}/handles", msg)
|
89
|
+
VCAP::Services::Api::GatewayBindResponse.decode(resp.body)
|
90
|
+
end
|
91
|
+
|
92
|
+
def unbind(args)
|
93
|
+
msg = VCAP::Services::Api::GatewayUnbindRequest.new(args)
|
94
|
+
perform_request(Net::HTTP::Delete, "/gateway/v1/configurations/#{msg.service_id}/handles/#{msg.handle_id}", msg)
|
95
|
+
end
|
96
|
+
|
97
|
+
protected
|
98
|
+
|
99
|
+
def perform_request(klass, path, msg=VCAP::Services::Api::EMPTY_REQUEST)
|
100
|
+
req = klass.new(path, initheader=@hdrs)
|
101
|
+
req.body = msg.encode
|
102
|
+
resp = Net::HTTP.new(@host, @port).start {|http| http.request(req)}
|
103
|
+
raise UnexpectedResponse, resp unless resp.is_a? Net::HTTPOK
|
104
|
+
resp
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
@@ -0,0 +1,153 @@
|
|
1
|
+
# Copyright (c) 2009-2011 VMware, Inc.
|
2
|
+
require 'uri'
|
3
|
+
|
4
|
+
require 'services/api/const'
|
5
|
+
require 'json_message'
|
6
|
+
|
7
|
+
module VCAP
|
8
|
+
module Services
|
9
|
+
module Api
|
10
|
+
|
11
|
+
class EmptyRequest < JsonMessage
|
12
|
+
end
|
13
|
+
EMPTY_REQUEST = EmptyRequest.new.freeze
|
14
|
+
|
15
|
+
#
|
16
|
+
# Tell the CloudController about a service
|
17
|
+
# NB: Deleting an offering takes all args in the url
|
18
|
+
#
|
19
|
+
class ServiceOfferingRequest < JsonMessage
|
20
|
+
required :label, SERVICE_LABEL_REGEX
|
21
|
+
required :url, URI::regexp(%w(http https))
|
22
|
+
|
23
|
+
optional :description, String
|
24
|
+
optional :info_url, URI::regexp(%w(http https))
|
25
|
+
optional :tags, [String]
|
26
|
+
optional :plans, [String]
|
27
|
+
optional :plan_options
|
28
|
+
optional :binding_options
|
29
|
+
optional :acls
|
30
|
+
optional :active
|
31
|
+
optional :timeout, Integer
|
32
|
+
end
|
33
|
+
|
34
|
+
class BrokeredServiceOfferingRequest < JsonMessage
|
35
|
+
required :label, SERVICE_LABEL_REGEX
|
36
|
+
required :options, [{"name" => String, "credentials" => Hash}]
|
37
|
+
optional :description, String
|
38
|
+
end
|
39
|
+
|
40
|
+
class HandleUpdateRequest < JsonMessage
|
41
|
+
required :service_id, String
|
42
|
+
required :configuration
|
43
|
+
required :credentials
|
44
|
+
end
|
45
|
+
|
46
|
+
class ListHandlesResponse < JsonMessage
|
47
|
+
required :handles, [::JsonSchema::WILDCARD]
|
48
|
+
end
|
49
|
+
|
50
|
+
class ListBrokeredServicesResponse < JsonMessage
|
51
|
+
required :brokered_services, [{"label" => String, "description" => String, "acls" => {"users" => [String], "wildcards" => [String]}}]
|
52
|
+
end
|
53
|
+
|
54
|
+
#
|
55
|
+
# Provision a service instance
|
56
|
+
# NB: Unprovision takes all args in the url
|
57
|
+
#
|
58
|
+
class CloudControllerProvisionRequest < JsonMessage
|
59
|
+
required :label, SERVICE_LABEL_REGEX
|
60
|
+
required :name, String
|
61
|
+
required :plan, String
|
62
|
+
|
63
|
+
optional :plan_option
|
64
|
+
end
|
65
|
+
|
66
|
+
class GatewayProvisionRequest < JsonMessage
|
67
|
+
required :label, SERVICE_LABEL_REGEX
|
68
|
+
required :name, String
|
69
|
+
required :plan, String
|
70
|
+
required :email, String
|
71
|
+
|
72
|
+
optional :plan_option
|
73
|
+
end
|
74
|
+
|
75
|
+
class GatewayProvisionResponse < JsonMessage
|
76
|
+
required :service_id, String
|
77
|
+
required :data
|
78
|
+
required :credentials
|
79
|
+
end
|
80
|
+
|
81
|
+
#
|
82
|
+
# Bind a previously provisioned service to an app
|
83
|
+
#
|
84
|
+
class CloudControllerBindRequest < JsonMessage
|
85
|
+
required :service_id, String
|
86
|
+
required :app_id, Integer
|
87
|
+
required :binding_options
|
88
|
+
end
|
89
|
+
|
90
|
+
class GatewayBindRequest < JsonMessage
|
91
|
+
required :service_id, String
|
92
|
+
required :label, String
|
93
|
+
required :email, String
|
94
|
+
required :binding_options
|
95
|
+
end
|
96
|
+
|
97
|
+
class GatewayUnbindRequest < JsonMessage
|
98
|
+
required :service_id, String
|
99
|
+
required :handle_id, String
|
100
|
+
required :binding_options
|
101
|
+
end
|
102
|
+
|
103
|
+
class CloudControllerBindResponse < JsonMessage
|
104
|
+
required :label, SERVICE_LABEL_REGEX
|
105
|
+
required :binding_token, String
|
106
|
+
end
|
107
|
+
|
108
|
+
class GatewayBindResponse < JsonMessage
|
109
|
+
required :service_id, String
|
110
|
+
required :configuration
|
111
|
+
required :credentials
|
112
|
+
end
|
113
|
+
|
114
|
+
# Bind app_name using binding_token
|
115
|
+
class BindExternalRequest < JsonMessage
|
116
|
+
required :binding_token, String
|
117
|
+
required :app_id, Integer
|
118
|
+
end
|
119
|
+
|
120
|
+
class BindingTokenRequest < JsonMessage
|
121
|
+
required :service_id, String
|
122
|
+
required :binding_options
|
123
|
+
end
|
124
|
+
|
125
|
+
class Snapshot < JsonMessage
|
126
|
+
required :snapshot_id, String
|
127
|
+
required :date, String
|
128
|
+
required :size, Integer
|
129
|
+
end
|
130
|
+
|
131
|
+
class SnapshotList < JsonMessage
|
132
|
+
required :snapshots, [::JsonSchema::WILDCARD]
|
133
|
+
end
|
134
|
+
|
135
|
+
class Job < JsonMessage
|
136
|
+
required :job_id, String
|
137
|
+
required :status, String
|
138
|
+
required :start_time, String
|
139
|
+
optional :description, String
|
140
|
+
optional :complete_time, String
|
141
|
+
optional :result, ::JsonSchema::WILDCARD
|
142
|
+
end
|
143
|
+
|
144
|
+
class SerializedURL < JsonMessage
|
145
|
+
required :url, URI::regexp(%w(http https))
|
146
|
+
end
|
147
|
+
|
148
|
+
class SerializedData < JsonMessage
|
149
|
+
required :data, String
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# Copyright (c) 2009-2011 VMware, Inc.
|
2
|
+
module VCAP
|
3
|
+
module Services
|
4
|
+
module Api
|
5
|
+
end
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
class VCAP::Services::Api::Util
|
10
|
+
class << self
|
11
|
+
def parse_label(label)
|
12
|
+
raise ArgumentError, "Invalid label" unless label.match(/-/)
|
13
|
+
name, _, version = label.rpartition(/-/)
|
14
|
+
[name, version]
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|