Skip to content

Commit 15421b3

Browse files
committed
Introduce 'file-based-vcap-services' app feature
1 parent 4c308e2 commit 15421b3

File tree

10 files changed

+256
-29
lines changed

10 files changed

+256
-29
lines changed

app/actions/app_feature_update.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ def self.update(feature_name, app, message)
88
app.update(revisions_enabled: message.enabled)
99
when 'service-binding-k8s'
1010
app.update(service_binding_k8s_enabled: message.enabled)
11+
when 'file-based-vcap-services'
12+
app.update(file_based_vcap_services_enabled: message.enabled)
1113
end
1214
end
1315
end

app/controllers/v3/app_features_controller.rb

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
require 'presenters/v3/app_ssh_feature_presenter'
44
require 'presenters/v3/app_revisions_feature_presenter'
55
require 'presenters/v3/app_service_binding_k8s_feature_presenter'
6+
require 'presenters/v3/app_file_based_vcap_services_feature_presenter'
67
require 'presenters/v3/app_ssh_status_presenter'
78
require 'actions/app_feature_update'
89

@@ -12,8 +13,9 @@ class AppFeaturesController < ApplicationController
1213
SSH_FEATURE = 'ssh'.freeze
1314
REVISIONS_FEATURE = 'revisions'.freeze
1415
SERVICE_BINDING_K8S_FEATURE = 'service-binding-k8s'.freeze
16+
FILE_BASED_VCAP_SERVICES_FEATURE = 'file-based-vcap-services'.freeze
1517

16-
TRUSTED_APP_FEATURES = [SSH_FEATURE, SERVICE_BINDING_K8S_FEATURE].freeze
18+
TRUSTED_APP_FEATURES = [SSH_FEATURE, SERVICE_BINDING_K8S_FEATURE, FILE_BASED_VCAP_SERVICES_FEATURE].freeze
1719
UNTRUSTED_APP_FEATURES = [REVISIONS_FEATURE].freeze
1820
APP_FEATURES = (TRUSTED_APP_FEATURES + UNTRUSTED_APP_FEATURES).freeze
1921

@@ -53,6 +55,10 @@ def update
5355
message = VCAP::CloudController::AppFeatureUpdateMessage.new(hashed_params['body'])
5456
unprocessable!(message.errors.full_messages) unless message.valid?
5557

58+
if message.enabled && both_service_binding_features_enabled?(app, name)
59+
unprocessable!("'file-based-vcap-services' and 'service-binding-k8s' features cannot be enabled at the same time.")
60+
end
61+
5662
AppFeatureUpdate.update(hashed_params[:name], app, message)
5763
render status: :ok, json: feature_presenter_for(hashed_params[:name], app)
5864
end
@@ -83,7 +89,8 @@ def feature_presenter_for(feature_name, app)
8389
presenters = {
8490
SSH_FEATURE => Presenters::V3::AppSshFeaturePresenter,
8591
REVISIONS_FEATURE => Presenters::V3::AppRevisionsFeaturePresenter,
86-
SERVICE_BINDING_K8S_FEATURE => Presenters::V3::AppServiceBindingK8sFeaturePresenter
92+
SERVICE_BINDING_K8S_FEATURE => Presenters::V3::AppServiceBindingK8sFeaturePresenter,
93+
FILE_BASED_VCAP_SERVICES_FEATURE => Presenters::V3::AppFileBasedVcapServicesFeaturePresenter
8794
}
8895
presenters[feature_name].new(app)
8996
end
@@ -92,7 +99,16 @@ def presented_app_features(app)
9299
[
93100
Presenters::V3::AppSshFeaturePresenter.new(app),
94101
Presenters::V3::AppRevisionsFeaturePresenter.new(app),
95-
Presenters::V3::AppServiceBindingK8sFeaturePresenter.new(app)
102+
Presenters::V3::AppServiceBindingK8sFeaturePresenter.new(app),
103+
Presenters::V3::AppFileBasedVcapServicesFeaturePresenter.new(app)
96104
]
97105
end
106+
107+
def both_service_binding_features_enabled?(app, feature_name)
108+
if feature_name == 'file-based-vcap-services'
109+
app.service_binding_k8s_enabled
110+
elsif feature_name == 'service-binding-k8s'
111+
app.file_based_vcap_services_enabled
112+
end
113+
end
98114
end
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
require 'presenters/v3/base_presenter'
2+
3+
module VCAP::CloudController::Presenters::V3
4+
class AppFileBasedVcapServicesFeaturePresenter < BasePresenter
5+
def to_hash
6+
{
7+
name: 'file-based-vcap-services',
8+
description: 'Enable file-based VCAP service bindings for the app',
9+
enabled: app.file_based_vcap_services_enabled
10+
}
11+
end
12+
13+
private
14+
15+
def app
16+
@resource
17+
end
18+
end
19+
end
Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,19 @@
11
Sequel.migration do
2-
change do
3-
add_column :apps, :service_binding_k8s_enabled, :boolean, default: false, null: false
2+
up do
3+
alter_table :apps do
4+
add_column :service_binding_k8s_enabled, :boolean, default: false, null: false
5+
add_column :file_based_vcap_services_enabled, :boolean, default: false, null: false
6+
add_constraint(name: :only_one_sb_feature_enabled) do
7+
Sequel.lit('NOT (service_binding_k8s_enabled AND file_based_vcap_services_enabled)')
8+
end
9+
end
10+
end
11+
12+
down do
13+
alter_table :apps do
14+
drop_column :service_binding_k8s_enabled
15+
drop_column :file_based_vcap_services_enabled
16+
drop_constraint(name: :only_one_sb_feature_enabled)
17+
end
418
end
519
end

