Skip to content

Commit 05e617f

Browse files
hoffmaena18e
andauthored
Implement per-route options in Cloud Controller (#4080)
* 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 * Add route-options documentation Add documentation for the route options object, and its supported fields. * Adjust route options behaviour for manifest push Overwrite behaviour for route options is now fixed and tested: Existing options are not modified if options is nil, {} or not provided A single option (e.g. loadbalancing-algorithm) can be removed by setting its value to nil adjust test for manifest push: options {key:nil} should not modify the existing value * Adjust behaviour to new decisions: options default: {} API: options is not nullable specific option is additive specific option is nullable empty hash does not change anything (additive) get empty options -> {} manifest: options and specific option is nullable, but no-op * Remove route option validations from manifest_route * Rename 'lb_algo' and 'loadbalancing-algorithm' to 'algorithm' * Disallow null in manifest; Cleanup error outputs --------- Co-authored-by: Alexander Nicke <[email protected]>
1 parent 36ba7ed commit 05e617f

37 files changed

+1028
-96
lines changed

app/actions/manifest_route_update.rb

Lines changed: 10 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(
@@ -92,6 +94,13 @@ def find_or_create_valid_route(app, manifest_route, user_audit_info)
9294
)
9395
elsif route.space.guid != app.space_guid
9496
raise InvalidRoute.new('Routes cannot be mapped to destinations in different spaces')
97+
elsif manifest_route[:options] && route[:options] != manifest_route[:options]
98+
# remove nil values from options
99+
manifest_route[:options] = manifest_route[:options].compact
100+
message = RouteUpdateMessage.new({
101+
'options' => manifest_route[:options]
102+
})
103+
route = RouteUpdate.new.update(route:, message:)
95104
end
96105

97106
return route

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.nil? ? {} : message.options.compact
1920
)
2021

2122
Route.db.transaction do

app/actions/route_update.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ module VCAP::CloudController
22
class RouteUpdate
33
def update(route:, message:)
44
Route.db.transaction do
5+
route.options = route.options.symbolize_keys.merge(message.options).compact if message.requested?(:options)
6+
7+
route.save
58
MetadataUpdate.update(route, message)
69
end
710

app/messages/manifest_routes_update_message.rb

Lines changed: 47 additions & 2 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
@@ -25,22 +26,66 @@ def contains_non_route_hash_values?(routes)
2526
validates_with ManifestRoutesYAMLValidator, if: proc { |record| record.requested?(:routes) }
2627
validate :routes_are_uris, if: proc { |record| record.requested?(:routes) }
2728
validate :route_protocols_are_valid, if: proc { |record| record.requested?(:routes) }
29+
validate :route_options_are_valid, if: proc { |record| record.requested?(:routes) }
30+
validate :loadbalancings_are_valid, if: proc { |record| record.requested?(:routes) }
2831
validate :no_route_is_boolean
2932
validate :default_route_is_boolean
3033
validate :random_route_is_boolean
3134
validate :random_route_and_default_route_conflict
3235

