Skip to content

Commit d04c8c7

Browse files
authored
Add feature flags to v1 app manifests (#4358)
* Add feature flags to v1 app manifests (read-only) - introduce AppFeatures helper containing constants and the mapping to database columns - add FeaturesPresenter - wire into AppManifestPresenter - update request and unit tests for new 'features' hash * Update features from manifest - add bulk_update method to AppFeatureUpdate - add features to manifest message - call AppFeatureUpdate from AppApplyManifest - add features to SpaceDiffManifest - update API docs
1 parent d64bbd3 commit d04c8c7

26 files changed

+444
-39
lines changed

app/actions/app_apply_manifest.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ def apply(app_guid, message)
2828
app = AppModel.first(guid: app_guid)
2929
app_instance_not_found!(app_guid) unless app
3030

31+
AppFeatureUpdate.bulk_update(app, message.manifest_features_update_message)
32+
3133
message.manifest_process_update_messages.each do |manifest_process_update_msg|
3234
if manifest_process_update_msg.requested_keys == [:type] && manifest_process_update_msg.type == 'web'
3335
# ignore trivial messages, most likely from manifest

app/actions/app_feature_update.rb

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,41 @@
11
module VCAP::CloudController
22
class AppFeatureUpdate
3-
def self.update(feature_name, app, message)
4-
case feature_name
5-
when AppFeaturesController::SSH_FEATURE
6-
app.update(enable_ssh: message.enabled)
7-
when AppFeaturesController::REVISIONS_FEATURE
8-
app.update(revisions_enabled: message.enabled)
9-
when AppFeaturesController::SERVICE_BINDING_K8S_FEATURE
10-
app.update(service_binding_k8s_enabled: message.enabled)
11-
when AppFeaturesController::FILE_BASED_VCAP_SERVICES_FEATURE
12-
app.update(file_based_vcap_services_enabled: message.enabled)
3+
class InvalidCombination < StandardError; end
4+
5+
class << self
6+
def update(feature_name, app, app_feature_update_message)
7+
app.update({ feature_column_name(feature_name) => app_feature_update_message.enabled })
8+
end
9+
10+
def bulk_update(app, manifest_features_update_message)
11+
flags = {}
12+
13+
manifest_features_update_message.features&.each do |feature_name, enabled|
14+
flags[feature_column_name(feature_name)] = enabled
15+
end
16+
17+
return if flags.empty?
18+
19+
check_invalid_combination!(app, flags)
20+
app.update(flags)
21+
end
22+
23+
private
24+
25+
def feature_column_name(feature_name)
26+
column = AppFeatures::DATABASE_COLUMNS_MAPPING[feature_name.to_s]
27+
raise "Unknown feature name: #{feature_name}" if column.nil?
28+
29+
column
30+
end
31+
32+
def check_invalid_combination!(app, flags)
33+
file_based_vcap_services_enabled = flags[feature_column_name(AppFeatures::FILE_BASED_VCAP_SERVICES_FEATURE)].present? || app.file_based_vcap_services_enabled
34+
service_binding_k8s_enabled = flags[feature_column_name(AppFeatures::SERVICE_BINDING_K8S_FEATURE)].present? || app.service_binding_k8s_enabled
35+
return unless file_based_vcap_services_enabled && service_binding_k8s_enabled
36+
37+
msg = "'#{AppFeatures::FILE_BASED_VCAP_SERVICES_FEATURE}' and '#{AppFeatures::SERVICE_BINDING_K8S_FEATURE}' features cannot be enabled at the same time."
38+
raise InvalidCombination.new(msg)
1339
end
1440
end
1541
end

app/actions/space_diff_manifest.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ def generate_diff(app_manifests, space)
5151

5252
remove_default_missing_fields(existing_value, key, value) if nested_attribute_exists
5353

54+
remove_unspecified_app_features(existing_value, key, value)
55+
5456
# To preserve backwards compability, we've decided to skip diffs that satisfy this conditon
5557
next if !nested_attribute_exists && %w[disk_quota disk-quota memory].include?(key)
5658

@@ -211,6 +213,11 @@ def remove_default_missing_fields(existing_value, current_key, value)
211213
end
212214
end
213215
end
216+
217+
def remove_unspecified_app_features(existing_value, key, value)
218+
# Only show the diff for features that are specified in the manifest
219+
existing_value.slice!(*value.keys) if key == 'features'
220+
end
214221
end
215222
end
216223
end

app/controllers/v3/app_features_controller.rb

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,14 @@
66
require 'presenters/v3/app_file_based_vcap_services_feature_presenter'
77
require 'presenters/v3/app_ssh_status_presenter'
88
require 'actions/app_feature_update'
9+
require 'models/helpers/app_features'
910

1011
class AppFeaturesController < ApplicationController
1112
include AppSubResource
1213

13-
SSH_FEATURE = 'ssh'.freeze
14-
REVISIONS_FEATURE = 'revisions'.freeze
15-
SERVICE_BINDING_K8S_FEATURE = 'service-binding-k8s'.freeze
16-
FILE_BASED_VCAP_SERVICES_FEATURE = 'file-based-vcap-services'.freeze
17-
18-
TRUSTED_APP_FEATURES = [SSH_FEATURE, SERVICE_BINDING_K8S_FEATURE, FILE_BASED_VCAP_SERVICES_FEATURE].freeze
19-
UNTRUSTED_APP_FEATURES = [REVISIONS_FEATURE].freeze
14+
TRUSTED_APP_FEATURES = [VCAP::CloudController::AppFeatures::SSH_FEATURE, VCAP::CloudController::AppFeatures::SERVICE_BINDING_K8S_FEATURE,
15+
VCAP::CloudController::AppFeatures::FILE_BASED_VCAP_SERVICES_FEATURE].freeze
16+
UNTRUSTED_APP_FEATURES = [VCAP::CloudController::AppFeatures::REVISIONS_FEATURE].freeze
2017
APP_FEATURES = (TRUSTED_APP_FEATURES + UNTRUSTED_APP_FEATURES).freeze
2118

2219
def index
@@ -56,11 +53,12 @@ def update
5653
unprocessable!(message.errors.full_messages) unless message.valid?
5754

5855
if message.enabled && both_service_binding_features_enabled?(app, name)
59-
unprocessable!("'#{FILE_BASED_VCAP_SERVICES_FEATURE}' and '#{SERVICE_BINDING_K8S_FEATURE}' features cannot be enabled at the same time.")
56+
unprocessable!("'#{VCAP::CloudController::AppFeatures::FILE_BASED_VCAP_SERVICES_FEATURE}' and '#{VCAP::CloudController::AppFeatures::SERVICE_BINDING_K8S_FEATURE}' " \
57+
'features cannot be enabled at the same time.')
6058
end
6159

62-
AppFeatureUpdate.update(hashed_params[:name], app, message)
63-
render status: :ok, json: feature_presenter_for(hashed_params[:name], app)
60+
AppFeatureUpdate.update(name, app, message)
61+
render status: :ok, json: feature_presenter_for(name, app)
6462
end
6563

6664
def ssh_enabled
@@ -87,10 +85,10 @@ def present_unpagination_hash(result, path)
8785

8886
def feature_presenter_for(feature_name, app)
8987
presenters = {
90-
SSH_FEATURE => Presenters::V3::AppSshFeaturePresenter,
91-
REVISIONS_FEATURE => Presenters::V3::AppRevisionsFeaturePresenter,
92-
SERVICE_BINDING_K8S_FEATURE => Presenters::V3::AppServiceBindingK8sFeaturePresenter,
93-
FILE_BASED_VCAP_SERVICES_FEATURE => Presenters::V3::AppFileBasedVcapServicesFeaturePresenter
88+
VCAP::CloudController::AppFeatures::SSH_FEATURE => Presenters::V3::AppSshFeaturePresenter,
89+
VCAP::CloudController::AppFeatures::REVISIONS_FEATURE => Presenters::V3::AppRevisionsFeaturePresenter,
90+
VCAP::CloudController::AppFeatures::SERVICE_BINDING_K8S_FEATURE => Presenters::V3::AppServiceBindingK8sFeaturePresenter,
91+
VCAP::CloudController::AppFeatures::FILE_BASED_VCAP_SERVICES_FEATURE => Presenters::V3::AppFileBasedVcapServicesFeaturePresenter
9492
}
9593
presenters[feature_name].new(app)
9694
end
@@ -105,9 +103,9 @@ def presented_app_features(app)
105103
end
106104

107105
def both_service_binding_features_enabled?(app, feature_name)
108-
if feature_name == FILE_BASED_VCAP_SERVICES_FEATURE
106+
if feature_name == VCAP::CloudController::AppFeatures::FILE_BASED_VCAP_SERVICES_FEATURE
109107
app.service_binding_k8s_enabled
110-
elsif feature_name == SERVICE_BINDING_K8S_FEATURE
108+
elsif feature_name == VCAP::CloudController::AppFeatures::SERVICE_BINDING_K8S_FEATURE
111109
app.file_based_vcap_services_enabled
112110
end
113111
end

app/jobs/space_apply_manifest_action_job.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ def perform
2626
AppApplyManifest::ServiceBindingError,
2727
SidecarCreate::InvalidSidecar,
2828
SidecarUpdate::InvalidSidecar,
29-
ProcessScale::SidecarMemoryLessThanProcessMemory => e
29+
ProcessScale::SidecarMemoryLessThanProcessMemory,
30+
AppFeatureUpdate::InvalidCombination => e
3031

3132
app_name = AppModel.find(guid: app_guid)&.name
3233
error_message = app_name ? "For application '#{app_name}': #{e.message}" : e.message

app/messages/app_manifest_message.rb

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
require 'messages/manifest_buildpack_message'
55
require 'messages/manifest_service_binding_create_message'
66
require 'messages/manifest_routes_update_message'
7+
require 'messages/manifest_features_update_message'
78
require 'messages/validators/metadata_validator'
89
require 'cloud_controller/app_manifest/byte_converter'
910
require 'models/helpers/health_check_types'
@@ -43,6 +44,7 @@ class AppManifestMessage < BaseMessage
4344
stack
4445
timeout
4546
cnb_credentials
47+
features
4648
]
4749