docs/v3/source/includes/api_resources/_app_features.erb

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,15 @@
2323
"name": "service-binding-k8s",
2424
"description": "Enable k8s service bindings for the app",
2525
"enabled": false
26+
},
27+
{
28+
"name": "file-based-vcap-services",
29+
"description": "Enable file-based VCAP service bindings for the app",
30+
"enabled": false
2631
}
2732
],
2833
"pagination": {
29-
"total_results": 3,
34+
"total_results": 4,
3035
"total_pages": 1,
3136
"first": { "href": "/v3/apps/05d39de4-2c9e-4c76-8fd6-10417da07e42/features" },
3237
"last": { "href": "/v3/apps/05d39de4-2c9e-4c76-8fd6-10417da07e42/features" },

docs/v3/source/includes/resources/app_features/_supported_features.md.erb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ Name | Description
77
**ssh** | Enable SSHing into the app
88
**revisions** | Enable [versioning](#revisions) of an application
99
**service-binding-k8s** | Enable k8s service bindings for the app (experimental)
10+
**file-based-vcap-services** | Enable file-based VCAP service bindings for the app (experimental)

spec/request/app_features_spec.rb

Lines changed: 160 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,17 @@
77
let(:admin_header) { admin_headers_for(user) }
88
let(:org) { VCAP::CloudController::Organization.make(created_at: 3.days.ago) }
99
let(:space) { VCAP::CloudController::Space.make(organization: org) }
10-
let(:app_model) { VCAP::CloudController::AppModel.make(space: space, enable_ssh: true, service_binding_k8s_enabled: true) }
10+
let(:service_binding_k8s_enabled) { true }
11+
let(:file_based_vcap_services_enabled) { false }
12+
let(:request_body_enabled) { { body: { enabled: true } } }
13+
let(:app_model) do
14+
VCAP::CloudController::AppModel.make(
15+
space: space,
16+
enable_ssh: true,
17+
service_binding_k8s_enabled: service_binding_k8s_enabled,
18+
file_based_vcap_services_enabled: file_based_vcap_services_enabled
19+
)
20+
end
1121

1222
describe 'GET /v3/apps/:guid/features' do
1323
context 'getting a list of available features for the app' do
@@ -29,11 +39,16 @@
2939
'name' => 'service-binding-k8s',
3040
'description' => 'Enable k8s service bindings for the app',
3141
'enabled' => true
42+
},
43+
{
44+
'name' => 'file-based-vcap-services',
45+
'description' => 'Enable file-based VCAP service bindings for the app',
46+
'enabled' => false
3247
}
3348
],
3449
'pagination' =>
3550
{
36-
'total_results' => 3,
51+
'total_results' => 4,
3752
'total_pages' => 1,
3853
'first' => { 'href' => "/v3/apps/#{app_model.guid}/features" },
3954
'last' => { 'href' => "/v3/apps/#{app_model.guid}/features" },
@@ -112,6 +127,19 @@
112127

113128
it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS
114129
end
130+
131+
context 'file-based-vcap-services app feature' do
132+
let(:api_call) { ->(user_headers) { get "/v3/apps/#{app_model.guid}/features/file-based-vcap-services", nil, user_headers } }
133+
let(:feature_response_object) do
134+
{
135+
'name' => 'file-based-vcap-services',
136+
'description' => 'Enable file-based VCAP service bindings for the app',
137+
'enabled' => false
138+
}
139+
end
140+
141+
it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS
142+
end
115143
end
116144

117145
describe 'PATCH /v3/apps/:guid/features/:name' do
@@ -192,36 +220,148 @@
192220
end
193221

194222
context 'service-binding-k8s app feature' do
195-
let(:api_call) { ->(user_headers) { patch "/v3/apps/#{app_model.guid}/features/service-binding-k8s", request_body.to_json, user_headers } }
196-
let(:feature_response_object) do
197-
{
198-
'name' => 'service-binding-k8s',
199-
'description' => 'Enable k8s service bindings for the app',
200-
'enabled' => false
201-
}
223+
context 'when feature is enabled' do
224+
let(:service_binding_k8s_enabled) { true }
225+
let(:api_call) { ->(user_headers) { patch "/v3/apps/#{app_model.guid}/features/service-binding-k8s", request_body.to_json, user_headers } }
226+
let(:feature_response_object) do
227+
{
228+
'name' => 'service-binding-k8s',
229+
'description' => 'Enable k8s service bindings for the app',
230+
'enabled' => false
231+
}
232+
end
233+
234+
let(:expected_codes_and_responses) do
235+
h = Hash.new({ code: 403, errors: CF_NOT_AUTHORIZED }.freeze)
236+
%w[no_role org_auditor org_billing_manager].each { |r| h[r] = { code: 404 } }
237+
%w[admin space_developer].each { |r| h[r] = { code: 200, response_object: feature_response_object } }
238+
h
239+
end
240+
241+
it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS
242+
243+
context 'when organization is suspended' do
244+
let(:expected_codes_and_responses) do
245+
h = super()
246+
h['space_developer'] = { code: 403, errors: CF_ORG_SUSPENDED }
247+
h
248+
end
249+
250+
before do
251+
org.update(status: VCAP::CloudController::Organization::SUSPENDED)
252+
end
253+
254+
it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS
255+
end
202256
end
203257

204-
let(:expected_codes_and_responses) do
205-
h = Hash.new({ code: 403, errors: CF_NOT_AUTHORIZED }.freeze)
206-
%w[no_role org_auditor org_billing_manager].each { |r| h[r] = { code: 404 } }
207-
%w[admin space_developer].each { |r| h[r] = { code: 200, response_object: feature_response_object } }
208-
h
258+
context 'when feature is disabled' do
259+
let(:service_binding_k8s_enabled) { false }
260+
let(:api_call) { ->(user_headers) { patch "/v3/apps/#{app_model.guid}/features/service-binding-k8s", request_body_enabled.to_json, user_headers } }
261+
262+
let(:feature_response_object) do
263+
{
264+
'name' => 'service-binding-k8s',
265+
'description' => 'Enable k8s service bindings for the app',
266+
'enabled' => true
267+
}
268+
end
269+
270+
let(:expected_codes_and_responses) do
271+
h = Hash.new({ code: 403, errors: CF_NOT_AUTHORIZED }.freeze)
272+
%w[no_role org_auditor org_billing_manager].each { |r| h[r] = { code: 404 } }
273+
%w[admin space_developer].each { |r| h[r] = { code: 200, response_object: feature_response_object } }
274+
h
275+
end
276+
277+
it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS
278+
279+
context 'when file-based-vcap-services is enabled' do
280+
before do
281+
patch "/v3/apps/#{app_model.guid}/features/file-based-vcap-services", request_body_enabled.to_json, admin_header
282+
end
283+
284+
it 'returns an error which states that both features cannot be enabled at the same time' do
285+
patch "/v3/apps/#{app_model.guid}/features/service-binding-k8s", request_body_enabled.to_json, admin_header
286+
287+
expect(last_response.status).to eq(422)
288+
expect(parsed_response['errors'][0]['detail']).to eq("'file-based-vcap-services' and 'service-binding-k8s' features cannot be enabled at the same time.")
289+
end
290+
end
209291
end
292+
end
210293

211-
it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS
294+
context 'file-based-vcap-services app feature' do
295+
context 'when feature is enabled' do
296+
let(:service_binding_k8s_enabled) { false }
297+
let(:file_based_vcap_services_enabled) { true }
298+
let(:api_call) { ->(user_headers) { patch "/v3/apps/#{app_model.guid}/features/file-based-vcap-services", request_body.to_json, user_headers } }
299+
let(:feature_response_object) do
300+
{
301+
'name' => 'file-based-vcap-services',
302+
'description' => 'Enable file-based VCAP service bindings for the app',
303+
'enabled' => false
304+
}
305+
end
212306

213-
context 'when organization is suspended' do
214307
let(:expected_codes_and_responses) do
215-
h = super()
216-
h['space_developer'] = { code: 403, errors: CF_ORG_SUSPENDED }
308+
h = Hash.new({ code: 403, errors: CF_NOT_AUTHORIZED }.freeze)
309+
%w[no_role org_auditor org_billing_manager].each { |r| h[r] = { code: 404 } }
310+
%w[admin space_developer].each { |r| h[r] = { code: 200, response_object: feature_response_object } }
217311
h
218312
end
219313

220-
before do
221-
org.update(status: VCAP::CloudController::Organization::SUSPENDED)
314+
it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS
315+
316+
context 'when organization is suspended' do
317+
let(:expected_codes_and_responses) do
318+
h = super()
319+
h['space_developer'] = { code: 403, errors: CF_ORG_SUSPENDED }
320+
h
321+
end
322+
323+
before do
324+
org.update(status: VCAP::CloudController::Organization::SUSPENDED)
325+
end
326+
327+
it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS
328+
end
329+
end
330+
331+
context 'when feature is disabled' do
332+
let(:service_binding_k8s_enabled) { false }
333+
let(:file_based_vcap_services_enabled) { false }
334+
let(:api_call) { ->(user_headers) { patch "/v3/apps/#{app_model.guid}/features/file-based-vcap-services", request_body_enabled.to_json, user_headers } }
335+
336+
let(:feature_response_object) do
337+
{
338+
'name' => 'file-based-vcap-services',
339+
'description' => 'Enable file-based VCAP service bindings for the app',
340+
'enabled' => true
341+
}
342+
end
343+
344+
let(:expected_codes_and_responses) do
345+
h = Hash.new({ code: 403, errors: CF_NOT_AUTHORIZED }.freeze)
346+
%w[no_role org_auditor org_billing_manager].each { |r| h[r] = { code: 404 } }
347+
%w[admin space_developer].each { |r| h[r] = { code: 200, response_object: feature_response_object } }
348+
h
222349
end
223350

224351
it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS
352+
353+
context 'when service-binding-k8s is enabled' do
354+
before do
355+
patch "/v3/apps/#{app_model.guid}/features/service-binding-k8s", request_body_enabled.to_json, admin_header
356+
end
357+
358+
it 'returns an error which states that both features cannot be enabled at the same time' do
359+
patch "/v3/apps/#{app_model.guid}/features/file-based-vcap-services", request_body_enabled.to_json, admin_header
360+
361+
expect(last_response.status).to eq(422)
362+
expect(parsed_response['errors'][0]['detail']).to eq("'file-based-vcap-services' and 'service-binding-k8s' features cannot be enabled at the same time.")
363+
end
364+
end
225365
end
226366
end
227367
end

spec/unit/actions/app_feature_update_spec.rb

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
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, file_based_service_bindings_enabled: false) }
8+
let(:app) { AppModel.make(enable_ssh: false, revisions_enabled: false) }
99
let(:message) { AppFeatureUpdateMessage.new(enabled: true) }
1010

1111
describe '.update' do
@@ -32,6 +32,14 @@ module VCAP::CloudController
3232
end.to change { app.reload.service_binding_k8s_enabled }.to(true)
3333
end
3434
end
35+
36+
context 'when the feature name is file-based-vcap-services' do
37+
it 'updates the file_based_vcap_services_enabled column on the app' do
38+
expect do
39+
AppFeatureUpdate.update('file-based-vcap-services', app, message)
40+
end.to change { app.reload.file_based_vcap_services_enabled }.to(true)
41+
end
42+
end
3543
end
3644
end
3745
end

0 commit comments

Comments
 (0)