Skip to content
This repository was archived by the owner on Jun 2, 2021. It is now read-only.

Commit 515d6c0

Browse files
Restrict manifest diffs to fields in the request body manifest
[#173802711] Co-authored-by: Nick Webb <nwebb@pivotal.io> Co-authored-by: Sarah Weinstein <sweinstein@pivotal.io>
1 parent 4ce7e9f commit 515d6c0

4 files changed

Lines changed: 207 additions & 109 deletions

File tree

app/actions/space_diff_manifest.rb

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
require 'presenters/v3/app_manifest_presenter'
2+
require 'json-diff'
3+
4+
module VCAP::CloudController
5+
class SpaceDiffManifest
6+
def self.generate_diff(app_manifests, space)
7+
json_diff = []
8+
9+
app_manifests.each_with_index do |manifest_app_hash, index|
10+
existing_app = space.app_models.find { |app| app.name == manifest_app_hash['name'] }
11+
12+
if existing_app.nil?
13+
existing_app_hash = {}
14+
else
15+
manifest_presenter = Presenters::V3::AppManifestPresenter.new(
16+
existing_app,
17+
existing_app.service_bindings,
18+
existing_app.routes,
19+
)
20+
existing_app_hash = manifest_presenter.to_hash.deep_stringify_keys['applications'][0]
21+
end
22+
23+
manifest_app_hash.each do |key, value|
24+
existing_value = existing_app_hash[key]
25+
26+
key_diffs = JsonDiff.diff(
27+
existing_value,
28+
value,
29+
include_was: true,
30+
)
31+
32+
key_diffs.each do |diff|
33+
diff['path'] = "/applications/#{index}/#{key}" + diff['path']
34+
35+
if diff['op'] == 'replace' && diff['was'].nil?
36+
diff['op'] = 'add'
37+
end
38+
39+
json_diff << diff
40+
end
41+
end
42+
end
43+
44+
json_diff
45+
end
46+
end
47+
end

app/controllers/v3/space_manifests_controller.rb

Lines changed: 7 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
require 'messages/named_app_manifest_message'
44
require 'actions/app_find_or_create_skeleton'
55
require 'actions/app_create'
6-
require 'json-diff'
6+
require 'actions/space_diff_manifest'
77

88
class SpaceManifestsController < ApplicationController
99
wrap_parameters :body, format: [:yaml]
@@ -15,7 +15,7 @@ def apply_manifest
1515
space_not_found! unless space && permission_queryer.can_read_from_space?(space.guid, space.organization.guid)
1616
unauthorized! unless permission_queryer.can_write_to_space?(space.guid)
1717

18-
messages = parsed_app_manifests.map { |app_manifest| NamedAppManifestMessage.create_from_yml(app_manifest) }
18+
messages = parsed_app_manifests.map(&:to_unsafe_h).map { |app_manifest| NamedAppManifestMessage.create_from_yml(app_manifest) }
1919
errors = messages.each_with_index.flat_map { |message, i| errors_for_message(message, i) }
2020
compound_error!(errors) unless errors.empty?
2121

@@ -47,36 +47,11 @@ def diff_manifest
4747
space_not_found! unless space && permission_queryer.can_read_from_space?(space.guid, space.organization.guid)
4848
unauthorized! unless permission_queryer.can_write_to_space?(space.guid)
4949

50-
json_diff = []
50+
parsed_manifests = parsed_app_manifests.map(&:to_hash)
5151

52-
parsed_app_manifests.each_with_index do |manifest_app_hash, index|
53-
existing_app = space.app_models.find { |app| app.name == manifest_app_hash['name'] } || {}
52+
diff = SpaceDiffManifest.generate_diff(parsed_manifests, space)
5453

55-
if existing_app == {}
56-
existing_app_hash = {}
57-
else
58-
manifest_presenter = Presenters::V3::AppManifestPresenter.new(
59-
existing_app,
60-
existing_app.service_bindings,
61-
existing_app.routes,
62-
)
63-
existing_app_hash = manifest_presenter.to_hash.deep_stringify_keys['applications'][0]
64-
end
65-
66-
curr_diff = JsonDiff.diff(
67-
existing_app_hash,
68-
manifest_app_hash.to_hash,
69-
include_was: true,
70-
)
71-
72-
curr_diff.each do |diff|
73-
diff['path'] = "/applications/#{index}" + diff['path']
74-
end
75-
76-
json_diff += curr_diff
77-
end
78-
79-
render status: :created, json: { diff: json_diff }
54+
render status: :created, json: { diff: diff }
8055
end
8156

8257
private
@@ -119,10 +94,10 @@ def check_version_is_supported!
11994

12095
def parsed_app_manifests
12196
check_version_is_supported!
122-
parsed_applications = params[:body]['applications']
97+
parsed_applications = params[:body].permit!['applications']
12398
raise unprocessable!("Cannot parse manifest with no 'applications' field.") unless parsed_applications.present?
12499

125-
parsed_applications.map(&:to_unsafe_h)
100+
parsed_applications
126101
end
127102

128103
def space_not_found!

spec/request/space_manifests_spec.rb

Lines changed: 34 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,6 @@
109109
it 'applies the manifest' do
110110
web_process = app1_model.web_processes.first
111111
expect(web_process.instances).to eq(1)
112-
113112
post "/v3/spaces/#{space.guid}/actions/apply_manifest", yml_manifest, yml_headers(user_header)
114113

115114
expect(last_response.status).to eq(202)
@@ -462,11 +461,36 @@
462461
end
463462

464463
describe 'POST /v3/spaces/:guid/manifest_diff' do
464+
let(:api_call) { lambda { |user_headers| post "/v3/spaces/#{space.guid}/manifest_diff", yml_manifest, yml_headers(user_headers) } }
465+
465466
let(:org) { space.organization }
466467
let(:app1_model) { VCAP::CloudController::AppModel.make(name: 'app-1', space: space) }
467468
let!(:process1) { VCAP::CloudController::ProcessModel.make(app: app1_model) }
468-
469-
let(:api_call) { lambda { |user_headers| post "/v3/spaces/#{space.guid}/manifest_diff", yml_manifest, yml_headers(user_headers) } }
469+
let!(:route_mapping) { VCAP::CloudController::RouteMappingModel.make(app: app1_model, process_type: process1.type, route: route) }
470+
let(:default_manifest) do
471+
{
472+
'applications' => [
473+
{
474+
'name' => app1_model.name,
475+
'stack' => process1.stack.name,
476+
'routes' => [
477+
{
478+
'route' => "a_host.#{shared_domain.name}"
479+
}
480+
],
481+
'processes' => [
482+
{
483+
'type' => process1.type,
484+
'instances' => process1.instances,
485+
'memory' => '1024M',
486+
'disk_quota' => '1024M',
487+
'health-check-type' => process1.health_check_type
488+
}
489+
]
490+
},
491+
]
492+
}
493+
end
470494

471495
let(:expected_codes_and_responses) do
472496
h = Hash.new(code: 403)
@@ -487,92 +511,25 @@
487511
h.freeze
488512
end
489513

490-
context 'when there are no changes in the manifest' do
491-
let(:diff_json) do
492-
{
493-
diff: []
494-
}
495-
end
496-
497-
let(:yml_manifest) do
498-
{
499-
'applications' => [
500-
{
501-
'name' => app1_model.name,
502-
'stack' => process1.stack.name,
503-
'processes' => [
504-
{
505-
'type' => process1.type,
506-
'instances' => process1.instances,
507-
'memory' => '1024M',
508-
'disk_quota' => '1024M',
509-
'health-check-type' => process1.health_check_type
510-
}
511-
]
512-
},
513-
]
514-
}.to_yaml
515-
end
516-
it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS
517-
end
518-
519514
context 'when there are changes in the manifest' do
520515
let(:diff_json) do
521516
{
522-
diff: [
523-
{ op: 'add', path: '/applications/0/comp', value: 'hoh' },
517+
diff: a_collection_containing_exactly(
518+
{ op: 'add', path: '/applications/0/new-key', value: 'hoh' },
524519
{ op: 'replace', path: '/applications/0/stack', was: process1.stack.name, value: 'big brother' },
525-
{ op: 'remove', path: '/applications/0/processes/0/memory', was: '1024M' },
526-
]
520+
)
527521
}
528522
end
529523

530524
let(:yml_manifest) do
531-
{
532-
'version' => 1,
533-
'applications' => [
534-
{
535-
'name' => app1_model.name,
536-
'stack' => 'big brother',
537-
'comp' => 'hoh',
538-
'processes' => [
539-
{
540-
'type' => process1.type,
541-
'instances' => process1.instances,
542-
'disk_quota' => '1024M',
543-
'health-check-type' => process1.health_check_type
544-
}
545-
]
546-
},
547-
]
548-
}.to_yaml
549-
end
550-
it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS
551-
end
552-
context 'when there is a new app' do
553-
let(:diff_json) do
554-
{
555-
diff: [
556-
{ op: 'add', path: '/applications/0/name', value: 'new-app' },
557-
{ op: 'add', path: '/applications/1/name', value: 'newer-app' },
558-
]
559-
}
525+
default_manifest['applications'][0]['new-key'] = 'hoh'
526+
default_manifest['applications'][0]['stack'] = 'big brother'
527+
default_manifest.to_yaml
560528
end
561529

562-
let(:yml_manifest) do
563-
{
564-
'applications' => [
565-
{
566-
'name' => 'new-app',
567-
},
568-
{
569-
'name' => 'newer-app',
570-
}
571-
]
572-
}.to_yaml
573-
end
574530
it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS
575531
end
532+
576533
context 'when the request is invalid' do
577534
before do
578535
space.organization.add_user(user)
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
require 'spec_helper'
2+
require 'actions/space_diff_manifest'
3+
4+
module VCAP::CloudController
5+
RSpec.describe SpaceDiffManifest do
6+
describe 'generate_diff' do
7+
let(:default_manifest) {
8+
{
9+
'applications' => [
10+
{
11+
'name' => app1_model.name,
12+
'stack' => process1.stack.name,
13+
'routes' => [
14+
{
15+
'route' => "a_host.#{shared_domain.name}"
16+
}
17+
],
18+
'processes' => [
19+
{
20+
'type' => process1.type,
21+
'instances' => process1.instances,
22+
'memory' => '1024M',
23+
'disk_quota' => '1024M',
24+
'health-check-type' => process1.health_check_type
25+
}
26+
]
27+
},
28+
]
29+
}
30+
}
31+
32+
let(:app_manifests) { default_manifest['applications'] }
33+
let(:space) { Space.make }
34+
let(:app1_model) { AppModel.make(name: 'app-1', space: space) }
35+
let!(:process1) { ProcessModel.make(app: app1_model) }
36+
let(:shared_domain) { VCAP::CloudController::SharedDomain.make }
37+
let(:route) { VCAP::CloudController::Route.make(domain: shared_domain, space: space, host: 'a_host') }
38+
let!(:route_mapping) { VCAP::CloudController::RouteMappingModel.make(app: app1_model, process_type: process1.type, route: route) }
39+
40+
subject { SpaceDiffManifest.generate_diff(app_manifests, space) }
41+
42+
context 'when a top-level field is omitted' do
43+
before do
44+
default_manifest['applications'][0].delete 'stack'
45+
end
46+
47+
it 'returns an empty diff' do
48+
expect(subject).to eq([])
49+
end
50+
end
51+
52+
context 'when there are changes in the manifest' do
53+
before do
54+
default_manifest['applications'][0]['new-key'] = 'hoh'
55+
default_manifest['applications'][0]['stack'] = 'big brother'
56+
end
57+
58+
it 'returns the correct diff' do
59+
expect(subject).to match_array([
60+
{ 'op' => 'add', 'path' => '/applications/0/new-key', 'value' => 'hoh' },
61+
{ 'op' => 'replace', 'path' => '/applications/0/stack', 'was' => process1.stack.name, 'value' => 'big brother' },
62+
])
63+
end
64+
end
65+
66+
context 'when there is a change inside of a nested hash' do
67+
before do
68+
default_manifest['applications'][0]['processes'][0].delete('memory')
69+
end
70+
71+
it 'returns the correct diff' do
72+
expect(subject).to eq([
73+
{ 'op' => 'remove', 'path' => '/applications/0/processes/0/memory', 'was' => '1024M' },
74+
])
75+
end
76+
end
77+
78+
context 'when there is a difference in a nested array' do
79+
before do
80+
default_manifest['applications'][0]['sidecars'] = [
81+
{ 'name' => 'sidecar1', 'command' => 'bundle exec rake lol', 'process_types' => ['web', 'worker'] }
82+
]
83+
end
84+
85+
it 'returns the correct diff' do
86+
expect(subject).to eq([
87+
{
88+
'op' => 'add',
89+
'path' => '/applications/0/sidecars',
90+
'value' => [
91+
{ 'name' => 'sidecar1', 'command' => 'bundle exec rake lol', 'process_types' => ['web', 'worker'] }
92+
]
93+
},
94+
])
95+
end
96+
end
97+
98+
context 'when there is a new app' do
99+
let(:app_manifests) do
100+
[
101+
{
102+
'name' => 'new-app',
103+
},
104+
{
105+
'name' => 'newer-app',
106+
}
107+
]
108+
end
109+
110+
it 'returns the correct diff' do
111+
expect(subject).to eq([
112+
{ 'op' => 'add', 'path' => '/applications/0/name', 'value' => 'new-app' },
113+
{ 'op' => 'add', 'path' => '/applications/1/name', 'value' => 'newer-app' },
114+
])
115+
end
116+
end
117+
end
118+
end
119+
end

0 commit comments

Comments
 (0)