Skip to content

Commit 48603fa

Browse files
committed
Update features from manifest
- add bulk_update method to AppFeatureUpdate - add features to manifest message - call AppFeatureUpdate from AppApplyManifest - add features to SpaceDiffManifest
1 parent d825336 commit 48603fa

10 files changed

+301
-12
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: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,27 @@
11
module VCAP::CloudController
22
class AppFeatureUpdate
3-
def self.update(feature_name, app, message)
4-
case feature_name
5-
when AppFeatures::SSH_FEATURE
6-
app.update(enable_ssh: message.enabled)
7-
when AppFeatures::REVISIONS_FEATURE
8-
app.update(revisions_enabled: message.enabled)
9-
when AppFeatures::SERVICE_BINDING_K8S_FEATURE
10-
app.update(service_binding_k8s_enabled: message.enabled)
11-
when AppFeatures::FILE_BASED_VCAP_SERVICES_FEATURE
12-
app.update(file_based_vcap_services_enabled: message.enabled)
3+
class << self
4+
def update(feature_name, app, app_feature_update_message)
5+
app.update({ database_column(feature_name) => app_feature_update_message.enabled })
6+
end
7+
8+
def bulk_update(app, manifest_features_update_message)
9+
flags = {}
10+
11+
manifest_features_update_message.features&.each do |feature_name, enabled|
12+
flags[database_column(feature_name)] = enabled
13+
end
14+
15+
app.update(flags) unless flags.empty?
16+
end
17+
18+
private
19+
20+
def database_column(feature_name)
21+
column = AppFeatures::DATABASE_COLUMNS_MAPPING[feature_name.to_s]
22+
raise "Unknown feature name: #{feature_name}" if column.nil?
23+
24+
column
1325
end
1426
end
1527
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/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

spec/unit/actions/app_apply_manifest_spec.rb

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ module VCAP::CloudController
1414
let(:process_create) { instance_double(ProcessCreate) }
1515
let(:service_cred_binding_create) { instance_double(V3::ServiceCredentialBindingAppCreate) }
1616
let(:random_route_generator) { instance_double(RandomRouteGenerator, route: 'spiffy/donut') }
17+
let(:app_feature_update) { instance_double(AppFeatureUpdate) }
1718

1819
describe '#apply' do
1920
before do
@@ -52,6 +53,8 @@ module VCAP::CloudController
5253
allow(AppPatchEnvironmentVariables).
5354
to receive(:new).and_return(app_patch_env)
5455
allow(app_patch_env).to receive(:patch)
56+
57+
allow(AppFeatureUpdate).to receive(:bulk_update)
5558
end
5659

5760
describe 'scaling a process' do
@@ -1387,6 +1390,23 @@ module VCAP::CloudController
13871390
end.to raise_error(CloudController::Errors::NotFound, "App with guid '#{app_guid}' not found")
13881391
end
13891392
end
1393+
1394+
describe 'updating app features' do
1395+
let(:message) { AppManifestMessage.create_from_yml({ features: { ssh: true } }) }
1396+
let(:manifest_features_update_message) { message.manifest_features_update_message }
1397+
let(:app) { AppModel.make }
1398+
1399+
it 'returns the app' do
1400+
expect(
1401+
app_apply_manifest.apply(app.guid, message)
1402+
).to eq(app)
1403+
end
1404+
1405+
it 'calls AppFeatureUpdate.bulk_update with the correct arguments' do
1406+
app_apply_manifest.apply(app.guid, message)
1407+
expect(AppFeatureUpdate).to have_received(:bulk_update).with(app, manifest_features_update_message)
1408+
end
1409+
end
13901410
end
13911411
end
13921412

spec/unit/actions/app_feature_update_spec.rb

Lines changed: 83 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@
55
module VCAP::CloudController
66
RSpec.describe AppFeatureUpdate do
77
subject(:app_feature_update) { AppFeatureUpdate }
8-
let(:app) { AppModel.make(enable_ssh: false, revisions_enabled: false) }
9-
let(:message) { AppFeatureUpdateMessage.new(enabled: true) }
8+
let(:app) { AppModel.make(enable_ssh: false, revisions_enabled: false, service_binding_k8s_enabled: false, file_based_vcap_services_enabled: false) }
109

