Skip to content

Commit 33be9d9

Browse files
committed
V2 API stack validation and tests
Signed-off-by: Rashed Kamal <[email protected]>
1 parent 12a6644 commit 33be9d9

File tree

9 files changed

+300
-7
lines changed

9 files changed

+300
-7
lines changed

app/actions/v2/app_stage.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
module VCAP::CloudController
22
module V2
33
class AppStage
4+
attr_reader :warnings
5+
46
def initialize(stagers:)
57
@stagers = stagers
8+
@warnings = []
69
end
710

811
def stage(process)
@@ -25,6 +28,9 @@ def stage(process)
2528
lifecycle: lifecycle,
2629
start_after_staging: true
2730
)
31+
32+
@warnings = build.instance_variable_get(:@stack_warnings) || []
33+
2834
TelemetryLogger.v2_emit(
2935
'create-build',
3036
{

app/actions/v2/app_update.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44
module VCAP::CloudController
55
module V2
66
class AppUpdate
7+
attr_reader :warnings
78
def initialize(access_validator:, stagers:)
89
@access_validator = access_validator
910
@stagers = stagers
11+
@warnings = []
1012
end
1113

1214
def update(app, process, request_attrs)
@@ -116,7 +118,9 @@ def prepare_to_stage(app)
116118
end
117119

118120
def stage(process)
119-
V2::AppStage.new(stagers: @stagers).stage(process)
121+
app_stage = V2::AppStage.new(stagers: @stagers)
122+
app_stage.stage(process)
123+
@warnings = app_stage.warnings
120124
end
121125

122126
def start_or_stop(app, request_attrs)

app/controllers/runtime/apps_controller.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,7 @@ def update(guid)
294294

295295
updater = V2::AppUpdate.new(access_validator: self, stagers: @stagers)
296296
updater.update(app, process, request_attrs)
297+
updater.warnings.each { |warning| add_warning(warning) }
297298

298299
after_update(process)
299300

app/controllers/runtime/restages_controller.rb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,10 @@ def restage(guid)
3535
process.app.update(droplet_guid: nil)
3636
AppStart.start_without_event(process.app, create_revision: false)
3737
end
38-
V2::AppStage.new(stagers: @stagers).stage(process)
38+
# V2::AppStage.new(stagers: @stagers).stage(process)
39+
app_stage = V2::AppStage.new(stagers: @stagers)
40+
app_stage.stage(process)
41+
app_stage.warnings.each { |warning| add_warning(warning) }
3942

4043
@app_event_repository.record_app_restage(process, UserAuditInfo.from_context(SecurityContext))
4144

spec/request/v2/apps_spec.rb

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -926,6 +926,58 @@
926926
end
927927
end
928928

929+
context 'stack state validation on app update with staging' do
930+
let(:stager) { instance_double(VCAP::CloudController::Diego::Stager, stage: nil) }
931+
932+
before do
933+
allow_any_instance_of(VCAP::CloudController::Stagers).to receive(:validate_process)
934+
allow_any_instance_of(VCAP::CloudController::Stagers).to receive(:stager_for_build).and_return(stager)
935+
VCAP::CloudController::Buildpack.make
936+
VCAP::CloudController::PackageModel.make(app: process.app, state: VCAP::CloudController::PackageModel::READY_STATE)
937+
end
938+
939+
context 'when stack is DISABLED' do
940+
let(:disabled_stack) { VCAP::CloudController::Stack.make(name: 'cflinuxfs2', state: 'DISABLED', description: 'Migrate to cflinuxfs4') }
941+
let(:update_params) { Oj.dump({ state: 'STARTED' }) }
942+
943+
before do
944+
process.app.buildpack_lifecycle_data.update(stack: disabled_stack.name)
945+
process.update(state: 'STOPPED')
946+
end
947+
948+
it 'returns 422 with stack validation error when starting app' do
949+
put "/v2/apps/#{process.guid}", update_params, headers_for(user)
950+
951+
expect(last_response.status).to eq(422)
952+
parsed_response = Oj.load(last_response.body)
953+
expect(parsed_response['error_code']).to eq('CF-StackValidationFailed')
954+
expect(parsed_response['description']).to include('disabled')
955+
expect(parsed_response['description']).to include('cflinuxfs2')
956+
end
957+
end
958+
959+
context 'when stack is DEPRECATED' do
960+
let(:deprecated_stack) { VCAP::CloudController::Stack.make(name: 'cflinuxfs3', state: 'DEPRECATED', description: 'EOL Dec 2025') }
961+
let(:update_params) { Oj.dump({ state: 'STARTED' }) }
962+
963+
before do
964+
process.app.buildpack_lifecycle_data.update(stack: deprecated_stack.name)
965+
process.app.update(droplet_guid: nil)
966+
process.update(state: 'STOPPED')
967+
end
968+
969+
it 'allows starting app with deprecation warning' do
970+
put "/v2/apps/#{process.guid}", update_params, headers_for(user)
971+
972+
expect(last_response.status).to eq(201)
973+
expect(last_response.headers['X-Cf-Warnings']).to be_present
974+
decoded_warning = CGI.unescape(last_response.headers['X-Cf-Warnings'])
975+
expect(decoded_warning).to include('deprecated')
976+
expect(decoded_warning).to include('EOL Dec 2025')
977+
end
978+
end
979+
end
980+
929981
context 'when process memory is being decreased and the new memory allocation is lower than memory of associated sidecars' do
930982
let!(:process) do
931983
VCAP::CloudController::ProcessModelFactory.make(
@@ -1563,6 +1615,139 @@ def make_actual_lrp(instance_guid:, index:, state:, error:, since:)
15631615
end
15641616
end
15651617
end
1618+
1619+
1620+
context 'stack state validation' do
1621+
let(:process) { VCAP::CloudController::ProcessModelFactory.make(name: 'maria', space: space, diego: true) }
1622+
let(:stager) { instance_double(VCAP::CloudController::Diego::Stager, stage: nil) }
1623+
1624+
before do
1625+
allow_any_instance_of(VCAP::CloudController::Stagers).to receive(:validate_process)
1626+
allow_any_instance_of(VCAP::CloudController::Stagers).to receive(:stager_for_build).and_return(stager)
1627+
VCAP::CloudController::Buildpack.make
1628+
end
1629+
1630+
context 'when stack is DISABLED' do
1631+
let(:disabled_stack) { VCAP::CloudController::Stack.make(name: 'cflinuxfs2', state: 'DISABLED', description: 'Migrate to cflinuxfs4') }
1632+
1633+
before do
1634+
process.app.buildpack_lifecycle_data.update(stack: disabled_stack.name)
1635+
end
1636+
1637+
it 'returns 422 with stack validation error' do
1638+
post "/v2/apps/#{process.guid}/restage", nil, headers_for(user)
1639+
1640+
expect(last_response.status).to eq(422)
1641+
parsed_response = Oj.load(last_response.body)
1642+
expect(parsed_response['error_code']).to eq('CF-StackValidationFailed')
1643+
expect(parsed_response['description']).to include('disabled')
1644+
expect(parsed_response['description']).to include('cannot be used for staging')
1645+
expect(parsed_response['description']).to include('cflinuxfs2')
1646+
expect(parsed_response['description']).to include('Migrate to cflinuxfs4')
1647+
end
1648+
1649+
it 'does not expose stack state field in error response' do
1650+
post "/v2/apps/#{process.guid}/restage", nil, headers_for(user)
1651+
1652+
parsed_response = Oj.load(last_response.body)
1653+
expect(parsed_response['entity']).to be_nil
1654+
expect(parsed_response['error_code']).to eq('CF-StackValidationFailed')
1655+
end
1656+
end
1657+
1658+
context 'when stack is RESTRICTED' do
1659+
let(:restricted_stack) { VCAP::CloudController::Stack.make(name: 'cflinuxfs3', state: 'RESTRICTED', description: 'No new apps') }
1660+
1661+
before do
1662+
process.app.buildpack_lifecycle_data.update(stack: restricted_stack.name)
1663+
end
1664+
1665+
context 'for first build' do
1666+
before do
1667+
process.app.builds_dataset.destroy
1668+
end
1669+
1670+
it 'returns 422 with stack validation error' do
1671+
expect(process.app.builds_dataset.count).to eq(0)
1672+
1673+
post "/v2/apps/#{process.guid}/restage", nil, headers_for(user)
1674+
1675+
expect(last_response.status).to eq(422)
1676+
parsed_response = Oj.load(last_response.body)
1677+
expect(parsed_response['error_code']).to eq('CF-StackValidationFailed')
1678+
expect(parsed_response['description']).to include('cannot be used for staging new applications')
1679+
expect(parsed_response['description']).to include('cflinuxfs3')
1680+
end
1681+
end
1682+
1683+
context 'for restaging existing app' do
1684+
before do
1685+
VCAP::CloudController::BuildModel.make(app: process.app, state: 'STAGED')
1686+
end
1687+
1688+
it 'allows restaging without errors' do
1689+
post "/v2/apps/#{process.guid}/restage", nil, headers_for(user)
1690+
1691+
expect(last_response.status).to eq(201)
1692+
end
1693+
1694+
it 'does not include warnings header' do
1695+
post "/v2/apps/#{process.guid}/restage", nil, headers_for(user)
1696+
1697+
expect(last_response.headers['X-Cf-Warnings']).to be_nil
1698+
end
1699+
end
1700+
end
1701+
1702+
context 'when stack is DEPRECATED' do
1703+
let(:deprecated_stack) { VCAP::CloudController::Stack.make(name: 'cflinuxfs3', state: 'DEPRECATED', description: 'EOL Dec 2025') }
1704+
1705+
before do
1706+
process.app.buildpack_lifecycle_data.update(stack: deprecated_stack.name)
1707+
end
1708+
1709+
it 'allows restaging with success' do
1710+
post "/v2/apps/#{process.guid}/restage", nil, headers_for(user)
1711+
1712+
expect(last_response.status).to eq(201)
1713+
end
1714+
1715+
it 'includes deprecation warning in X-Cf-Warnings header' do
1716+
post "/v2/apps/#{process.guid}/restage", nil, headers_for(user)
1717+
1718+
expect(last_response.headers['X-Cf-Warnings']).to be_present
1719+
decoded_warning = CGI.unescape(last_response.headers['X-Cf-Warnings'])
1720+
expect(decoded_warning).to include('deprecated')
1721+
expect(decoded_warning).to include('cflinuxfs3')
1722+
expect(decoded_warning).to include('EOL Dec 2025')
1723+
end
1724+
1725+
it 'does not expose stack state field in response body' do
1726+
post "/v2/apps/#{process.guid}/restage", nil, headers_for(user)
1727+
1728+
parsed_response = Oj.load(last_response.body)
1729+
# The response includes process 'state' (STARTED/STOPPED) which is different from stack 'state'
1730+
# We verify stack state is not exposed by checking it's not in the stack-related fields
1731+
expect(parsed_response['entity']['stack_guid']).to be_present
1732+
expect(parsed_response.to_s).not_to match(/DEPRECATED/)
1733+
end
1734+
end
1735+
1736+
context 'when stack is ACTIVE' do
1737+
let(:active_stack) { VCAP::CloudController::Stack.make(name: 'cflinuxfs5', state: 'ACTIVE') }
1738+
1739+
before do
1740+
process.app.buildpack_lifecycle_data.update(stack: active_stack.name)
1741+
end
1742+
1743+
it 'allows restaging without warnings' do
1744+
post "/v2/apps/#{process.guid}/restage", nil, headers_for(user)
1745+
1746+
expect(last_response.status).to eq(201)
1747+
expect(last_response.headers['X-Cf-Warnings']).to be_nil
1748+
end
1749+
end
1750+
end
15661751
end
15671752

15681753
describe 'PUT /v2/apps/:guid/bits' do

spec/unit/actions/v2/app_stage_spec.rb

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,100 @@ module V2
215215
end
216216
end
217217
end
218+
219+
context 'stack state warnings' do
220+
let(:process) { ProcessModelFactory.make }
221+
let(:user_audit_info) do
222+
UserAuditInfo.new(user_email: '[email protected]', user_name: 'test-user', user_guid: 'test-guid')
223+
end
224+
225+
before do
226+
allow(UserAuditInfo).to receive(:from_context).and_return(user_audit_info)
227+
allow(BuildCreate).to receive(:new).and_call_original
228+
allow_any_instance_of(Diego::Stager).to receive(:stage).and_return 'staging-complete'
229+
end
230+
231+
context 'when stack is DEPRECATED' do
232+
let(:deprecated_stack) { Stack.make(name: 'cflinuxfs3', state: 'DEPRECATED', description: 'EOL Dec 2025') }
233+
234+
before do
235+
process.app.buildpack_lifecycle_data.update(stack: deprecated_stack.name)
236+
end
237+
238+
it 'captures warnings from BuildCreate' do
239+
action.stage(process)
240+
241+
expect(action.warnings).not_to be_empty
242+
expect(action.warnings.first).to include('deprecated')
243+
expect(action.warnings.first).to include('cflinuxfs3')
244+
expect(action.warnings.first).to include('EOL Dec 2025')
245+
end
246+
end
247+
248+
context 'when stack is ACTIVE' do
249+
let(:active_stack) { Stack.make(name: 'cflinuxfs5', state: 'ACTIVE') }
250+
251+
before do
252+
process.app.buildpack_lifecycle_data.update(stack: active_stack.name)
253+
end
254+
255+
it 'has no warnings' do
256+
action.stage(process)
257+
258+
expect(action.warnings).to be_empty
259+
end
260+
end
261+
262+
context 'when stack is DISABLED' do
263+
let(:disabled_stack) { Stack.make(name: 'cflinuxfs2', state: 'DISABLED', description: 'Migrate to cflinuxfs4') }
264+
265+
before do
266+
process.app.buildpack_lifecycle_data.update(stack: disabled_stack.name)
267+
end
268+
269+
it 'raises StackValidationFailed error' do
270+
expect { action.stage(process) }.to raise_error(CloudController::Errors::ApiError) do |error|
271+
expect(error.name).to eq('StackValidationFailed')
272+
expect(error.message).to include('disabled')
273+
expect(error.message).to include('cannot be used for staging')
274+
end
275+
end
276+
end
277+
278+
context 'when stack is RESTRICTED' do
279+
let(:restricted_stack) { Stack.make(name: 'cflinuxfs3-restricted', state: 'RESTRICTED', description: 'No new apps') }
280+
281+
before do
282+
process.app.buildpack_lifecycle_data.update(stack: restricted_stack.name)
283+
end
284+
285+
context 'for first build' do
286+
before do
287+
process.app.builds_dataset.destroy
288+
end
289+
290+
it 'raises StackValidationFailed error' do
291+
expect(process.app.builds_dataset.count).to eq(0)
292+
293+
expect { action.stage(process) }.to raise_error(CloudController::Errors::ApiError) do |error|
294+
expect(error.name).to eq('StackValidationFailed')
295+
expect(error.message).to include('cannot be used for staging new applications')
296+
end
297+
end
298+
end
299+
300+
context 'for restaging existing app' do
301+
before do
302+
BuildModel.make(app: process.app, state: 'STAGED')
303+
end
304+
305+
it 'allows staging without warnings' do
306+
expect { action.stage(process) }.not_to raise_error
307+
expect(action.warnings).to be_empty
308+
end
309+
end
310+
end
311+
end
218312
end
219313
end
220314
end

spec/unit/actions/v2/app_update_spec.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -432,7 +432,7 @@ module VCAP::CloudController
432432
describe 'updating docker_image' do
433433
let(:process) { ProcessModelFactory.make(app: AppModel.make(:docker), docker_image: 'repo/original-image') }
434434
let!(:original_package) { process.latest_package }
435-
let(:app_stage) { instance_double(V2::AppStage, stage: nil) }
435+
let(:app_stage) { instance_double(V2::AppStage, stage: nil, warnings: []) }
436436

437437
before do
438438
FeatureFlag.create(name: 'diego_docker', enabled: true)
@@ -507,7 +507,7 @@ module VCAP::CloudController
507507
describe 'updating docker_credentials' do
508508
let(:process) { ProcessModelFactory.make(app: AppModel.make(:docker), docker_image: 'repo/original-image') }
509509
let!(:original_package) { process.latest_package }
510-
let(:app_stage) { instance_double(V2::AppStage, stage: nil) }
510+
let(:app_stage) { instance_double(V2::AppStage, stage: nil, warnings: []) }
511511

512512
before do
513513
FeatureFlag.create(name: 'diego_docker', enabled: true)
@@ -545,7 +545,7 @@ module VCAP::CloudController
545545
end
546546

547547
describe 'staging' do
548-
let(:app_stage) { instance_double(V2::AppStage, stage: nil) }
548+
let(:app_stage) { instance_double(V2::AppStage, stage: nil, warnings: []) }
549549
let(:process) { ProcessModelFactory.make(state: 'STARTED') }
550550
let(:app) { process.app }
551551

0 commit comments

Comments
 (0)