diff --git a/app/fetchers/app_revisions_fetcher.rb b/app/fetchers/app_revisions_fetcher.rb index 5ef206b78f6..5dc2ab578c4 100644 --- a/app/fetchers/app_revisions_fetcher.rb +++ b/app/fetchers/app_revisions_fetcher.rb @@ -1,10 +1,14 @@ module VCAP::CloudController class AppRevisionsFetcher def self.fetch(app, message) - dataset = RevisionModel.where(app_guid: app.guid) + dataset = RevisionModel.where(Sequel[:revisions][:app_guid] => app.guid) if message.requested?(:versions) - dataset = dataset.where(app_guid: app.guid, version: message.versions) + dataset = dataset.where(Sequel[:revisions][:app_guid] => app.guid, version: message.versions) + end + + if message.requested?(:deployable) + dataset = dataset.join(:droplets, guid: :droplet_guid).where(Sequel[:revisions][:app_guid] => app.guid, Sequel[:droplets][:state] => DropletModel::STAGED_STATE) end if message.requested?(:label_selector) diff --git a/app/messages/app_revisions_list_message.rb b/app/messages/app_revisions_list_message.rb index 657051d1cee..065bdc23623 100644 --- a/app/messages/app_revisions_list_message.rb +++ b/app/messages/app_revisions_list_message.rb @@ -4,11 +4,15 @@ module VCAP::CloudController class AppRevisionsListMessage < MetadataListMessage register_allowed_keys [ :versions, + :deployable, ] validates_with NoAdditionalParamsValidator validates :versions, array: true, allow_nil: true + validates :deployable, + inclusion: { in: [true, false], message: 'must be a boolean' }, + allow_nil: true def self.from_params(params) super(params, %w(versions)) diff --git a/app/presenters/v3/revision_presenter.rb b/app/presenters/v3/revision_presenter.rb index 0479a453407..f870ae55101 100644 --- a/app/presenters/v3/revision_presenter.rb +++ b/app/presenters/v3/revision_presenter.rb @@ -30,7 +30,9 @@ def to_hash metadata: { labels: hashified_labels(revision.labels), annotations: hashified_annotations(revision.annotations), - } + }, + deployable: deployable + } end @@ -68,6 +70,10 @@ def sidecars } end end + + def deployable + revision.droplet.staged? + end end end end diff --git a/docs/v3/source/includes/api_resources/_revisions.erb b/docs/v3/source/includes/api_resources/_revisions.erb index e56e19f03c5..7635e98b552 100644 --- a/docs/v3/source/includes/api_resources/_revisions.erb +++ b/docs/v3/source/includes/api_resources/_revisions.erb @@ -19,6 +19,7 @@ } ], "description": "Initial revision.", + "deployable": true, "relationships": { "app": { "data": { @@ -81,6 +82,7 @@ } ], "description": "Initial revision.", + "deployable": true, "relationships": { "app": { "data": { diff --git a/docs/v3/source/includes/experimental_resources/revisions/_object.md.erb b/docs/v3/source/includes/experimental_resources/revisions/_object.md.erb index 932878d467a..bd098d62eff 100644 --- a/docs/v3/source/includes/experimental_resources/revisions/_object.md.erb +++ b/docs/v3/source/includes/experimental_resources/revisions/_object.md.erb @@ -17,6 +17,7 @@ Name | Type | Description **created_at** | _datetime_ | The time with zone when the object was created. **updated_at** | _datetime_ | The time with zone when the object was last updated. **description** | _string_ | A short description of the reason for revision. +**deployable** _(experimental)_ | _boolean_ | Indicates if the revision's droplet is staged and the revision can be used to [create a deployment](#create-a-deployment). **relationships.app** | [_to-one relationship_](#to-one-relationships) | The app the revision is associated with. **metadata.labels** | [_label object_](#labels) | Labels applied to the revision. **metadata.annotations** | [_annotation object_](#annotations) | Annotations applied to the revision. diff --git a/spec/request/revisions_spec.rb b/spec/request/revisions_spec.rb index 3506606f2b4..ee782612e14 100644 --- a/spec/request/revisions_spec.rb +++ b/spec/request/revisions_spec.rb @@ -69,7 +69,8 @@ 'command' => 'run-sidecar', 'process_types' => ['web'], 'memory_in_mb' => 300, - }] + }], + 'deployable' => true } ) end @@ -146,6 +147,7 @@ }, }, 'sidecars' => [], + 'deployable' => true }, { 'guid' => revision2.guid, @@ -181,6 +183,7 @@ }, }, 'sidecars' => [], + 'deployable' => true } ] } @@ -244,6 +247,7 @@ }, }, 'sidecars' => [], + 'deployable' => true }, { 'guid' => revision3.guid, @@ -279,6 +283,7 @@ }, }, 'sidecars' => [], + 'deployable' => true } ] } @@ -367,6 +372,7 @@ }, }, 'sidecars' => [], + 'deployable' => true } ) end @@ -461,6 +467,7 @@ }, }, 'sidecars' => [], + 'deployable' => true }, { 'guid' => revision2.guid, @@ -496,6 +503,7 @@ }, }, 'sidecars' => [], + 'deployable' => true } ] } diff --git a/spec/unit/fetchers/app_revisions_fetcher_spec.rb b/spec/unit/fetchers/app_revisions_fetcher_spec.rb index b580f4f2dcf..c866115e5ff 100644 --- a/spec/unit/fetchers/app_revisions_fetcher_spec.rb +++ b/spec/unit/fetchers/app_revisions_fetcher_spec.rb @@ -5,8 +5,12 @@ module VCAP::CloudController RSpec.describe AppRevisionsFetcher do let(:fetcher) { AppRevisionsFetcher } let!(:app) { AppModel.make } - let!(:revision1) { RevisionModel.make(version: 21, app: app) } - let!(:revision2) { RevisionModel.make(version: 34, app: app) } + + let(:expired_droplet) { DropletModel.make(:droplet, app: app, state: DropletModel::EXPIRED_STATE) } + let(:staged_droplet) { DropletModel.make(:droplet, app: app, state: DropletModel::STAGED_STATE) } + + let!(:revision1) { RevisionModel.make(version: 21, droplet_guid: staged_droplet.guid, app: app) } + let!(:revision2) { RevisionModel.make(version: 34, droplet_guid: expired_droplet.guid, app: app) } describe '#fetch' do let(:message) { AppRevisionsListMessage.from_params(filters) } @@ -20,7 +24,7 @@ module VCAP::CloudController end end - context 'when the revisions are filtered' do + context 'when the revisions are filtered on version' do let(:filters) { { versions: [revision1.version] } } it 'returns all of the desired revisions' do @@ -29,6 +33,15 @@ module VCAP::CloudController end end + context 'when the revisions are filtered on deployable' do + let(:filters) { { deployable: true } } + + it 'returns all of the desired revisions' do + expect(subject).to include(revision1) + expect(subject).to_not include(revision2) + end + end + context 'when a label_selector is provided' do let(:message) { AppRevisionsListMessage.from_params({ 'label_selector' => 'key=value' }) } let!(:revision1label) { RevisionLabelModel.make(key_name: 'key', value: 'value', revision: revision1) } diff --git a/spec/unit/messages/app_revisions_list_message_spec.rb b/spec/unit/messages/app_revisions_list_message_spec.rb index 9b2c1136cab..6bab855a7f6 100644 --- a/spec/unit/messages/app_revisions_list_message_spec.rb +++ b/spec/unit/messages/app_revisions_list_message_spec.rb @@ -10,6 +10,7 @@ module VCAP::CloudController 'page' => 1, 'per_page' => 5, 'label_selector' => 'key=value', + 'deployable' => true } end @@ -20,6 +21,7 @@ module VCAP::CloudController expect(message.page).to eq(1) expect(message.per_page).to eq(5) expect(message.versions).to eq(['1', '3']) + expect(message.deployable).to eq(true) expect(message.label_selector).to eq('key=value') end @@ -29,6 +31,7 @@ module VCAP::CloudController expect(message.requested?(:page)).to be_truthy expect(message.requested?(:per_page)).to be_truthy expect(message.requested?(:versions)).to be_truthy + expect(message.requested?(:deployable)).to be_truthy expect(message.requested?(:label_selector)).to be_truthy end end @@ -55,7 +58,8 @@ module VCAP::CloudController AppRevisionsListMessage.from_params({ page: 1, per_page: 5, - versions: ['1'], + versions: ['1'], + deployable: true, label_selector: 'key=value', }) }.not_to raise_error @@ -88,6 +92,19 @@ module VCAP::CloudController end end + context 'deployable' do + it 'validates deployable to be a boolean' do + message = AppRevisionsListMessage.from_params(deployable: 'not a boolean') + expect(message).to be_invalid + expect(message.errors[:deployable]).to include('must be a boolean') + end + + it 'allows deployable to be nil' do + message = AppRevisionsListMessage.from_params(deployable: nil) + expect(message).to be_valid + end + end + it 'validates metadata requirements' do message = AppRevisionsListMessage.from_params('label_selector' => '') diff --git a/spec/unit/presenters/v3/revision_presenter_spec.rb b/spec/unit/presenters/v3/revision_presenter_spec.rb index cb00d7ee530..da3d461c955 100644 --- a/spec/unit/presenters/v3/revision_presenter_spec.rb +++ b/spec/unit/presenters/v3/revision_presenter_spec.rb @@ -96,6 +96,24 @@ module VCAP::CloudController::Presenters::V3 expect(result[:sidecars][0][:memory_in_mb]).to eq(300) expect(result[:sidecars][0][:process_types]).to eq(['web']) expect(result[:description]).to eq('Initial revision') + expect(result[:deployable]).to eq(true) + end + + context 'when the droplet is not staged' do + let(:droplet) do + VCAP::CloudController::DropletModel.make( + app: app_model, + state: VCAP::CloudController::DropletModel::EXPIRED_STATE, + process_types: { + 'web' => 'droplet_web_command', + 'worker' => 'droplet_worker_command', + }) + end + + it 'returns deployable is false' do + result = RevisionPresenter.new(revision).to_hash + expect(result[:deployable]).to eq(false) + end end end end