3336
def manifest_route_mappings
3437
@manifest_route_mappings ||= routes.map do |route|
35-
{
36-
route: ManifestRoute.parse(route[:route]),
38+
r = {
39+
route: ManifestRoute.parse(route[:route], route[:options]),
3740
protocol: route[:protocol]
3841
}
42+
r[:options] = route[:options] unless route[:options].nil?
43+
r
3944
end
4045
end
4146

4247
private
4348

49+
def route_options_are_valid
50+
return if errors[:routes].present?
51+
52+
routes.any? do |r|
53+
next unless r.keys.include?(:options)
54+
55+
unless r[:options].is_a?(Hash)
56+
errors.add(:base, message: "Route '#{r[:route]}': options must be an object")
57+
next
58+
end
59+
60+
r[:options].each_key do |key|
61+
RouteOptionsMessage::VALID_MANIFEST_ROUTE_OPTIONS.exclude?(key) &&
62+
errors.add(:base,
63+
message: "Route '#{r[:route]}' contains invalid route option '#{key}'. \
64+
Valid keys: '#{RouteOptionsMessage::VALID_MANIFEST_ROUTE_OPTIONS.join(', ')}'")
65+
end
66+
end
67+
end
68+
69+
def loadbalancings_are_valid
70+
return if errors[:routes].present?
71+
72+
routes.each do |r|
73+
next unless r.keys.include?(:options) && r[:options].is_a?(Hash) && r[:options].keys.include?(:loadbalancing)
74+
75+
loadbalancing = r[:options][:loadbalancing]
76+
unless loadbalancing.is_a?(String)
77+
errors.add(:base,
78+
message: "Invalid value for 'loadbalancing' for Route '#{r[:route]}'; \
79+
Valid values are: '#{RouteOptionsMessage::VALID_LOADBALANCING_ALGORITHMS.join(', ')}'")
80+
next
81+
end
82+
RouteOptionsMessage::VALID_LOADBALANCING_ALGORITHMS.exclude?(loadbalancing) &&
83+
errors.add(:base,
84+
message: "Cannot use loadbalancing value '#{loadbalancing}' for Route '#{r[:route]}'; \
85+
Valid values are: '#{RouteOptionsMessage::VALID_LOADBALANCING_ALGORITHMS.join(', ')}'")
86+
end
87+
end
88+
4489
def routes_are_uris
4590
return if errors[:routes].present?
4691

app/messages/route_create_message.rb

Lines changed: 11 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,8 +11,13 @@ class RouteCreateMessage < MetadataBaseMessage
1011
path
1112
port
1213
relationships
14+
options
1315
]
1416

17+
def self.options_requested?
18+
@options_requested ||= proc { |a| a.requested?(:options) }
19+
end
20+
1521
validates :host,
1622
allow_nil: true,
1723
string: true,
@@ -56,6 +62,7 @@ class RouteCreateMessage < MetadataBaseMessage
5662

5763
validates_with NoAdditionalKeysValidator
5864
validates_with RelationshipValidator
65+
validates_with OptionsValidator, if: options_requested?
5966

6067
delegate :space_guid, to: :relationships_message
6168
delegate :domain_guid, to: :relationships_message
@@ -65,6 +72,10 @@ def relationships_message
6572
@relationships_message ||= Relationships.new(relationships&.deep_symbolize_keys)
6673
end
6774

75+
def options_message
76+
@options_message ||= RouteOptionsMessage.new(options&.deep_symbolize_keys)
77+
end
78+
6879
def wildcard?
6980
host == '*'
7081
end

app/messages/route_options_message.rb

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].freeze
6+
VALID_ROUTE_OPTIONS = %i[loadbalancing].freeze
7+
VALID_LOADBALANCING_ALGORITHMS = %w[round-robin least-connections].freeze
8+
9+
register_allowed_keys VALID_ROUTE_OPTIONS
10+
validates_with NoAdditionalKeysValidator
11+
validates :loadbalancing,
12+
inclusion: { in: VALID_LOADBALANCING_ALGORITHMS, message: "must be one of '#{RouteOptionsMessage::VALID_LOADBALANCING_ALGORITHMS.join(', ')}' if present" },
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: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,23 @@ def validate(record)
240240
end
241241
end
242242

243+
class OptionsValidator < ActiveModel::Validator
244+
def validate(record)
245+
unless record.options.is_a?(Hash)
246+
record.errors.add(:options, message: "'options' is not a valid object")
247+
return
248+
end
249+
250+
opt = record.options_message
251+
252+
return if opt.valid?
253+
254+
opt.errors.full_messages.each do |message|
255+
record.errors.add(:options, message:)
256+
end
257+
end
258+
end
259+
243260
class ToOneRelationshipValidator < ActiveModel::EachValidator
244261
def validate_each(record, attribute, relationship)
245262
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] = route_mapping.route.options[:loadbalancing] if route_mapping.route.options[:loadbalancing]
16+
end
17+
18+
route_hash
1219
end
1320

1421
{ routes: alphabetize(route_hashes).presence }

0 commit comments

Comments
 (0)