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

Commit 080a393

Browse files
jpalmerpivotalTeal Stannardreidmitsweinstein22
authored andcommitted
Implement naive manifest diffing
- add new `/v3/spaces/:guid/manifest-diff` endpoint - does not handle array manifest fields with push style logic - buildpacks are coincidentally right - uses JSONpatch convention - only reports diff for apps in the provided manifest, ignoring any other apps in the space - endpoint only accessible to space writer roles [#173688915] [finishes #173478913] Co-authored-by: James Palmer <jpalmer@pivotal.io> Co-authored-by: Teal Stannard <tstannard@pivotal.io> Co-authored-by: Reid Mitchell <rmitchell@pivotal.io> Co-authored-by: Sarah Weinstein <sweinstein@pivotal.io>
1 parent fd7c580 commit 080a393

File tree

7 files changed

+257
-7
lines changed

7 files changed

+257
-7
lines changed

Gemfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ gem 'fluent-logger'
1111
gem 'googleapis-common-protos'
1212
gem 'hashdiff'
1313
gem 'httpclient'
14+
gem 'json-diff'
1415
gem 'json-schema'
1516
gem 'json_pure'
1617
gem 'kubeclient'

Gemfile.lock

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,7 @@ GEM
239239
ipaddress (0.8.3)
240240
jaro_winkler (1.5.4)
241241
json (2.3.0)
242+
json-diff (0.4.1)
242243
json-schema (2.8.0)
243244
addressable (>= 2.4)
244245
json_pure (2.1.0)
@@ -512,6 +513,7 @@ DEPENDENCIES
512513
googleapis-common-protos
513514
hashdiff
514515
httpclient
516+
json-diff
515517
json-schema
516518
json_pure
517519
kubeclient

app/controllers/v3/space_manifests_controller.rb

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +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'
67

78
class SpaceManifestsController < ApplicationController
89
wrap_parameters :body, format: [:yaml]
@@ -41,6 +42,43 @@ def apply_manifest
4142
head HTTP::ACCEPTED, 'Location' => url_builder.build_url(path: "/v3/jobs/#{job.guid}")
4243
end
4344

45+
def diff_manifest
46+
space = Space.find(guid: hashed_params[:guid])
47+
space_not_found! unless space && permission_queryer.can_read_from_space?(space.guid, space.organization.guid)
48+
unauthorized! unless permission_queryer.can_write_to_space?(space.guid)
49+
50+
json_diff = []
51+
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'] } || {}
54+
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 }
80+
end
81+
4482
private
4583

4684
def errors_for_message(message, index)

config/routes.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,7 @@
228228

229229
# space_manifests
230230
post '/spaces/:guid/actions/apply_manifest', to: 'space_manifests#apply_manifest'
231+
post '/spaces/:guid/manifest_diff', to: 'space_manifests#diff_manifest'
231232

232233
# space_quotas
233234
post '/space_quotas', to: 'space_quotas#create'
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
### Create a manifest diff for apps
2+
3+
```
4+
Example Request
5+
```
6+
7+
```shell
8+
curl "https://api.example.org/v3/spaces/[guid]/manifest_diff" \
9+
-X POST \
10+
-H "Authorization: bearer [token]" \
11+
-d @/path/to/manifest.yml
12+
```
13+
14+
```
15+
Example Response
16+
```
17+
18+
```http
19+
HTTP/1.1 202 OK
20+
Content-Type: application/json
21+
22+
{
23+
"diff": [
24+
{
25+
"op": "remove",
26+
"path": "/applications/0/routes/1",
27+
"was": {"route": "route.example.com"}
28+
},
29+
{
30+
"op": "add",
31+
"path": "/applications/1/buildpacks/2",
32+
"value": "java_buildpack"
33+
},
34+
{
35+
"op": "replace",
36+
"path": "/applications/2/processes/1/memory",
37+
"was": "256M",
38+
"value": "512M"
39+
}
40+
]
41+
}
42+
```
43+
44+
Create a manifest diff for apps in the provided manifest and their underlying processes.
45+
46+
#### Definition
47+
`GET /v3/spaces/:guid/manifest_diff`
48+
49+
#### Permitted Roles
50+
|
51+
--- | ---
52+
Admin |
53+
Space Developer |

docs/v3/source/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ includes:
168168
- resources/manifests/object
169169
- resources/manifests/apply
170170
- resources/manifests/get
171+
- resources/manifests/create_diff
171172
- resources/organizations/header
172173
- resources/organizations/object
173174
- resources/organizations/create

spec/request/space_manifests_spec.rb

Lines changed: 161 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
require 'spec_helper'
2+
require 'request_spec_shared_examples'
23