4850
HEALTH_CHECK_TYPE_MAPPING = { HealthCheckTypes::NONE => HealthCheckTypes::PROCESS }.freeze
@@ -82,6 +84,7 @@ def self.underscore_keys(hash)
8284
record.requested?(:random_route) ||
8385
record.requested?(:default_route)
8486
}
87+
validate :validate_features!, if: ->(record) { record.requested?(:features) }
8588

8689
def initialize(original_yaml, attrs={})
8790
super(attrs)
@@ -116,6 +119,10 @@ def manifest_routes_update_message
116119
@manifest_routes_update_message ||= ManifestRoutesUpdateMessage.new(routes_attribute_mapping)
117120
end
118121

122+
def manifest_features_update_message
123+
@manifest_features_update_message ||= ManifestFeaturesUpdateMessage.new(features_attribute_mapping)
124+
end
125+
119126
def audit_hash
120127
override_env = original_yaml['env'] ? { 'env' => Presenters::Censorship::PRIVATE_DATA_HIDDEN } : {}
121128
override_cnb = original_yaml['cnb-credentials'] ? { 'cnb-credentials' => Presenters::Censorship::PRIVATE_DATA_HIDDEN } : {}
@@ -295,6 +302,12 @@ def service_bindings_attribute_mapping
295302
mapping
296303
end
297304

