Skip to content

Commit 4fe3f45

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 4fe3f45

12 files changed

+325
-14
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: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,34 @@
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 InvalidCombination < StandardError; end
4+
5+
class << self
6+
def update(feature_name, app, app_feature_update_message)
7+
app.update({ database_column(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[database_column(feature_name)] = enabled
15+
end
16+
17+
app.update(flags) unless flags.empty?
18+
rescue Sequel::CheckConstraintViolation => e
19+
raise e unless e.message.include?('only_one_sb_feature_enabled')
20+
21+
msg = "'#{AppFeatures::FILE_BASED_VCAP_SERVICES_FEATURE}' and '#{AppFeatures::SERVICE_BINDING_K8S_FEATURE}' features cannot be enabled at the same time."
22+
raise InvalidCombination.new(msg)
23+
end
24+
25+
private
26+
27+
def database_column(feature_name)
28+
column = AppFeatures::DATABASE_COLUMNS_MAPPING[feature_name.to_s]
29+
raise "Unknown feature name: #{feature_name}" if column.nil?
30+
31+
column
1332
end
1433
end
1534
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/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

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: 96 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,98 @@ 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+
125+
context 'when conflicting features are specified' do
126+
before do
127+
features[:'service-binding-k8s'] = true
128+
features[:'file-based-vcap-services'] = true
129+
end
130+
131+
it 'raises an error' do
132+
expect do
133+
AppFeatureUpdate.bulk_update(app, message)
134+
end.to raise_error(AppFeatureUpdate::InvalidCombination, /'file-based-vcap-services' and 'service-binding-k8s' features cannot be enabled at the same time/)
135+
end
136+
end
137+
end
44138
end
45139
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/jobs/space_apply_manifest_action_job_spec.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,8 @@ module Jobs
5858
AppApplyManifest::ServiceBindingError,
5959
SidecarCreate::InvalidSidecar,
6060
SidecarUpdate::InvalidSidecar,
61-
ProcessScale::SidecarMemoryLessThanProcessMemory
61+
ProcessScale::SidecarMemoryLessThanProcessMemory,
62+
AppFeatureUpdate::InvalidCombination
6263
].each do |klass|
6364
it "wraps a #{klass} in an ApiError" do
6465
allow(apply_manifest_action).to receive(:apply).

0 commit comments

Comments
 (0)