34
RSpec.describe 'Space Manifests' do
45
let(:user) { VCAP::CloudController::User.make }
@@ -10,12 +11,6 @@
1011
VCAP::CloudController::Route.make(domain: shared_domain, space: space, path: '/path', host: 'b_host')
1112
}
1213

13-
before do
14-
space.organization.add_user(user)
15-
space.add_developer(user)
16-
TestConfig.override(kubernetes: {})
17-
end
18-
1914
describe 'POST /v3/spaces/:guid/actions/apply_manifest' do
2015
let(:buildpack) { VCAP::CloudController::Buildpack.make }
2116
let(:service_instance) { VCAP::CloudController::ManagedServiceInstance.make(space: space) }
@@ -100,9 +95,13 @@
10095
end
10196

10297
before do
98+
space.organization.add_user(user)
99+
space.add_developer(user)
100+
TestConfig.override(kubernetes: {})
101+
103102
stub_bind(service_instance)
104103
VCAP::CloudController::LabelsUpdate.update(app1_model, { 'potato' => 'french',
105-
'downton' => 'abbey road', }, VCAP::CloudController::AppLabelModel)
104+
'downton' => 'abbey road', }, VCAP::CloudController::AppLabelModel)
106105
VCAP::CloudController::AnnotationsUpdate.update(app1_model, { 'potato' => 'baked',
107106
'berry' => 'white', }, VCAP::CloudController::AppAnnotationModel)
108107
end
@@ -461,4 +460,159 @@
461460
end
462461
end
463462
end
463+
464+
describe 'POST /v3/spaces/:guid/manifest_diff' do
465+
let(:org) { space.organization }
466+
let(:app1_model) { VCAP::CloudController::AppModel.make(name: 'app-1', space: space) }
467+
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) } }
470+
471+
let(:expected_codes_and_responses) do
472+
h = Hash.new(code: 403)
473+
h['org_auditor'] = { code: 404 }
474+
h['org_billing_manager'] = { code: 404 }
475+
h['no_role'] = { code: 404 }
476+
477+
h['admin'] = {
478+
code: 201,
479+
response_object: diff_json
480+
}
481+
482+
h['space_developer'] = {
483+
code: 201,
484+
response_object: diff_json
485+
}
486+
487+
h.freeze
488+
end
489+
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+
519+
context 'when there are changes in the manifest' do
520+
let(:diff_json) do
521+
{
522+
diff: [
523+
{ op: 'add', path: '/applications/0/comp', value: 'hoh' },
524+
{ 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+
]
527+
}
528+
end
529+
530+
let(:yml_manifest) do
531+
{
532+
'applications' => [
533+
{
534+
'name' => app1_model.name,
535+
'stack' => 'big brother',
536+
'comp' => 'hoh',
537+
'processes' => [
538+
{
539+
'type' => process1.type,
540+
'instances' => process1.instances,
541+
'disk_quota' => '1024M',
542+
'health-check-type' => process1.health_check_type
543+
}
544+
]
545+
},
546+
]
547+
}.to_yaml
548+
end
549+
it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS
550+
end
551+
context 'when there is a new app' do
552+
let(:diff_json) do
553+
{
554+
diff: [
555+
{ op: 'add', path: '/applications/0/name', value: 'new-app' },
556+
{ op: 'add', path: '/applications/1/name', value: 'newer-app' },
557+
]
558+
}
559+
end
560+
561+
let(:yml_manifest) do
562+
{
563+
'applications' => [
564+
{
565+
'name' => 'new-app',
566+
},
567+
{
568+
'name' => 'newer-app',
569+
}
570+
]
571+
}.to_yaml
572+
end
573+
it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS
574+
end
575+
576+
context 'the manifest is unparseable' do
577+
let(:yml_manifest) do
578+
{
579+
89 => [
580+
{
581+
'name' => 'new-app',
582+
},
583+
]
584+
}.to_yaml
585+
end
586+
587+
before do
588+
space.organization.add_user(user)
589+
space.add_developer(user)
590+
end
591+
592+
it 'returns an appropriate error' do
593+
post "/v3/spaces/#{space.guid}/manifest_diff", yml_manifest, yml_headers(user_header)
594+
parsed_response = MultiJson.load(last_response.body)
595+
596+
expect(last_response).to have_status_code(400)
597+
expect(parsed_response['errors'].first['detail']).to eq('The request is invalid')
598+
end
599+
end
600+
context 'the space does not exist' do
601+
let(:yml_manifest) do
602+
{
603+
'applications' => [
604+
{
605+
'name' => 'new-app',
606+
},
607+
]
608+
}.to_yaml
609+
end
610+
611+
it 'returns an appropriate error' do
612+
post '/v3/spaces/not-space-guid/manifest_diff', yml_manifest, yml_headers(user_header)
613+
614+
expect(last_response).to have_status_code(404)
615+
end
616+
end
617+
end
464618
end

0 commit comments

Comments
 (0)