Skip to content

Commit da667dd

Browse files
a18ehoffmaen
authored andcommitted
Introduce per-route options
This commit adds a configurable load-balancing algorithm as a first example of per-route options. It adds the 'options' field to the route object in the V3 API, and the app manifest. The options field is an object storing key-value pairs, with 'lb_algo' being the only supported key for now. The supported load-balancing algorithms are 'round-robin' and 'least-connections'. The options field is introduced as a column in the database route table, and forwarded to the Diego backend. Co-authored-by: Alexander Nicke <[email protected]> See: cloudfoundry/capi-release#482 See: https://github.com/cloudfoundry/community/blob/main/toc/rfc/rfc-0027-generic-per-route-features.md
1 parent d84cd86 commit da667dd

25 files changed

+606
-23
lines changed

app/actions/manifest_route_update.rb

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
require 'cloud_controller/app_manifest/manifest_route'
22
require 'actions/route_create'
3+
require 'actions/route_update'
34

45
module VCAP::CloudController
56
class ManifestRouteUpdate
@@ -81,7 +82,8 @@ def find_or_create_valid_route(app, manifest_route, user_audit_info)
8182
message = RouteCreateMessage.new({
8283
'host' => host,
8384
'path' => manifest_route[:path],
84-
'port' => manifest_route[:port]
85+
'port' => manifest_route[:port],
86+
'options' => manifest_route[:options]
8587
})
8688

8789
route = RouteCreate.new(user_audit_info).create(
@@ -90,6 +92,12 @@ def find_or_create_valid_route(app, manifest_route, user_audit_info)
9092
domain: existing_domain,
9193
manifest_triggered: true
9294
)
95+
elsif route[:options] != manifest_route[:options]
96+
message = RouteUpdateMessage.new({
97+
'options' => manifest_route[:options]
98+
})
99+
route = RouteUpdate.new.update(route:, message:)
100+
93101
elsif route.space.guid != app.space_guid
94102
raise InvalidRoute.new('Routes cannot be mapped to destinations in different spaces')
95103
end

app/actions/route_create.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ def create(message:, space:, domain:, manifest_triggered: false)
1515
path: message.path || '',
1616
port: port(message, domain),
1717
space: space,
18-
domain: domain
18+
domain: domain,
19+
options: message.options
1920
)
2021

2122
Route.db.transaction do

app/actions/route_update.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,16 @@ module VCAP::CloudController
22
class RouteUpdate
33
def update(route:, message:)
44
Route.db.transaction do
5+
if message.requested?(:options)
6+
route.options = if message.options.nil?
7+
nil
8+
elsif route.options.nil?
9+
message.options
10+
else
11+
route.options.merge(message.options)
12+
end
13+
end
14+
route.save
515
MetadataUpdate.update(route, message)
616
end
717

app/messages/manifest_routes_update_message.rb

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
require 'messages/base_message'
2+
require 'messages/route_options_message'
23
require 'cloud_controller/app_manifest/manifest_route'
34

45
module VCAP::CloudController
@@ -7,9 +8,20 @@ class ManifestRoutesUpdateMessage < BaseMessage
78

89
class ManifestRoutesYAMLValidator < ActiveModel::Validator
910
def validate(record)
10-
return unless is_not_array?(record.routes) || contains_non_route_hash_values?(record.routes)
11+
if is_not_array?(record.routes) || contains_non_route_hash_values?(record.routes)
12+
record.errors.add(:routes, message: 'must be a list of route objects')
13+
return
14+
end
1115

12-
record.errors.add(:routes, message: 'must be a list of route objects')
16+
if contains_invalid_route_options?(record.routes)
17+
record.errors.add(:routes, message: 'contains invalid route options')
18+
return
19+
end
20+
21+
return unless contains_invalid_lb_algo?(record.routes)
22+
23+
record.errors.add(:routes, message: 'contains an invalid loadbalancing-algorithm option')
24+
nil
1325
end
1426

1527
def is_not_array?(routes)
@@ -19,6 +31,26 @@ def is_not_array?(routes)
1931
def contains_non_route_hash_values?(routes)
2032
routes.any? { |r| !(r.is_a?(Hash) && r[:route].present?) }
2133
end
34+
35+
def contains_invalid_route_options?(routes)
36+
routes.any? do |r|
37+
next unless r[:options]
38+
39+
return true unless r[:options].is_a?(Hash)
40+
41+
return false if r[:options].empty?
42+
43+
return r[:options].keys.all? { |key| RouteOptionsMessage::VALID_MANIFEST_ROUTE_OPTIONS.exclude?(key) }
44+
end
45+
end
46+
47+
def contains_invalid_lb_algo?(routes)
48+
routes.any? do |r|
49+
next unless r[:options] && r[:options][:'loadbalancing-algorithm']
50+
51+
return true if r[:options][:'loadbalancing-algorithm'] && RouteOptionsMessage::VALID_LOADBALANCING_ALGORITHMS.exclude?(r[:options][:'loadbalancing-algorithm'])
52+
end
53+
end
2254
end
2355

2456
validates_with NoAdditionalKeysValidator
@@ -32,10 +64,12 @@ def contains_non_route_hash_values?(routes)
3264

3365
def manifest_route_mappings
3466
@manifest_route_mappings ||= routes.map do |route|
35-
{
36-
route: ManifestRoute.parse(route[:route]),
67+
r = {
68+
route: ManifestRoute.parse(route[:route], route[:options]),
3769
protocol: route[:protocol]
3870
}
71+
r[:options] = route[:options] unless route[:options].nil?
72+
r
3973
end
4074
end
4175

app/messages/route_create_message.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
require 'messages/metadata_base_message'
2+
require 'messages/route_options_message'
23

34
module VCAP::CloudController
45
class RouteCreateMessage < MetadataBaseMessage
@@ -10,6 +11,7 @@ class RouteCreateMessage < MetadataBaseMessage
1011
path
1112
port
1213
relationships
14+
options
1315
]
1416