1110
describe '.update' do
11+
let(:message) { AppFeatureUpdateMessage.new(enabled: true) }
12+
1213
context 'when the feature name is ssh' do
1314
it 'updates the enable_ssh column on the app' do
1415
expect do
@@ -41,5 +42,85 @@ module VCAP::CloudController
4142
end
4243
end
4344
end
45+
46+
describe '.bulk_update' do
47+
let(:features) do
48+
{
49+
ssh: false,
50+
revisions: false,
51+
'service-binding-k8s': false,
52+
'file-based-vcap-services': false
53+
}
54+
end
55+
let(:message) { ManifestFeaturesUpdateMessage.new(features:) }
56+
57+
context 'when the ssh feature is specified' do
58+
before do
59+
features[:ssh] = true
60+
end
61+
62+
it 'updates the enable_ssh column on the app' do
63+
expect do
64+
AppFeatureUpdate.bulk_update(app, message)
65+
end.to change { app.reload.enable_ssh }.to(true)
66+
end
67+
end
68+
69+
context 'when the revisions feature is specified' do
70+
before do
71+
features[:revisions] = true
72+
end
73+
74+
it 'updates the revisions_enabled column on the app' do
75+
expect do
76+
AppFeatureUpdate.bulk_update(app, message)
77+
end.to change { app.reload.revisions_enabled }.to(true)
78+
end
79+
end
80+
81+
context 'when the service-binding-k8s feature is specified' do
82+
before do
83+
features[:'service-binding-k8s'] = true
84+
end
85+
86+
it 'updates the service_binding_k8s_enabled column on the app' do
87+
expect do
88+
AppFeatureUpdate.bulk_update(app, message)
89+
end.to change { app.reload.service_binding_k8s_enabled }.to(true)
90+
end
91+
end
92+
93+
context 'when the file-based-vcap-services feature is specified' do
94+
before do
95+
features[:'file-based-vcap-services'] = true
96+
end
97+
98+
it 'updates the file_based_vcap_services_enabled column on the app' do
99+
expect do
100+
AppFeatureUpdate.bulk_update(app, message)
101+
end.to change { app.reload.file_based_vcap_services_enabled }.to(true)
102+
end
103+
end
104+
105+
context 'when multiple features are specified' do
106+
before do
107+
features[:ssh] = true
108+
features[:'service-binding-k8s'] = true
109+
end
110+
111+
it 'updates the corresponding columns in a single database call' do
112+
expect(app.enable_ssh).to be(false)
113+
expect(app.service_binding_k8s_enabled).to be(false)
114+
115+
expect do
116+
AppFeatureUpdate.bulk_update(app, message)
117+
end.to have_queried_db_times(/update .apps./i, 1)
118+
119+
app.reload
120+
expect(app.enable_ssh).to be(true)
121+
expect(app.service_binding_k8s_enabled).to be(true)
122+
end
123+
end
124+
end
44125
end
45126
end

spec/unit/actions/space_diff_manifest_spec.rb

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -503,6 +503,28 @@ module VCAP::CloudController
503503
end
504504
end
505505
end
506+
507+
context 'when the given manifest contains features' do
508+
before do
509+
default_manifest['applications'][0]['features'] = {
510+
'ssh' => true, # unchanged
511+
'service-binding-k8s' => true # changed
512+
}
513+
end
514+
515+
it 'does not show unchanged features' do
516+
expect(subject).not_to include(hash_including('path' => '/applications/0/features/ssh'))
517+
end
518+
519+
it 'shows changed features' do
520+
expect(subject).to include({ 'op' => 'replace', 'path' => '/applications/0/features/service-binding-k8s', 'value' => true, 'was' => false })
521+
end
522+
523+
it 'does not show unspecified features' do
524+
expect(subject).not_to include(hash_including('path' => '/applications/0/features/revisions'))
525+
expect(subject).not_to include(hash_including('path' => '/applications/0/features/file-based-vcap-services'))
526+
end
527+
end
506528
end
507529
end
508530
end

spec/unit/messages/app_manifest_message_spec.rb

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2200,5 +2200,43 @@ module VCAP::CloudController
22002200
end
22012201
end
22022202
end
2203+
2204+
describe '#manifest_features_update_message' do
2205+
context 'when no features are specified' do
2206+
let(:parsed_yaml) do
2207+
{ name: 'app' }
2208+
end
2209+
2210+
it 'does not set the features in the message' do
2211+
message = AppManifestMessage.create_from_yml(parsed_yaml)
2212+
expect(message).to be_valid
2213+
expect(message.manifest_features_update_message).not_to be_requested(:features)
2214+
end
2215+
end
2216+
2217+
context 'when features are specified' do
2218+
let(:parsed_yaml) do
2219+
{ name: 'app', features: { ssh: true, 'service-binding-k8s': false } }
2220+
end
2221+
2222+
it 'returns a ManifestFeaturesUpdateMessage containing the features' do
2223+
message = AppManifestMessage.create_from_yml(parsed_yaml)
2224+
expect(message).to be_valid
2225+
expect(message.manifest_features_update_message.features).to eq({ ssh: true, 'service-binding-k8s': false })
2226+
end
2227+
end
2228+
2229+
context 'when an invalid feature is specified' do
2230+
let(:parsed_yaml) do
2231+
{ features: { invalid_feature: true } }
2232+
end
2233+
2234+
it 'is invalid and contains the correct error message' do
2235+
message = AppManifestMessage.create_from_yml(parsed_yaml)
2236+
expect(message).not_to be_valid
2237+
expect(message.errors[:base]).to include('Features must be a map of valid feature names to booleans (true = enabled, false = disabled)')
2238+
end
2239+
end
2240+
end
22032241
end
22042242
end

0 commit comments

Comments
 (0)