Skip to content

Commit e770c0f

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 - update API docs
1 parent d825336 commit e770c0f

15 files changed

+344
-15
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 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+
return if flags.empty?
18+
19+
check_invalid_combination!(app, flags)
20+
app.update(flags)
21+
end
22+
23+
private
24+
25+
def database_column(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[database_column(AppFeatures::FILE_BASED_VCAP_SERVICES_FEATURE)].present? || app.file_based_vcap_services_enabled
34+
service_binding_k8s_enabled = flags[database_column(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/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

docs/v3/source/includes/resources/manifests/_apply.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ Location: https://api.example.org/v3/jobs/[guid]
2424
Apply changes specified in a manifest to the named apps and their underlying
2525
processes. The apps must reside in the space. These changes are additive
2626
and will not modify any unspecified properties or remove any existing
27-
environment variables, routes, or services.
27+
environment variables, app features, routes, or services.
2828

2929
<aside class="notice">
3030
Apply manifest will only trigger an immediate update for the "instances" property or routing changes. All other properties require an app restart to take effect.

docs/v3/source/includes/resources/manifests/_get.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ Content-Type: application/x-yaml
2222
applications:
2323
- name: my-app
2424
stack: cflinuxfs4
25+
features:
26+
ssh: true
27+
revisions: true
28+
service-binding-k8s: false
29+
file-based-vcap-services: false
2530
services:
2631
- my-service
2732
routes:

docs/v3/source/includes/resources/manifests/_object.md.erb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ applications:
1919
env:
2020
VAR1: value1
2121
VAR2: value2
22+
features:
23+
ssh: true
24+
revisions: true
25+
service-binding-k8s: false
26+
file-based-vcap-services: false
2227
routes:
2328
- route: route.example.com
2429
- route: another-route.example.com
@@ -94,6 +99,7 @@ Name | Type | Description
9499
**buildpacks** | _list of strings_ | a) An empty array, which will automatically select the appropriate default buildpack according to the coding language (incompatible with **lifecycle: cnb**) <br>b) An array of one or more URLs pointing to buildpacks <br>c) An array of one or more installed buildpack names <br>Replaces the legacy `buildpack` field
95100
**docker** | _object_ | If present, the created app will have Docker lifecycle type; the value of this key is ignored by the API but may be used by clients to source the registry address of the image and credentials, if needed; the [generate manifest endpoint](#generate-a-manifest-for-an-app) will return the registry address of the image and username provided with this key
96101
**env** | _object_ | A key-value mapping of environment variables to be used for the app when running
102+
**features** | _object_ | A key-value mapping of feature names to booleans (true = enabled, false = disabled)
97103
**no-route** | _boolean_ | When set to `true`, any routes specified with the `routes` attribute will be ignored and any existing routes will be removed
98104
**processes** | _array of [process configurations](#space-manifest-process-level-configuration)_ | List of configurations for individual process types
99105
**random-route** | _boolean_ | Creates a random route for the app if `true`; if `routes` is specified, if the app already has routes, or if `no-route` is specified, this field is ignored regardless of its value

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

0 commit comments

Comments
 (0)