1517
validates :host,
@@ -56,6 +58,7 @@ class RouteCreateMessage < MetadataBaseMessage
5658

5759
validates_with NoAdditionalKeysValidator
5860
validates_with RelationshipValidator
61+
validates_with OptionsValidator
5962

6063
delegate :space_guid, to: :relationships_message
6164
delegate :domain_guid, to: :relationships_message
@@ -65,6 +68,10 @@ def relationships_message
6568
@relationships_message ||= Relationships.new(relationships&.deep_symbolize_keys)
6669
end
6770

71+
def options_message
72+
@options_message ||= RouteOptionsMessage.new(options&.deep_symbolize_keys)
73+
end
74+
6875
def wildcard?
6976
host == '*'
7077
end
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
require 'messages/metadata_base_message'
2+
3+
module VCAP::CloudController
4+
class RouteOptionsMessage < BaseMessage
5+
VALID_MANIFEST_ROUTE_OPTIONS = %i[loadbalancing-algorithm].freeze
6+
VALID_ROUTE_OPTIONS = %i[lb_algo].freeze
7+
VALID_LOADBALANCING_ALGORITHMS = %w[round-robin least-connections].freeze
8+
9+
register_allowed_keys %i[lb_algo]
10+
validates_with NoAdditionalKeysValidator
11+
validates :lb_algo,
12+
inclusion: { in: VALID_LOADBALANCING_ALGORITHMS, message: "'%<value>s' is not a supported load-balancing algorithm" },
13+
presence: true,
14+
allow_nil: true
15+
end
16+
end

app/messages/route_update_message.rb

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,19 @@
11
require 'messages/metadata_base_message'
2+
require 'messages/route_options_message'
23

34
module VCAP::CloudController
45
class RouteUpdateMessage < MetadataBaseMessage
5-
register_allowed_keys []
6+
register_allowed_keys %i[options]
7+
8+
def self.options_requested?
9+
@options_requested ||= proc { |a| a.requested?(:options) }
10+
end
11+
12+
def options_message
13+
@options_message ||= RouteOptionsMessage.new(options&.deep_symbolize_keys)
14+
end
15+
16+
validates_with OptionsValidator, if: options_requested?
617

718
validates_with NoAdditionalKeysValidator
819
end

app/messages/validators.rb

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,26 @@ def validate(record)
240240
end
241241
end
242242

243+
class OptionsValidator < ActiveModel::Validator
244+
def validate(record)
245+
# Empty option hashes are allowed, so we skip further validation
246+
record.options.blank? && return
247+
248+
unless record.options.is_a?(Hash)
249+
record.errors.add(:options, message: "'options' is not a valid object")
250+
return
251+
end
252+
253+
opt = record.options_message
254+
255+
return if opt.valid?
256+
257+
opt.errors.full_messages.each do |message|
258+
record.errors.add(:options, message:)
259+
end
260+
end
261+
end
262+
243263
class ToOneRelationshipValidator < ActiveModel::EachValidator
244264
def validate_each(record, attribute, relationship)
245265
if has_correct_structure?(relationship)

app/models/runtime/route.rb

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@ class OutOfVIPException < CloudController::Errors::InvalidRelation; end
4040

4141
add_association_dependencies route_mappings: :destroy
4242

43-
export_attributes :host, :path, :domain_guid, :space_guid, :service_instance_guid, :port
44-
import_attributes :host, :path, :domain_guid, :space_guid, :app_guids, :port
43+
export_attributes :host, :path, :domain_guid, :space_guid, :service_instance_guid, :port, :options
44+
import_attributes :host, :path, :domain_guid, :space_guid, :app_guids, :port, :options
4545

4646
add_association_dependencies labels: :destroy
4747
add_association_dependencies annotations: :destroy
@@ -71,6 +71,23 @@ def as_summary_json
7171
}
7272
end
7373

74+
def options_with_serialization=(opts)
75+
self.options_without_serialization = Oj.dump(opts)
76+
end
77+
78+
alias_method :options_without_serialization=, :options=
79+
alias_method :options=, :options_with_serialization=
80+
81+
def options_with_serialization
82+
string = options_without_serialization
83+
return nil if string.blank?
84+
85+
Oj.load(string)
86+
end
87+
88+
alias_method :options_without_serialization, :options
89+
alias_method :options, :options_with_serialization
90+
7491
alias_method :old_path, :path
7592
def path
7693
old_path.nil? ? '' : old_path

app/presenters/v3/app_manifest_presenters/route_properties_presenter.rb

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,17 @@ module AppManifestPresenters
55
class RoutePropertiesPresenter
66
def to_hash(route_mappings:, app:, **_)
77
route_hashes = route_mappings.map do |route_mapping|
8-
{
8+
route_hash = {
99
route: route_mapping.route.uri,
1010
protocol: route_mapping.protocol
1111
}
12+
13+
if route_mapping.route.options
14+
route_hash[:options] = {}
15+
route_hash[:options][:'loadbalancing-algorithm'] = route_mapping.route.options[:lb_algo] if route_mapping.route.options[:lb_algo]
16+
end
17+
18+
route_hash
1219
end
1320

1421
{ routes: alphabetize(route_hashes).presence }

0 commit comments

Comments
 (0)