305+
def features_attribute_mapping
306+
mapping = {}
307+
mapping[:features] = features if requested?(:features)
308+
mapping
309+
end
310+
298311
def should_autodetect?(buildpack)
299312
buildpack == 'default' || buildpack == 'null' || buildpack.nil?
300313
end
@@ -464,6 +477,13 @@ def validate_docker_buildpacks_combination!
464477
errors.add(:base, 'Cannot specify both buildpack(s) and docker keys')
465478
end
466479

480+
def validate_features!
481+
manifest_features_update_message.valid?
482+
manifest_features_update_message.errors.full_messages.each do |error_message|
483+
errors.add(:base, error_message)
484+
end
485+
end
486+
467487
def add_process_error!(error_message, type)
468488
errors.add(:base, %(Process "#{type}": #{error_message}))
469489
end
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
require 'messages/base_message'
2+
3+
module VCAP::CloudController
4+
class ManifestFeaturesUpdateMessage < BaseMessage
5+
register_allowed_keys [:features]
6+
7+
validates_with NoAdditionalKeysValidator
8+
9+
validate :features do
10+
errors.add(:features, 'must be a map of valid feature names to booleans (true = enabled, false = disabled)') unless is_valid(features)
11+
end
12+
13+
private
14+
15+
def is_valid(features)
16+
return false unless features.is_a?(Hash) && features.any?
17+
18+
features.all? { |feature, enabled| valid_feature?(feature) && [true, false].include?(enabled) }
19+
end
20+
21+
def valid_feature?(feature)
22+
AppFeatures.all_features.include?(feature.to_s)
23+
end
24+
end
25+
end

app/models/helpers/app_features.rb

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
module VCAP::CloudController
2+
class AppFeatures
3+
SSH_FEATURE = 'ssh'.freeze
4+
REVISIONS_FEATURE = 'revisions'.freeze
5+
SERVICE_BINDING_K8S_FEATURE = 'service-binding-k8s'.freeze
6+
FILE_BASED_VCAP_SERVICES_FEATURE = 'file-based-vcap-services'.freeze
7+
8+
DATABASE_COLUMNS_MAPPING = {
9+
SSH_FEATURE => :enable_ssh,
10+
REVISIONS_FEATURE => :revisions_enabled,
11+
SERVICE_BINDING_K8S_FEATURE => :service_binding_k8s_enabled,
12+
FILE_BASED_VCAP_SERVICES_FEATURE => :file_based_vcap_services_enabled
13+
}.freeze
14+
15+
def self.all_features
16+
[SSH_FEATURE, REVISIONS_FEATURE, SERVICE_BINDING_K8S_FEATURE, FILE_BASED_VCAP_SERVICES_FEATURE]
17+
end
18+
end
19+
end

app/presenters/v3/app_file_based_vcap_services_feature_presenter.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ module VCAP::CloudController::Presenters::V3
44
class AppFileBasedVcapServicesFeaturePresenter < BasePresenter
55
def to_hash
66
{
7-
name: AppFeaturesController::FILE_BASED_VCAP_SERVICES_FEATURE,
7+
name: VCAP::CloudController::AppFeatures::FILE_BASED_VCAP_SERVICES_FEATURE,
88
description: 'Enable file-based VCAP service bindings for the app',
99
enabled: app.file_based_vcap_services_enabled
1010
}

app/presenters/v3/app_manifest_presenter.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
require 'presenters/v3/app_manifest_presenters/process_properties_presenter'
88
require 'presenters/v3/app_manifest_presenters/sidecar_properties_presenter'
99
require 'presenters/v3/app_manifest_presenters/lifecycle_presenter'
10+
require 'presenters/v3/app_manifest_presenters/features_presenter'
1011

1112
module VCAP::CloudController
1213
module Presenters
@@ -17,6 +18,7 @@ class AppManifestPresenter
1718
AppManifestPresenters::LifecyclePresenter.new,
1819
AppManifestPresenters::DockerPresenter.new,
1920
AppManifestPresenters::BuildpackPresenter.new,
21+
AppManifestPresenters::FeaturesPresenter.new,
2022
AppManifestPresenters::ServicesPropertiesPresenter.new,
2123
AppManifestPresenters::RoutePropertiesPresenter.new,
2224
AppManifestPresenters::MetadataPresenter.new,

0 commit comments

Comments
 (0)