diff --git a/app/actions/app_apply_manifest.rb b/app/actions/app_apply_manifest.rb
index 3c41074b104..756ad73f8c4 100644
--- a/app/actions/app_apply_manifest.rb
+++ b/app/actions/app_apply_manifest.rb
@@ -59,6 +59,7 @@ def apply(app_guid, message)
app_update_message = message.app_update_message
lifecycle = AppLifecycleProvider.provide_for_update(app_update_message, app)
+
AppUpdate.new(@user_audit_info, manifest_triggered: true).update(app, app_update_message, lifecycle)
update_routes(app, message)
diff --git a/app/controllers/v3/buildpacks_controller.rb b/app/controllers/v3/buildpacks_controller.rb
index 28386823917..2916970fa59 100644
--- a/app/controllers/v3/buildpacks_controller.rb
+++ b/app/controllers/v3/buildpacks_controller.rb
@@ -77,7 +77,7 @@ def upload
unauthorized! unless permission_queryer.can_write_globally?
- message = BuildpackUploadMessage.create_from_params(hashed_params[:body])
+ message = BuildpackUploadMessage.create_from_params(hashed_params[:body], buildpack.lifecycle)
combine_messages(message.errors.full_messages) unless message.valid?
unprocessable!('Buildpack is locked') if buildpack.locked
diff --git a/app/controllers/v3/space_manifests_controller.rb b/app/controllers/v3/space_manifests_controller.rb
index 3a001763d4d..9e8f8cd91ae 100644
--- a/app/controllers/v3/space_manifests_controller.rb
+++ b/app/controllers/v3/space_manifests_controller.rb
@@ -16,6 +16,7 @@ def apply_manifest
can_write_space(space)
messages = parsed_app_manifests.map { |app_manifest| AppManifestMessage.create_from_yml(app_manifest) }
+
errors = messages.each_with_index.flat_map { |message, i| errors_for_message(message, i) }
compound_error!(errors) unless errors.empty?
diff --git a/app/fetchers/buildpack_list_fetcher.rb b/app/fetchers/buildpack_list_fetcher.rb
index 1ae20e8052c..1c3713b269c 100644
--- a/app/fetchers/buildpack_list_fetcher.rb
+++ b/app/fetchers/buildpack_list_fetcher.rb
@@ -19,7 +19,8 @@ def filter(message, dataset)
dataset = dataset.where(name: message.names) if message.requested?(:names)
dataset = NullFilterQueryGenerator.add_filter(dataset, :stack, message.stacks) if message.requested?(:stacks)
- dataset = dataset.where(lifecycle: message.requested?(:lifecycle) ? message.lifecycle : Config.config.get(:default_app_lifecycle))
+
+ dataset = dataset.where(lifecycle: message.lifecycle) if message.requested?(:lifecycle)
if message.requested?(:label_selector)
dataset = LabelSelectorQueryGenerator.add_selector_queries(
diff --git a/app/messages/app_manifest_message.rb b/app/messages/app_manifest_message.rb
index ee4757fd13f..36faa3f0e88 100644
--- a/app/messages/app_manifest_message.rb
+++ b/app/messages/app_manifest_message.rb
@@ -123,16 +123,32 @@ def audit_hash
end
def app_lifecycle_hash
- lifecycle_data = if requested?(:lifecycle) && @lifecycle == 'cnb'
- cnb_lifecycle_data
+ lifecycle_type = if requested?(:lifecycle) && @lifecycle == 'cnb'
+ Lifecycles::CNB
+ elsif requested?(:lifecycle) && @lifecycle == 'buildpack'
+ Lifecycles::BUILDPACK
elsif requested?(:docker)
- docker_lifecycle_data
- elsif requested?(:buildpacks) || requested?(:buildpack) || requested?(:stack)
- buildpacks_lifecycle_data
+ Lifecycles::DOCKER
end
+ data = {
+ buildpacks: requested_buildpacks,
+ stack: @stack,
+ credentials: @cnb_credentials
+ }.compact
+
+ if lifecycle_type == Lifecycles::DOCKER
+ lifecycle = {
+ type: Lifecycles::DOCKER
+ }
+ elsif lifecycle_type || data.present?
+ lifecycle = {}
+ lifecycle[:type] = lifecycle_type if lifecycle_type.present?
+ lifecycle[:data] = data if data.present?
+ end
+
{
- lifecycle: lifecycle_data,
+ lifecycle: lifecycle,
metadata: requested?(:metadata) ? metadata : nil
}.compact
end
@@ -279,31 +295,6 @@ def service_bindings_attribute_mapping
mapping
end
- def docker_lifecycle_data
- { type: Lifecycles::DOCKER }
- end
-
- def cnb_lifecycle_data
- {
- type: Lifecycles::CNB,
- data: {
- buildpacks: requested_buildpacks,
- stack: @stack,
- credentials: @cnb_credentials
- }.compact
- }
- end
-
- def buildpacks_lifecycle_data
- {
- type: Lifecycles::BUILDPACK,
- data: {
- buildpacks: requested_buildpacks,
- stack: @stack
- }.compact
- }
- end
-
def should_autodetect?(buildpack)
buildpack == 'default' || buildpack == 'null' || buildpack.nil?
end
diff --git a/app/messages/buildpack_upload_message.rb b/app/messages/buildpack_upload_message.rb
index f0ebe0b1da0..0742ea88102 100644
--- a/app/messages/buildpack_upload_message.rb
+++ b/app/messages/buildpack_upload_message.rb
@@ -3,6 +3,7 @@
module VCAP::CloudController
GZIP_MIME = Regexp.new("\x1F\x8B\x08".force_encoding('binary'))
ZIP_MIME = Regexp.new("PK\x03\x04".force_encoding('binary'))
+ CNB_MIME = Regexp.new("\x75\x73\x74\x61\x72\x00\x30\x30".force_encoding('binary'))
class BuildpackUploadMessage < BaseMessage
class MissingFilePathError < StandardError; end
@@ -16,8 +17,15 @@ class MissingFilePathError < StandardError; end
validate :is_not_empty
validate :missing_file_path
- def self.create_from_params(params)
- BuildpackUploadMessage.new(params.dup.symbolize_keys)
+ attr_reader :lifecycle
+
+ def initialize(params, lifecycle)
+ @lifecycle = lifecycle
+ super(params)
+ end
+
+ def self.create_from_params(params, lifecycle)
+ BuildpackUploadMessage.new(params.dup.symbolize_keys, lifecycle)
end
def nginx_fields
@@ -51,9 +59,18 @@ def is_archive
mime_bits = File.read(bits_path, 4)
- return if mime_bits =~ /^#{VCAP::CloudController::GZIP_MIME}/ || mime_bits =~ /^#{VCAP::CloudController::ZIP_MIME}/
+ if lifecycle == VCAP::CloudController::Lifecycles::BUILDPACK
+ return if mime_bits =~ /^#{VCAP::CloudController::ZIP_MIME}/
+
+ errors.add(:base, "#{bits_name} is not a zip file. Buildpacks of lifecycle \"#{lifecycle}\" must be valid zip files.")
+ elsif lifecycle == VCAP::CloudController::Lifecycles::CNB
+ return if mime_bits =~ /^#{VCAP::CloudController::GZIP_MIME}/
+
+ mime_bits_at_offset = File.read(bits_path, 8, 257)
+ return if mime_bits_at_offset =~ /^#{VCAP::CloudController::CNB_MIME}/
- errors.add(:base, "#{bits_name} is not a zip or gzip archive")
+ errors.add(:base, "#{bits_name} is not a gzip archive or cnb file. Buildpacks of lifecycle \"#{lifecycle}\" must be valid gzip archives or cnb files.")
+ end
end
def missing_file_path
diff --git a/app/messages/buildpacks_list_message.rb b/app/messages/buildpacks_list_message.rb
index b0ba8cb5a64..293ec500248 100644
--- a/app/messages/buildpacks_list_message.rb
+++ b/app/messages/buildpacks_list_message.rb
@@ -21,7 +21,8 @@ class BuildpacksListMessage < MetadataListMessage
def initialize(params={})
super
- pagination_options.default_order_by = :position
+ pagination_options.default_order_by = :lifecycle
+ pagination_options.secondary_default_order_by = :position
end
def self.from_params(params)
@@ -33,7 +34,7 @@ def to_param_hash
end
def valid_order_by_values
- super << :position
+ super + %i[position lifecycle]
end
end
diff --git a/docs/v3/source/includes/api_resources/_buildpacks.erb b/docs/v3/source/includes/api_resources/_buildpacks.erb
index c2586026f10..2caea659bf6 100644
--- a/docs/v3/source/includes/api_resources/_buildpacks.erb
+++ b/docs/v3/source/includes/api_resources/_buildpacks.erb
@@ -8,6 +8,7 @@
"filename": null,
"stack": "windows64",
"position": 42,
+ "lifecycle": "buildpack",
"enabled": true,
"locked": false,
"metadata": {
@@ -52,6 +53,7 @@
"filename": null,
"stack": "my-stack",
"position": 1,
+ "lifecycle": "cnb",
"enabled": true,
"locked": false,
"metadata": {
diff --git a/docs/v3/source/includes/resources/apps/_update.md.erb b/docs/v3/source/includes/resources/apps/_update.md.erb
index d28292408c9..4c28f1a6d55 100644
--- a/docs/v3/source/includes/resources/apps/_update.md.erb
+++ b/docs/v3/source/includes/resources/apps/_update.md.erb
@@ -39,7 +39,7 @@ Content-Type: application/json
Name | Type | Description
---- | ---- | -----------
**name** | _string_ | Name of the app
-**lifecycle** | [_lifecycle object_](#the-lifecycle-object) | Lifecycle to be used when updating the app; note: `data` is a required field in lifecycle if lifecycle is updated
+**lifecycle** | [_lifecycle object_](#the-lifecycle-object) | Lifecycle to be used when updating the app; note: `data` is a required field in lifecycle if lifecycle is updated. `type` may NOT be changed from its current value.
**metadata.labels** | [_label object_](#labels) | Labels applied to the app
**metadata.annotations** | [_annotation object_](#annotations) | Annotations applied to the app
diff --git a/docs/v3/source/includes/resources/buildpacks/_create.md.erb b/docs/v3/source/includes/resources/buildpacks/_create.md.erb
index 00e818e08e3..dbe4eb8907e 100644
--- a/docs/v3/source/includes/resources/buildpacks/_create.md.erb
+++ b/docs/v3/source/includes/resources/buildpacks/_create.md.erb
@@ -44,6 +44,7 @@ Name | Type | Description
----------- | -------- | ----------------------------------------------------------------------------- | -------
**stack** | _string_ | The name of the stack that the buildpack will use | null
**position** | _integer_ | The order in which the buildpacks are checked during buildpack auto-detection | 1
+**lifecycle**| _string_ | The version of buildpack the buildpack will use. `buildpack` indicates [Classic Buildpacks](https://docs.cloudfoundry.org/buildpacks/classic.html). `cnb` indicates [Cloud Native Buildpacks](https://docs.cloudfoundry.org/buildpacks/cnb/) | buildpack
**enabled** | _boolean_ | Whether or not the buildpack will be used for staging | true
**locked** | _boolean_ | Whether or not the buildpack is locked to prevent updating the bits | false
**metadata.labels** | [_label object_](#labels) | Labels applied to the buildpack
diff --git a/docs/v3/source/includes/resources/buildpacks/_list.md.erb b/docs/v3/source/includes/resources/buildpacks/_list.md.erb
index bfad835b683..3fe885d0478 100644
--- a/docs/v3/source/includes/resources/buildpacks/_list.md.erb
+++ b/docs/v3/source/includes/resources/buildpacks/_list.md.erb
@@ -34,7 +34,8 @@ Name | Type | Description
**per_page** | _integer_ | Number of results per page;
valid values are 1 through 5000
**names** | _list of strings_ | Comma-delimited list of buildpack names to filter by
**stacks**| _list of strings_ | Comma-delimited list of stack names to filter by
-**order_by** | _string_ | Value to sort by; defaults to ascending. Prepend with `-` to sort descending.
Valid values are `created_at`, `updated_at`, and `position`
+**lifecycle**| _string_ | Type of buildpack. Valid values are `buildpack` and `cnb`
+**order_by** | _string_ | Value to sort by; defaults to ascending. Prepend with `-` to sort descending.
Valid values are `created_at`, `updated_at`, `lifecycle`, and `position`
**label_selector** | _string_ | A query string containing a list of [label selector](#labels-and-selectors) requirements
**created_ats** | _[timestamp](#timestamps)_ | Timestamp to filter by. When filtering on equality, several comma-delimited timestamps may be passed. Also supports filtering with [relational operators](#relational-operators)
**updated_ats** | _[timestamp](#timestamps)_ | Timestamp to filter by. When filtering on equality, several comma-delimited timestamps may be passed. Also supports filtering with [relational operators](#relational-operators)
diff --git a/docs/v3/source/includes/resources/buildpacks/_object.md.erb b/docs/v3/source/includes/resources/buildpacks/_object.md.erb
index 462f3b4a778..aa8918501f7 100644
--- a/docs/v3/source/includes/resources/buildpacks/_object.md.erb
+++ b/docs/v3/source/includes/resources/buildpacks/_object.md.erb
@@ -15,7 +15,8 @@ Name | Type | Description
**updated_at** | _[timestamp](#timestamps)_ | The time with zone when the object was last updated
**name** | _string_ | The name of the buildpack; to be used by app buildpack field (only alphanumeric characters)
**state** | _string_ | The state of the buildpack; valid states are: `AWAITING_UPLOAD`, `READY`
-**stack** | _string_ | The name of the stack that the buildpack will use
+**stack** | _string_ | The name of the stack that the buildpack uses
+**lifecycle** | _string_ | The version of buildpacks the buildpack uses. `buildpack` indicates [Classic Buildpacks](https://docs.cloudfoundry.org/buildpacks/classic.html). `cnb` indicates [Cloud Native Buildpacks](https://docs.cloudfoundry.org/buildpacks/cnb/)
**filename** | _string_ | The filename of the buildpack
**position** | _integer_ | The order in which the buildpacks are checked during buildpack auto-detection
**enabled** | _boolean_ | Whether or not the buildpack can be used for staging
diff --git a/lib/cloud_controller/install_buildpacks.rb b/lib/cloud_controller/install_buildpacks.rb
index 1dbd8cb8c50..fa356d2a051 100644
--- a/lib/cloud_controller/install_buildpacks.rb
+++ b/lib/cloud_controller/install_buildpacks.rb
@@ -75,7 +75,7 @@ def detected_stack(file, opts)
def buildpack_zip(package, zipfile)
return zipfile if zipfile
- job_dir = File.join('/var/vcap/packages', package, '*.zip')
+ job_dir = File.join('/var/vcap/packages', package, '*[.zip|.cnb|.tgz|.tar.gz]')
Dir[job_dir].first
end
diff --git a/lib/cloud_controller/paging/pagination_options.rb b/lib/cloud_controller/paging/pagination_options.rb
index 0555db155dc..795ac55617c 100644
--- a/lib/cloud_controller/paging/pagination_options.rb
+++ b/lib/cloud_controller/paging/pagination_options.rb
@@ -12,7 +12,7 @@ class PaginationOptions
DIRECTION_DEFAULT = 'asc'.freeze
VALID_DIRECTIONS = %w[asc desc].freeze
- attr_writer :order_by, :order_direction, :default_order_by
+ attr_writer :order_by, :order_direction, :default_order_by, :secondary_default_order_by
attr_accessor :page, :per_page
def initialize(params)
@@ -38,6 +38,12 @@ def order_direction
@order_direction || DIRECTION_DEFAULT
end
+ def secondary_order_by
+ return if @order_by && @order_by.to_s != default_order_by.to_s
+
+ @secondary_default_order_by
+ end
+
def keys
%i[page per_page order_by order_direction]
end
diff --git a/lib/cloud_controller/paging/sequel_paginator.rb b/lib/cloud_controller/paging/sequel_paginator.rb
index 46d8acf933c..c104b0de08b 100644
--- a/lib/cloud_controller/paging/sequel_paginator.rb
+++ b/lib/cloud_controller/paging/sequel_paginator.rb
@@ -13,6 +13,8 @@ def get_page(dataset, pagination_options)
order_type = Sequel.send(order_direction, Sequel.qualify(table_name, order_by))
dataset = dataset.order(order_type)
+ secondary_order_by = pagination_options.secondary_order_by
+ dataset = dataset.order_append(Sequel.send(order_direction, Sequel.qualify(table_name, secondary_order_by))) if secondary_order_by
dataset = dataset.order_append(Sequel.send(order_direction, Sequel.qualify(table_name, :guid))) if order_by != 'id' && has_guid_column
distinct_opt = dataset.opts[:distinct]
diff --git a/spec/request/buildpacks_spec.rb b/spec/request/buildpacks_spec.rb
index 991d70e6e46..81b07353d88 100644
--- a/spec/request/buildpacks_spec.rb
+++ b/spec/request/buildpacks_spec.rb
@@ -93,24 +93,26 @@
let!(:stack2) { VCAP::CloudController::Stack.make }
let!(:stack3) { VCAP::CloudController::Stack.make }
+ let!(:buildpack4) { VCAP::CloudController::Buildpack.make(stack: stack1.name, position: 2, lifecycle: 'cnb') }
+ let!(:buildpack5) { VCAP::CloudController::Buildpack.make(stack: stack1.name, position: 1, lifecycle: 'cnb') }
+
let!(:buildpack1) { VCAP::CloudController::Buildpack.make(stack: stack1.name, position: 1) }
let!(:buildpack2) { VCAP::CloudController::Buildpack.make(stack: stack2.name, position: 3) }
let!(:buildpack3) { VCAP::CloudController::Buildpack.make(stack: stack3.name, position: 2) }
- let!(:buildpack4) { VCAP::CloudController::Buildpack.make(stack: stack1.name, position: 1, lifecycle: 'cnb') }
- it 'returns a paginated list of buildpacks, sorted by position' do
+ it 'returns a paginated list of buildpacks, sorted by lifecycle and position' do
get '/v3/buildpacks?page=1&per_page=2', nil, headers
expect(parsed_response).to be_a_response_like(
{
'pagination' => {
- 'total_results' => 3,
- 'total_pages' => 2,
+ 'total_results' => 5,
+ 'total_pages' => 3,
'first' => {
'href' => "#{link_prefix}/v3/buildpacks?page=1&per_page=2"
},
'last' => {
- 'href' => "#{link_prefix}/v3/buildpacks?page=2&per_page=2"
+ 'href' => "#{link_prefix}/v3/buildpacks?page=3&per_page=2"
},
'next' => {
'href' => "#{link_prefix}/v3/buildpacks?page=2&per_page=2"
@@ -169,6 +171,144 @@
)
end
+ it 'with no filters, returns a list of buildpacks, sorted by lifecycle and position' do
+ get '/v3/buildpacks', nil, headers
+
+ expect(parsed_response).to be_a_response_like(
+ {
+ 'pagination' => {
+ 'total_results' => 5,
+ 'total_pages' => 1,
+ 'first' => {
+ 'href' => "#{link_prefix}/v3/buildpacks?page=1&per_page=50"
+ },
+ 'last' => {
+ 'href' => "#{link_prefix}/v3/buildpacks?page=1&per_page=50"
+ },
+ 'next' => nil,
+ 'previous' => nil
+ },
+ 'resources' => [
+ {
+ 'guid' => buildpack1.guid,
+ 'lifecycle' => 'buildpack',
+ 'created_at' => iso8601,
+ 'updated_at' => iso8601,
+ 'name' => buildpack1.name,
+ 'state' => buildpack1.state,
+ 'filename' => buildpack1.filename,
+ 'stack' => buildpack1.stack,
+ 'position' => 1,
+ 'enabled' => true,
+ 'locked' => false,
+ 'metadata' => { 'labels' => {}, 'annotations' => {} },
+ 'links' => {
+ 'self' => {
+ 'href' => "#{link_prefix}/v3/buildpacks/#{buildpack1.guid}"
+ },
+ 'upload' => {
+ 'href' => "#{link_prefix}/v3/buildpacks/#{buildpack1.guid}/upload",
+ 'method' => 'POST'
+ }
+ }
+ },
+ {
+ 'guid' => buildpack3.guid,
+ 'lifecycle' => 'buildpack',
+ 'created_at' => iso8601,
+ 'updated_at' => iso8601,
+ 'name' => buildpack3.name,
+ 'state' => buildpack3.state,
+ 'filename' => buildpack3.filename,
+ 'stack' => buildpack3.stack,
+ 'position' => 2,
+ 'enabled' => true,
+ 'locked' => false,
+ 'metadata' => { 'labels' => {}, 'annotations' => {} },
+ 'links' => {
+ 'self' => {
+ 'href' => "#{link_prefix}/v3/buildpacks/#{buildpack3.guid}"
+ },
+ 'upload' => {
+ 'href' => "#{link_prefix}/v3/buildpacks/#{buildpack3.guid}/upload",
+ 'method' => 'POST'
+ }
+ }
+ },
+ {
+ 'guid' => buildpack2.guid,
+ 'lifecycle' => 'buildpack',
+ 'created_at' => iso8601,
+ 'updated_at' => iso8601,
+ 'name' => buildpack2.name,
+ 'state' => buildpack2.state,
+ 'filename' => buildpack2.filename,
+ 'stack' => buildpack2.stack,
+ 'position' => 3,
+ 'enabled' => true,
+ 'locked' => false,
+ 'metadata' => { 'labels' => {}, 'annotations' => {} },
+ 'links' => {
+ 'self' => {
+ 'href' => "#{link_prefix}/v3/buildpacks/#{buildpack2.guid}"
+ },
+ 'upload' => {
+ 'href' => "#{link_prefix}/v3/buildpacks/#{buildpack2.guid}/upload",
+ 'method' => 'POST'
+ }
+ }
+ },
+ {
+ 'guid' => buildpack5.guid,
+ 'lifecycle' => 'cnb',
+ 'created_at' => iso8601,
+ 'updated_at' => iso8601,
+ 'name' => buildpack5.name,
+ 'state' => buildpack5.state,
+ 'filename' => buildpack5.filename,
+ 'stack' => buildpack5.stack,
+ 'position' => 1,
+ 'enabled' => true,
+ 'locked' => false,
+ 'metadata' => { 'labels' => {}, 'annotations' => {} },
+ 'links' => {
+ 'self' => {
+ 'href' => "#{link_prefix}/v3/buildpacks/#{buildpack5.guid}"
+ },
+ 'upload' => {
+ 'href' => "#{link_prefix}/v3/buildpacks/#{buildpack5.guid}/upload",
+ 'method' => 'POST'
+ }
+ }
+ },
+ {
+ 'guid' => buildpack4.guid,
+ 'lifecycle' => 'cnb',
+ 'created_at' => iso8601,
+ 'updated_at' => iso8601,
+ 'name' => buildpack4.name,
+ 'state' => buildpack4.state,
+ 'filename' => buildpack4.filename,
+ 'stack' => buildpack4.stack,
+ 'position' => 2,
+ 'enabled' => true,
+ 'locked' => false,
+ 'metadata' => { 'labels' => {}, 'annotations' => {} },
+ 'links' => {
+ 'self' => {
+ 'href' => "#{link_prefix}/v3/buildpacks/#{buildpack4.guid}"
+ },
+ 'upload' => {
+ 'href' => "#{link_prefix}/v3/buildpacks/#{buildpack4.guid}/upload",
+ 'method' => 'POST'
+ }
+ }
+ }
+ ]
+ }
+ )
+ end
+
it 'returns a list of filtered buildpacks' do
get "/v3/buildpacks?names=#{buildpack1.name},#{buildpack3.name}&stacks=#{stack1.name}", nil, headers
@@ -215,13 +355,84 @@
)
end
+ it 'returns a paginated list of buildpacks filtered by lifecycle' do
+ get '/v3/buildpacks?lifecycle=buildpack&per_page=2', nil, headers
+
+ expect(parsed_response).to be_a_response_like(
+ {
+ 'pagination' => {
+ 'total_results' => 3,
+ 'total_pages' => 2,
+ 'first' => {
+ 'href' => "#{link_prefix}/v3/buildpacks?lifecycle=buildpack&page=1&per_page=2"
+ },
+ 'last' => {
+ 'href' => "#{link_prefix}/v3/buildpacks?lifecycle=buildpack&page=2&per_page=2"
+ },
+ 'next' => {
+ 'href' => "#{link_prefix}/v3/buildpacks?lifecycle=buildpack&page=2&per_page=2"
+ },
+ 'previous' => nil
+ },
+ 'resources' => [
+ {
+ 'guid' => buildpack1.guid,
+ 'lifecycle' => 'buildpack',
+ 'created_at' => iso8601,
+ 'updated_at' => iso8601,
+ 'name' => buildpack1.name,
+ 'state' => buildpack1.state,
+ 'filename' => buildpack1.filename,
+ 'stack' => buildpack1.stack,
+ 'position' => 1,
+ 'enabled' => true,
+ 'locked' => false,
+ 'metadata' => { 'labels' => {}, 'annotations' => {} },
+ 'links' => {
+ 'self' => {
+ 'href' => "#{link_prefix}/v3/buildpacks/#{buildpack1.guid}"
+ },
+ 'upload' => {
+ 'href' => "#{link_prefix}/v3/buildpacks/#{buildpack1.guid}/upload",
+ 'method' => 'POST'
+ }
+ }
+ },
+ {
+ 'guid' => buildpack3.guid,
+ 'lifecycle' => 'buildpack',
+ 'created_at' => iso8601,
+ 'updated_at' => iso8601,
+ 'name' => buildpack3.name,
+ 'state' => buildpack3.state,
+ 'filename' => buildpack3.filename,
+ 'stack' => buildpack3.stack,
+ 'position' => 2,
+ 'enabled' => true,
+ 'locked' => false,
+ 'metadata' => { 'labels' => {}, 'annotations' => {} },
+ 'links' => {
+ 'self' => {
+ 'href' => "#{link_prefix}/v3/buildpacks/#{buildpack3.guid}"
+ },
+ 'upload' => {
+ 'href' => "#{link_prefix}/v3/buildpacks/#{buildpack3.guid}/upload",
+ 'method' => 'POST'
+ }
+ }
+ }
+ ]
+ }
+ )
+ end
+
it 'returns a list of buildpacks filtered by lifecycle' do
get '/v3/buildpacks?lifecycle=cnb', nil, headers
expect(parsed_response).to be_a_response_like(
{
'pagination' => {
- 'total_results' => 1,
+ 'total_results' => 2,
'total_pages' => 1,
'first' => {
'href' => "#{link_prefix}/v3/buildpacks?lifecycle=cnb&page=1&per_page=50"
@@ -233,6 +444,29 @@
'previous' => nil
},
'resources' => [
+ {
+ 'guid' => buildpack5.guid,
+ 'lifecycle' => 'cnb',
+ 'created_at' => iso8601,
+ 'updated_at' => iso8601,
+ 'name' => buildpack5.name,
+ 'state' => buildpack5.state,
+ 'filename' => buildpack5.filename,
+ 'stack' => buildpack5.stack,
+ 'position' => 1,
+ 'enabled' => true,
+ 'locked' => false,
+ 'metadata' => { 'labels' => {}, 'annotations' => {} },
+ 'links' => {
+ 'self' => {
+ 'href' => "#{link_prefix}/v3/buildpacks/#{buildpack5.guid}"
+ },
+ 'upload' => {
+ 'href' => "#{link_prefix}/v3/buildpacks/#{buildpack5.guid}/upload",
+ 'method' => 'POST'
+ }
+ }
+ },
{
'guid' => buildpack4.guid,
'lifecycle' => 'cnb',
@@ -242,7 +476,7 @@
'state' => buildpack4.state,
'filename' => buildpack4.filename,
'stack' => buildpack4.stack,
- 'position' => 1,
+ 'position' => 2,
'enabled' => true,
'locked' => false,
'metadata' => { 'labels' => {}, 'annotations' => {} },
@@ -329,6 +563,145 @@
}
)
end
+
+ it 'reverse orders by lifecycle (with position as secondary)' do
+ get '/v3/buildpacks?order_by=-lifecycle', nil, headers
+
+ expect(parsed_response).to(be_a_response_like(
+ {
+ 'pagination' => {
+ 'total_results' => 5,
+ 'total_pages' => 1,
+ 'first' => {
+ 'href' => "#{link_prefix}/v3/buildpacks?order_by=-lifecycle&page=1&per_page=50"
+ },
+ 'last' => {
+ 'href' => "#{link_prefix}/v3/buildpacks?order_by=-lifecycle&page=1&per_page=50"
+ },
+ 'next' => nil,
+ 'previous' => nil
+ },
+ 'resources' => [
+ {
+ 'guid' => buildpack4.guid,
+ 'lifecycle' => 'cnb',
+ 'created_at' => iso8601,
+ 'updated_at' => iso8601,
+ 'name' => buildpack4.name,
+ 'state' => buildpack4.state,
+ 'filename' => buildpack4.filename,
+ 'stack' => buildpack4.stack,
+ 'position' => 2,
+ 'enabled' => true,
+ 'locked' => false,
+ 'metadata' => { 'labels' => {}, 'annotations' => {} },
+ 'links' => {
+ 'self' => {
+ 'href' => "#{link_prefix}/v3/buildpacks/#{buildpack4.guid}"
+ },
+ 'upload' => {
+ 'href' => "#{link_prefix}/v3/buildpacks/#{buildpack4.guid}/upload",
+ 'method' => 'POST'
+ }
+ }
+ },
+ {
+ 'guid' => buildpack5.guid,
+ 'lifecycle' => 'cnb',
+ 'created_at' => iso8601,
+ 'updated_at' => iso8601,
+ 'name' => buildpack5.name,
+ 'state' => buildpack5.state,
+ 'filename' => buildpack5.filename,
+ 'stack' => buildpack5.stack,
+ 'position' => 1,
+ 'enabled' => true,
+ 'locked' => false,
+ 'metadata' => { 'labels' => {}, 'annotations' => {} },
+ 'links' => {
+ 'self' => {
+ 'href' => "#{link_prefix}/v3/buildpacks/#{buildpack5.guid}"
+ },
+ 'upload' => {
+ 'href' => "#{link_prefix}/v3/buildpacks/#{buildpack5.guid}/upload",
+ 'method' => 'POST'
+ }
+ }
+ },
+ {
+ 'guid' => buildpack2.guid,
+ 'lifecycle' => 'buildpack',
+ 'created_at' => iso8601,
+ 'updated_at' => iso8601,
+ 'name' => buildpack2.name,
+ 'state' => buildpack2.state,
+ 'filename' => buildpack2.filename,
+ 'stack' => buildpack2.stack,
+ 'position' => 3,
+ 'enabled' => true,
+ 'locked' => false,
+ 'metadata' => { 'labels' => {}, 'annotations' => {} },
+ 'links' => {
+ 'self' => {
+ 'href' => "#{link_prefix}/v3/buildpacks/#{buildpack2.guid}"
+ },
+ 'upload' => {
+ 'href' => "#{link_prefix}/v3/buildpacks/#{buildpack2.guid}/upload",
+ 'method' => 'POST'
+ }
+ }
+ },
+ {
+ 'guid' => buildpack3.guid,
+ 'lifecycle' => 'buildpack',
+ 'created_at' => iso8601,
+ 'updated_at' => iso8601,
+ 'name' => buildpack3.name,
+ 'state' => buildpack3.state,
+ 'filename' => buildpack3.filename,
+ 'stack' => buildpack3.stack,
+ 'position' => 2,
+ 'enabled' => true,
+ 'locked' => false,
+ 'metadata' => { 'labels' => {}, 'annotations' => {} },
+ 'links' => {
+ 'self' => {
+ 'href' => "#{link_prefix}/v3/buildpacks/#{buildpack3.guid}"
+ },
+ 'upload' => {
+ 'href' => "#{link_prefix}/v3/buildpacks/#{buildpack3.guid}/upload",
+ 'method' => 'POST'
+ }
+ }
+ },
+ {
+ 'guid' => buildpack1.guid,
+ 'lifecycle' => 'buildpack',
+ 'created_at' => iso8601,
+ 'updated_at' => iso8601,
+ 'name' => buildpack1.name,
+ 'state' => buildpack1.state,
+ 'filename' => buildpack1.filename,
+ 'stack' => buildpack1.stack,
+ 'position' => 1,
+ 'enabled' => true,
+ 'locked' => false,
+ 'metadata' => { 'labels' => {}, 'annotations' => {} },
+ 'links' => {
+ 'self' => {
+ 'href' => "#{link_prefix}/v3/buildpacks/#{buildpack1.guid}"
+ },
+ 'upload' => {
+ 'href' => "#{link_prefix}/v3/buildpacks/#{buildpack1.guid}/upload",
+ 'method' => 'POST'
+ }
+ }
+ }
+
+ ]
+ }
+ ))
+ end
end
context 'permissions' do
diff --git a/spec/support/test_cnb.rb b/spec/support/test_cnb.rb
new file mode 100644
index 00000000000..b17a5c35321
--- /dev/null
+++ b/spec/support/test_cnb.rb
@@ -0,0 +1,16 @@
+require 'zlib'
+require 'rubygems/package'
+
+module TestCnb
+ def self.create(name, file_count, file_size=1024, &)
+ File.open(name, 'wb') do |file|
+ Gem::Package::TarWriter.new(file) do |tar|
+ file_count.times do |i|
+ tar.add_file_simple("test_#{i}", 0o644, file_size) do |f|
+ f.write('A' * file_size)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/unit/actions/app_apply_manifest_spec.rb b/spec/unit/actions/app_apply_manifest_spec.rb
index f0761d7cfab..5dcb67fc7cb 100644
--- a/spec/unit/actions/app_apply_manifest_spec.rb
+++ b/spec/unit/actions/app_apply_manifest_spec.rb
@@ -147,6 +147,98 @@ module VCAP::CloudController
end
end
+ context 'cnb apps' do
+ let(:buildpack) { VCAP::CloudController::Buildpack.make }
+ let(:message) { AppManifestMessage.create_from_yml({ name: 'blah' }) }
+ let(:app_update_message) { message.app_update_message }
+ let(:app) { AppModel.make(:cnb) }
+
+ before do
+ TestConfig.override(default_app_lifecycle: 'cnb')
+ end
+
+ context 'when the default_app_lifecycle is set and the the lifecycle is not specified' do
+ it 'preserves the lifecycle' do
+ app_apply_manifest.apply(app.guid, message)
+ expect(AppUpdate).to have_received(:new).with(user_audit_info, manifest_triggered: true)
+ expect(app_update).to have_received(:update).
+ with(app, app_update_message, instance_of(AppCNBLifecycle))
+ expect(app.reload.lifecycle_type).to eq('cnb')
+ end
+ end
+
+ context 'when buildpack is specified' do
+ let(:message) { AppManifestMessage.create_from_yml({ name: 'blah', buildpack: buildpack.name }) }
+
+ it 'preserves the lifecycle' do
+ app_apply_manifest.apply(app.guid, message)
+ expect(AppUpdate).to have_received(:new).with(user_audit_info, manifest_triggered: true)
+ expect(app_update).to have_received(:update).
+ with(app, app_update_message, instance_of(AppCNBLifecycle))
+ expect(app.reload.lifecycle_type).to eq('cnb')
+ end
+ end
+
+ context 'when the default differs from what is already set on the app' do
+ let(:message) { AppManifestMessage.create_from_yml({ name: 'blah', buildpack: buildpack.name }) }
+ let(:app) { AppModel.make(:buildpack) }
+
+ it 'preserves the apps lifecycle' do
+ app_apply_manifest.apply(app.guid, message)
+ expect(AppUpdate).to have_received(:new).with(user_audit_info, manifest_triggered: true)
+ expect(app_update).to have_received(:update).
+ with(app, app_update_message, instance_of(AppBuildpackLifecycle))
+ expect(app.reload.lifecycle_type).to eq('buildpack')
+ end
+ end
+ end
+
+ context 'buildpack apps' do
+ let(:buildpack) { VCAP::CloudController::Buildpack.make }
+ let(:message) { AppManifestMessage.create_from_yml({ name: 'blah' }) }
+ let(:app_update_message) { message.app_update_message }
+ let(:app) { AppModel.make(:buildpack) }
+
+ before do
+ TestConfig.override(default_app_lifecycle: 'buildpack')
+ end
+
+ context 'when the default_app_lifecycle is set and the the lifecycle is not specified' do
+ it 'preserves the lifecycle' do
+ app_apply_manifest.apply(app.guid, message)
+ expect(AppUpdate).to have_received(:new).with(user_audit_info, manifest_triggered: true)
+ expect(app_update).to have_received(:update).
+ with(app, app_update_message, instance_of(AppBuildpackLifecycle))
+ expect(app.reload.lifecycle_type).to eq('buildpack')
+ end
+ end
+
+ context 'when buildpack is specified' do
+ let(:message) { AppManifestMessage.create_from_yml({ name: 'blah', buildpack: buildpack.name }) }
+
+ it 'preserves the lifecycle' do
+ app_apply_manifest.apply(app.guid, message)
+ expect(AppUpdate).to have_received(:new).with(user_audit_info, manifest_triggered: true)
+ expect(app_update).to have_received(:update).
+ with(app, app_update_message, instance_of(AppBuildpackLifecycle))
+ expect(app.reload.lifecycle_type).to eq('buildpack')
+ end
+ end
+
+ context 'when the default differs from what is already set on the app' do
+ let(:message) { AppManifestMessage.create_from_yml({ name: 'blah', buildpack: buildpack.name }) }
+ let(:app) { AppModel.make(:cnb) }
+
+ it 'preserves the apps lifecycle' do
+ app_apply_manifest.apply(app.guid, message)
+ expect(AppUpdate).to have_received(:new).with(user_audit_info, manifest_triggered: true)
+ expect(app_update).to have_received(:update).
+ with(app, app_update_message, instance_of(AppCNBLifecycle))
+ expect(app.reload.lifecycle_type).to eq('cnb')
+ end
+ end
+ end
+
describe 'updating stack' do
let(:message) { AppManifestMessage.create_from_yml({ name: 'stack-test', stack: 'cflinuxfs4' }) }
let(:app_update_message) { message.app_update_message }
diff --git a/spec/unit/actions/buildpack_upload_spec.rb b/spec/unit/actions/buildpack_upload_spec.rb
index cf3c5f4aa8a..642e56e1428 100644
--- a/spec/unit/actions/buildpack_upload_spec.rb
+++ b/spec/unit/actions/buildpack_upload_spec.rb
@@ -7,7 +7,7 @@ module VCAP::CloudController
describe '#upload_async' do
let!(:buildpack) { VCAP::CloudController::Buildpack.create_from_hash({ name: 'upload_binary_buildpack', stack: nil, position: 0 }) }
- let(:message) { BuildpackUploadMessage.new({ 'bits_path' => '/tmp/path', 'bits_name' => 'buildpack.zip' }) }
+ let(:message) { BuildpackUploadMessage.new({ 'bits_path' => '/tmp/path', 'bits_name' => 'buildpack.zip' }, VCAP::CloudController::Lifecycles::BUILDPACK) }
let(:config) { Config.new({ name: 'local', index: '1' }) }
before do
diff --git a/spec/unit/controllers/v3/buildpacks_controller_spec.rb b/spec/unit/controllers/v3/buildpacks_controller_spec.rb
index f99d423e528..89fcc22444f 100644
--- a/spec/unit/controllers/v3/buildpacks_controller_spec.rb
+++ b/spec/unit/controllers/v3/buildpacks_controller_spec.rb
@@ -52,9 +52,9 @@
let!(:stack1) { VCAP::CloudController::Stack.make }
let!(:stack2) { VCAP::CloudController::Stack.make }
- let!(:buildpack1) { VCAP::CloudController::Buildpack.make(stack: stack1.name) }
- let!(:buildpack2) { VCAP::CloudController::Buildpack.make(stack: stack2.name) }
- let!(:buildpack3) { VCAP::CloudController::Buildpack.make(stack: stack1.name, lifecycle: 'cnb') }
+ let!(:buildpack1) { VCAP::CloudController::Buildpack.make(stack: stack1.name, position: 2) }
+ let!(:buildpack2) { VCAP::CloudController::Buildpack.make(stack: stack2.name, position: 1) }
+ let!(:buildpack3) { VCAP::CloudController::Buildpack.make(stack: stack1.name, lifecycle: 'cnb', position: 1) }
before do
set_current_user(user)
@@ -63,8 +63,9 @@
it 'renders a paginated list of buildpacks' do
get :index
- expect(parsed_body['resources'].first['guid']).to eq(buildpack1.guid)
- expect(parsed_body['resources'].second['guid']).to eq(buildpack2.guid)
+ expect(parsed_body['resources'].first['guid']).to eq(buildpack2.guid)
+ expect(parsed_body['resources'].second['guid']).to eq(buildpack1.guid)
+ expect(parsed_body['resources'].third['guid']).to eq(buildpack3.guid)
end
it 'renders a lifecycle filtered list of buildpacks' do
@@ -90,9 +91,10 @@
it 'renders an ordered list of buildpacks' do
get :index, params: { order_by: '-position' }
- expect(parsed_body['resources']).to have(2).buildpack
- expect(parsed_body['resources'].first['position']).to eq(buildpack2.position)
- expect(parsed_body['resources'].second['position']).to eq(buildpack1.position)
+ expect(parsed_body['resources']).to have(3).buildpack
+ expect(parsed_body['resources'].first['position']).to eq(2)
+ expect(parsed_body['resources'].second['position']).to eq(1)
+ expect(parsed_body['resources'].third['position']).to eq(1)
end
it 'eager loads associated resources that the presenter specifies' do
diff --git a/spec/unit/fetchers/buildpack_list_fetcher_spec.rb b/spec/unit/fetchers/buildpack_list_fetcher_spec.rb
index 9f95ea95cf1..c3891f6bc6f 100644
--- a/spec/unit/fetchers/buildpack_list_fetcher_spec.rb
+++ b/spec/unit/fetchers/buildpack_list_fetcher_spec.rb
@@ -38,7 +38,7 @@ module VCAP::CloudController
let(:filters) { {} }
it 'fetches all the buildpacks' do
- expect(subject).to contain_exactly(buildpack1, buildpack2, buildpack3, buildpack4, buildpack_without_stack)
+ expect(subject).to contain_exactly(buildpack1, buildpack2, buildpack3, buildpack4, buildpack_without_stack, buildpack5, buildpack6, buildpack7)
end
end
@@ -63,7 +63,7 @@ module VCAP::CloudController
end
it 'returns all of the desired buildpacks' do
- expect(subject).to contain_exactly(buildpack_without_stack)
+ expect(subject).to contain_exactly(buildpack_without_stack, buildpack7)
end
end
@@ -73,7 +73,7 @@ module VCAP::CloudController
end
it 'returns all of the desired buildpacks' do
- expect(subject).to contain_exactly(buildpack2, buildpack_without_stack)
+ expect(subject).to contain_exactly(buildpack2, buildpack_without_stack, buildpack6, buildpack7)
end
end
diff --git a/spec/unit/jobs/deserialization_spec.rb b/spec/unit/jobs/deserialization_spec.rb
index 31b350a3e41..c8d15dedc47 100644
--- a/spec/unit/jobs/deserialization_spec.rb
+++ b/spec/unit/jobs/deserialization_spec.rb
@@ -178,7 +178,6 @@ module Jobs
- :lifecycle
extra_keys: []
lifecycle:
- :type: buildpack
:data:
:buildpacks:
- ruby
diff --git a/spec/unit/lib/cloud_controller/install_buildpacks_spec.rb b/spec/unit/lib/cloud_controller/install_buildpacks_spec.rb
index e3b7d9f8320..932edeb18a4 100644
--- a/spec/unit/lib/cloud_controller/install_buildpacks_spec.rb
+++ b/spec/unit/lib/cloud_controller/install_buildpacks_spec.rb
@@ -40,6 +40,74 @@ module VCAP::CloudController
end
end
+ context 'when a cnb buildpack is specified' do
+ let(:file) { 'buildpack.cnb' }
+ let(:buildpack_fields) do
+ { name: 'cnb_buildpack', file: file, stack: nil, options: { lifecycle: Lifecycles::CNB } }
+ end
+
+ let(:install_buildpack_config) do
+ {
+ install_buildpacks: [
+ {
+ 'name' => 'cnb_buildpack',
+ 'package' => 'cnb-buildpack-package',
+ 'lifecycle' => 'cnb'
+ }
+ ]
+ }
+ end
+
+ before do
+ expect(Dir).to receive(:[]).with('/var/vcap/packages/cnb-buildpack-package/*[.zip|.cnb|.tgz|.tar.gz]').
+ and_return([file])
+ expect(File).to receive(:file?).with(file).
+ and_return(true)
+ allow(job_factory).to receive(:plan).with('cnb_buildpack', [buildpack_fields]).and_return([enqueued_job1])
+ allow(Jobs::Enqueuer).to receive(:new).and_return(enqueuer)
+ end
+
+ it 'succeeds' do
+ installer.install(TestConfig.config_instance.get(:install_buildpacks))
+
+ expect(job_factory).to have_received(:plan).with('cnb_buildpack', [buildpack_fields])
+ end
+ end
+
+ context 'when a cnb buildpack is specified with .tgz extension' do
+ let(:file) { 'buildpack.tgz' }
+ let(:buildpack_fields) do
+ { name: 'cnb_buildpack', file: file, stack: nil, options: { lifecycle: Lifecycles::CNB } }
+ end
+
+ let(:install_buildpack_config) do
+ {
+ install_buildpacks: [
+ {
+ 'name' => 'cnb_buildpack',
+ 'package' => 'cnb-buildpack-package',
+ 'lifecycle' => 'cnb'
+ }
+ ]
+ }
+ end
+
+ before do
+ expect(Dir).to receive(:[]).with('/var/vcap/packages/cnb-buildpack-package/*[.zip|.cnb|.tgz|.tar.gz]').
+ and_return([file])
+ expect(File).to receive(:file?).with(file).
+ and_return(true)
+ allow(job_factory).to receive(:plan).with('cnb_buildpack', [buildpack_fields]).and_return([enqueued_job1])
+ allow(Jobs::Enqueuer).to receive(:new).and_return(enqueuer)
+ end
+
+ it 'succeeds' do
+ installer.install(TestConfig.config_instance.get(:install_buildpacks))
+
+ expect(job_factory).to have_received(:plan).with('cnb_buildpack', [buildpack_fields])
+ end
+ end
+
context 'when there are multiple buildpacks' do
let(:buildpack1a_file) { 'abuildpacka.zip' }
let(:buildpack1b_file) { 'abuildpackb.zip' }
@@ -56,15 +124,15 @@ module VCAP::CloudController
end
before do
- expect(Dir).to receive(:[]).with('/var/vcap/packages/mybuildpackpkg/*.zip').
+ expect(Dir).to receive(:[]).with('/var/vcap/packages/mybuildpackpkg/*[.zip|.cnb|.tgz|.tar.gz]').
and_return([buildpack1a_file])
expect(File).to receive(:file?).with(buildpack1a_file).
and_return(true)
- expect(Dir).to receive(:[]).with('/var/vcap/packages/myotherpkg/*.zip').
+ expect(Dir).to receive(:[]).with('/var/vcap/packages/myotherpkg/*[.zip|.cnb|.tgz|.tar.gz]').
and_return([buildpack1b_file])
expect(File).to receive(:file?).with(buildpack1b_file).
and_return(true)
- expect(Dir).to receive(:[]).with('/var/vcap/packages/myotherpkg2/*.zip').
+ expect(Dir).to receive(:[]).with('/var/vcap/packages/myotherpkg2/*[.zip|.cnb|.tgz|.tar.gz]').
and_return([buildpack2_file])
expect(File).to receive(:file?).with(buildpack2_file).
and_return(true)
@@ -124,7 +192,7 @@ module VCAP::CloudController
end
it 'logs an error when no buildpack zip file is found' do
- expect(Dir).to receive(:[]).with('/var/vcap/packages/mybuildpackpkg/*.zip').and_return([])
+ expect(Dir).to receive(:[]).with('/var/vcap/packages/mybuildpackpkg/*[.zip|.cnb|.tgz|.tar.gz]').and_return([])
expect(installer.logger).to receive(:error).with(/No file found for the buildpack/)
installer.install(TestConfig.config_instance.get(:install_buildpacks))
@@ -212,7 +280,7 @@ module VCAP::CloudController
it 'passes optional attributes to the job factory' do
expect(Dir).to receive(:[]).
- with('/var/vcap/packages/mybuildpackpkg/*.zip').
+ with('/var/vcap/packages/mybuildpackpkg/*[.zip|.cnb|.tgz|.tar.gz]').
and_return(['abuildpack.zip'])
expect(File).to receive(:file?).
with('abuildpack.zip').
diff --git a/spec/unit/lib/cloud_controller/paging/pagination_options_spec.rb b/spec/unit/lib/cloud_controller/paging/pagination_options_spec.rb
index 7197c0b7e5c..a122662c729 100644
--- a/spec/unit/lib/cloud_controller/paging/pagination_options_spec.rb
+++ b/spec/unit/lib/cloud_controller/paging/pagination_options_spec.rb
@@ -104,6 +104,34 @@ module VCAP::CloudController
end
end
+ context 'when secondary_default_order_by is configured' do
+ before do
+ pagination_options.secondary_default_order_by = 'id'
+ pagination_options.default_order_by = 'something'
+ end
+
+ context 'when order_by is not configured by the user' do
+ it 'secondary_order_by returns the secondary_default_order_by' do
+ pagination_options.order_by = nil
+ expect(pagination_options.secondary_order_by).to eq('id')
+ end
+ end
+
+ context 'when order_by is configured by the user' do
+ it 'secondary_order_by returns nil' do
+ pagination_options.order_by = 'first_name'
+ expect(pagination_options.secondary_order_by).to be_nil
+ end
+ end
+
+ context 'when order_by is configured by the user to be the same as the default' do
+ it 'secondary_order_by returns the secondary_default_order_b' do
+ pagination_options.order_by = 'something'
+ expect(pagination_options.secondary_order_by).to eq('id')
+ end
+ end
+ end
+
context 'order_direction' do
context 'when the order_direction is nil' do
let(:order_direction) { nil }
diff --git a/spec/unit/lib/cloud_controller/paging/sequel_paginator_spec.rb b/spec/unit/lib/cloud_controller/paging/sequel_paginator_spec.rb
index 7f74df6d7d6..c1ef2d196cd 100644
--- a/spec/unit/lib/cloud_controller/paging/sequel_paginator_spec.rb
+++ b/spec/unit/lib/cloud_controller/paging/sequel_paginator_spec.rb
@@ -143,6 +143,50 @@ class TableWithoutGuid < Sequel::Model(:table_without_guid); end
expect(paginated_result.records.first.keys).to match_array(AppModel.columns)
end
+ it 'orders by secondary_default_order_by if using default order_by' do
+ Space.make(guid: '1')
+ Space.make(guid: '2')
+ Space.make(guid: '3')
+ Space.make(guid: '4')
+ options = { page: page, per_page: 4, order_direction: 'asc' }
+ app_model1.update(guid: '1', space_guid: '2', name: 'yourapp')
+ app_model2.update(guid: '2', space_guid: '1', name: 'yourapp')
+ app_model3.update(guid: '3', space_guid: '3', name: 'myapp')
+ app_model4.update(guid: '4', space_guid: '4', name: 'myapp')
+ pagination_options = PaginationOptions.new(options)
+ pagination_options.default_order_by = 'name'
+ pagination_options.secondary_default_order_by = 'space_guid'
+
+ paginated_result = paginator.get_page(dataset, pagination_options)
+
+ expect(paginated_result.records[0].guid).to eq(app_model3.guid)
+ expect(paginated_result.records[1].guid).to eq(app_model4.guid)
+ expect(paginated_result.records[2].guid).to eq(app_model2.guid)
+ expect(paginated_result.records[3].guid).to eq(app_model1.guid)
+ end
+
+ it 'does not order by secondary_default_order_by if order_by is set' do
+ Space.make(guid: '1')
+ Space.make(guid: '2')
+ Space.make(guid: '3')
+ Space.make(guid: '4')
+ options = { page: page, order_by: 'name', per_page: 4, order_direction: 'asc' }
+ app_model1.update(guid: '1', space_guid: '2', name: 'yourapp')
+ app_model2.update(guid: '2', space_guid: '1', name: 'yourapp')
+ app_model3.update(guid: '3', space_guid: '3', name: 'myapp')
+ app_model4.update(guid: '4', space_guid: '4', name: 'myapp')
+ pagination_options = PaginationOptions.new(options)
+ pagination_options.default_order_by = 'guid'
+ pagination_options.secondary_default_order_by = 'space_guid'
+
+ paginated_result = paginator.get_page(dataset, pagination_options)
+
+ expect(paginated_result.records[0].guid).to eq(app_model3.guid)
+ expect(paginated_result.records[1].guid).to eq(app_model4.guid)
+ expect(paginated_result.records[2].guid).to eq(app_model1.guid)
+ expect(paginated_result.records[3].guid).to eq(app_model2.guid)
+ end
+
it 'orders by GUID as a secondary field when available' do
options = { page: page, per_page: 2, order_by: 'created_at', order_direction: 'asc' }
app_model1.update(guid: '1', created_at: '2019-12-25T13:00:00Z')
diff --git a/spec/unit/lib/cloud_controller/upload_buildpack_spec.rb b/spec/unit/lib/cloud_controller/upload_buildpack_spec.rb
index 67d93a1ea4c..507f56f9abd 100644
--- a/spec/unit/lib/cloud_controller/upload_buildpack_spec.rb
+++ b/spec/unit/lib/cloud_controller/upload_buildpack_spec.rb
@@ -10,10 +10,12 @@ module VCAP::CloudController
let(:tmpdir) { Dir.mktmpdir }
let(:filename) { 'file.zip' }
let(:tgz_filename) { 'file.tgz' }
+ let(:cnb_filename) { 'file.cnb' }
let(:sha_valid_zip) { Digester.new(algorithm: OpenSSL::Digest::SHA256).digest_file(valid_zip) }
let(:sha_valid_zip2) { Digester.new(algorithm: OpenSSL::Digest::SHA256).digest_file(valid_zip2) }
let(:sha_valid_tgz) { Digester.new(algorithm: OpenSSL::Digest::SHA256).digest_file(valid_tgz) }
+ let(:sha_valid_cnb) { Digester.new(algorithm: OpenSSL::Digest::SHA256).digest_file(valid_cnb) }
let(:valid_zip_manifest_stack) { nil }
let(:valid_zip) do
@@ -43,6 +45,13 @@ module VCAP::CloudController
Rack::Test::UploadedFile.new(tgz_file)
end
+ let(:valid_cnb) do
+ cnb_name = File.join(tmpdir, cnb_filename)
+ TestCnb.create(cnb_name, 3, 1024)
+ cnb_file = File.new(cnb_name)
+ Rack::Test::UploadedFile.new(cnb_file)
+ end
+
let(:staging_timeout) { TestConfig.config_instance.get(:staging, :timeout_in_seconds) }
let(:expected_sha_valid_zip) { "#{buildpack.guid}_#{sha_valid_zip}" }
@@ -152,7 +161,7 @@ module VCAP::CloudController
end
end
- context 'lifecycle: cnb' do
+ context 'lifecycle: gzip cnb' do
let!(:buildpack) { VCAP::CloudController::Buildpack.create_from_hash({ name: 'upload_cnb_buildpack', stack: 'cider', position: 0, lifecycle: 'cnb' }) }
let(:expected_sha_valid_tgz) { "#{buildpack.guid}_#{sha_valid_tgz}" }
@@ -163,6 +172,17 @@ module VCAP::CloudController
end
end
+ context 'lifecycle: cnb' do
+ let!(:buildpack) { VCAP::CloudController::Buildpack.create_from_hash({ name: 'upload_cnb_buildpack', stack: 'cider', position: 0, lifecycle: 'cnb' }) }
+ let(:expected_sha_valid_cnb) { "#{buildpack.guid}_#{sha_valid_cnb}" }
+
+ it 'uploads' do
+ expect(buildpack_blobstore).to receive(:cp_to_blobstore).with(valid_cnb, expected_sha_valid_cnb)
+
+ upload_buildpack.upload_buildpack(buildpack, valid_cnb, cnb_filename)
+ end
+ end
+
it 'updates the buildpack filename' do
expect do
upload_buildpack.upload_buildpack(buildpack, valid_zip, filename)
diff --git a/spec/unit/messages/app_manifest_message_spec.rb b/spec/unit/messages/app_manifest_message_spec.rb
index 3680e2de7bc..64fd81bf2e5 100644
--- a/spec/unit/messages/app_manifest_message_spec.rb
+++ b/spec/unit/messages/app_manifest_message_spec.rb
@@ -1064,6 +1064,7 @@ module VCAP::CloudController
message = AppManifestMessage.create_from_yml(params_from_yaml)
expect(message).not_to be_valid
+
expect(message.errors).to have(1).items
expect(message.errors.full_messages).to include('Cannot specify both buildpack(s) and docker keys')
end
@@ -1995,7 +1996,6 @@ module VCAP::CloudController
it 'does not forward missing attributes to the AppUpdateMessage' do
message = AppManifestMessage.create_from_yml(parsed_yaml)
-
expect(message.app_update_message.requested?(:lifecycle)).to be false
end
end
@@ -2105,6 +2105,42 @@ module VCAP::CloudController
end
end
+ context 'when buildpack is the default_app_lifecycle but lifecycle is not set' do
+ before do
+ TestConfig.override(default_app_lifecycle: 'buildpack')
+ end
+
+ let(:parsed_yaml) { { name: 'cnb', buildpacks: %w[nodejs java], stack: stack.name } }
+
+ it 'sets the app lifecycle to nil' do
+ message = AppManifestMessage.create_from_yml(parsed_yaml)
+
+ expect(message).to be_valid
+ expect(message.app_update_message.lifecycle_type).to be_nil
+ expect(message.app_update_message.buildpack_data.buildpacks).to eq(%w[nodejs java])
+ expect(message.app_update_message.buildpack_data.stack).to eq(stack.name)
+ expect(message.app_update_message.buildpack_data.credentials).to be_nil
+ end
+ end
+
+ context 'when cnb is the default_app_lifecycle but lifecycle is not set' do
+ before do
+ TestConfig.override(default_app_lifecycle: 'cnb')
+ end
+
+ let(:parsed_yaml) { { name: 'cnb', buildpacks: %w[nodejs java], stack: stack.name } }
+
+ it 'sets the app lifecycle to cnb' do
+ message = AppManifestMessage.create_from_yml(parsed_yaml)
+
+ expect(message).to be_valid
+ expect(message.app_update_message.lifecycle_type).to be_nil
+ expect(message.app_update_message.buildpack_data.buildpacks).to eq(%w[nodejs java])
+ expect(message.app_update_message.buildpack_data.stack).to eq(stack.name)
+ expect(message.app_update_message.buildpack_data.credentials).to be_nil
+ end
+ end
+
context 'when cnb is disabled' do
before do
FeatureFlag.make(name: 'diego_cnb', enabled: false, error_message: 'I am a banana')
diff --git a/spec/unit/messages/buildpack_upload_message_spec.rb b/spec/unit/messages/buildpack_upload_message_spec.rb
index 7c295b783ac..fee9b05a1c3 100644
--- a/spec/unit/messages/buildpack_upload_message_spec.rb
+++ b/spec/unit/messages/buildpack_upload_message_spec.rb
@@ -5,6 +5,8 @@ module VCAP::CloudController
RSpec.describe BuildpackUploadMessage do
before { TestConfig.override(directories: { tmpdir: '/tmp/' }) }
+ let(:lifecycle) { VCAP::CloudController::Lifecycles::BUILDPACK }
+
describe 'validations' do
let(:stat_double) { instance_double(File::Stat, size: 2) }
@@ -19,7 +21,7 @@ module VCAP::CloudController
let(:opts) { { '' => '', bits_path: '/tmp/foobar', bits_name: 'buildpack.zip' } }
it 'is invalid' do
- upload_message = BuildpackUploadMessage.new(opts)
+ upload_message = BuildpackUploadMessage.new(opts, lifecycle)
expect(upload_message).not_to be_valid
expect(upload_message.errors[:base]).to include('Uploaded bits are not a valid buildpack file')
end
@@ -29,7 +31,7 @@ module VCAP::CloudController
let(:opts) { { bits_path: '/tmp/foobar', bits_name: 'buildpack.zip' } }
it 'is valid' do
- upload_message = BuildpackUploadMessage.new(opts)
+ upload_message = BuildpackUploadMessage.new(opts, lifecycle)
expect(upload_message).to be_valid
end
end
@@ -38,7 +40,7 @@ module VCAP::CloudController
let(:opts) { { bits_path: '../tmp/mango/pear', bits_name: 'buildpack.zip' } }
it 'is valid' do
- upload_message = BuildpackUploadMessage.new(opts)
+ upload_message = BuildpackUploadMessage.new(opts, lifecycle)
expect(upload_message).to be_valid
end
end
@@ -47,7 +49,7 @@ module VCAP::CloudController
let(:opts) { { bits_path: '../tmp-not!/mango/pear' } }
it 'is not valid' do
- upload_message = BuildpackUploadMessage.new(opts)
+ upload_message = BuildpackUploadMessage.new(opts, lifecycle)
expect(upload_message).not_to be_valid
expect(upload_message.errors[:bits_path]).to include('is invalid')
end
@@ -57,7 +59,7 @@ module VCAP::CloudController
let(:opts) { {} }
it 'is not valid' do
- upload_message = BuildpackUploadMessage.new(opts)
+ upload_message = BuildpackUploadMessage.new(opts, lifecycle)
expect(upload_message).not_to be_valid
expect(upload_message.errors[:base]).to include('A buildpack zip file must be uploaded as \'bits\'')
end
@@ -67,7 +69,7 @@ module VCAP::CloudController
let(:opts) { { bits_path: '/tmp/bar', unexpected: 'foo' } }
it 'is not valid' do
- message = BuildpackUploadMessage.new(opts)
+ message = BuildpackUploadMessage.new(opts, lifecycle)
expect(message).not_to be_valid
expect(message.errors.full_messages[0]).to include("Unknown field(s): 'unexpected'")
@@ -78,7 +80,7 @@ module VCAP::CloudController
let(:opts) { { bits_path: '/secret/file', bits_name: 'buildpack.zip' } }
it 'is not valid' do
- message = BuildpackUploadMessage.new(opts)
+ message = BuildpackUploadMessage.new(opts, lifecycle)
expect(message).not_to be_valid
expect(message.errors.full_messages[0]).to include('Bits path is invalid')
@@ -89,7 +91,7 @@ module VCAP::CloudController
let(:opts) { { bits_path: '/tmp/bar' } }
it 'is not valid' do
- upload_message = BuildpackUploadMessage.new(opts)
+ upload_message = BuildpackUploadMessage.new(opts, lifecycle)
expect(upload_message).not_to be_valid
expect(upload_message.errors[:base]).to include('A buildpack zip file must be uploaded as \'bits\'')
end
@@ -100,19 +102,85 @@ module VCAP::CloudController
it 'is not valid' do
allow(File).to receive(:read).and_return("PX\x03\x04".force_encoding('binary'))
- upload_message = BuildpackUploadMessage.new(opts)
+ upload_message = BuildpackUploadMessage.new(opts, lifecycle)
expect(upload_message).not_to be_valid
- expect(upload_message.errors.full_messages[0]).to include('buildpack.abcd is not a zip or gzip')
+ expect(upload_message.errors.full_messages[0]).to include('buildpack.abcd is not a zip file. Buildpacks of lifecycle "buildpack" must be valid zip files.')
end
end
- context 'when the file is a tgz' do
- let(:opts) { { bits_path: '/tmp/bar', bits_name: 'buildpack.tgz' } }
+ context 'buildpacks' do
+ let(:lifecycle) { VCAP::CloudController::Lifecycles::BUILDPACK }
- it 'is not valid' do
- allow(File).to receive(:read).and_return("\x1F\x8B\x08".force_encoding('binary'))
- upload_message = BuildpackUploadMessage.new(opts)
- expect(upload_message).to be_valid
+ context 'when the file is a zip' do
+ let(:opts) { { bits_path: '/tmp/bar', bits_name: 'buildpack.zip' } }
+
+ it 'is valid' do
+ allow(File).to receive(:read).and_return("PK\x03\x04".force_encoding('binary'))
+ upload_message = BuildpackUploadMessage.new(opts, lifecycle)
+ expect(upload_message).to be_valid
+ end
+ end
+
+ context 'when the file is a tgz' do
+ let(:opts) { { bits_path: '/tmp/bar', bits_name: 'buildpack.tgz' } }
+
+ it 'is not valid' do
+ allow(File).to receive(:read).and_return("\x1F\x8B\x08".force_encoding('binary'))
+ upload_message = BuildpackUploadMessage.new(opts, lifecycle)
+ expect(upload_message).not_to be_valid
+ expect(upload_message.errors.full_messages[0]).to include('buildpack.tgz is not a zip file. Buildpacks of lifecycle "buildpack" must be valid zip files.')
+ end
+ end
+
+ context 'when the file is a cnb/tar' do
+ let(:opts) { { bits_path: '/tmp/bar', bits_name: 'buildpack.cnb' } }
+
+ it 'is not valid' do
+ values = ["\x0".force_encoding('binary'), "\x75\x73\x74\x61\x72\x00\x30\x30".force_encoding('binary')]
+ allow(File).to receive(:read).and_return(*values)
+ upload_message = BuildpackUploadMessage.new(opts, lifecycle)
+ expect(upload_message).not_to be_valid
+ expect(upload_message.errors.full_messages[0]).to include('buildpack.cnb is not a zip file. Buildpacks of lifecycle "buildpack" must be valid zip files.')
+ end
+ end
+ end
+
+ context 'cloud native buildpacks (cnb)' do
+ let(:lifecycle) { VCAP::CloudController::Lifecycles::CNB }
+
+ context 'buildpacks' do
+ context 'when the file is a zip' do
+ let(:opts) { { bits_path: '/tmp/bar', bits_name: 'buildpack.zip' } }
+
+ it 'is valid' do
+ allow(File).to receive(:read).and_return("PK\x03\x04".force_encoding('binary'))
+ upload_message = BuildpackUploadMessage.new(opts, lifecycle)
+ expect(upload_message).not_to be_valid
+ expect(upload_message.errors.full_messages[0]).
+ to include('buildpack.zip is not a gzip archive or cnb file. Buildpacks of lifecycle "cnb" must be valid gzip archives or cnb files.')
+ end
+ end
+
+ context 'when the file is a tgz' do
+ let(:opts) { { bits_path: '/tmp/bar', bits_name: 'buildpack.tgz' } }
+
+ it 'is valid' do
+ allow(File).to receive(:read).and_return("\x1F\x8B\x08".force_encoding('binary'))
+ upload_message = BuildpackUploadMessage.new(opts, lifecycle)
+ expect(upload_message).to be_valid
+ end
+ end
+
+ context 'when the file is a cnb/tar' do
+ let(:opts) { { bits_path: '/tmp/bar', bits_name: 'buildpack.cnb' } }
+
+ it 'is valid' do
+ values = ["\x0".force_encoding('binary'), "\x75\x73\x74\x61\x72\x00\x30\x30".force_encoding('binary')]
+ allow(File).to receive(:read).and_return(*values)
+ upload_message = BuildpackUploadMessage.new(opts, lifecycle)
+ expect(upload_message).to be_valid
+ end
+ end
end
end
@@ -121,7 +189,7 @@ module VCAP::CloudController
let(:stat_double) { instance_double(File::Stat, size: 0) }
it 'is not valid' do
- upload_message = BuildpackUploadMessage.new(opts)
+ upload_message = BuildpackUploadMessage.new(opts, lifecycle)
expect(upload_message).not_to be_valid
expect(upload_message.errors.full_messages[0]).to include('buildpack.zip cannot be empty')
end
@@ -129,7 +197,7 @@ module VCAP::CloudController
end
describe '#bits_path=' do
- subject(:upload_message) { BuildpackUploadMessage.new(bits_path: 'not-nil') }
+ subject(:upload_message) { BuildpackUploadMessage.new({ bits_path: 'not-nil' }, lifecycle) }
context 'when the bits_path is relative' do
it 'makes it absolute (within the tmpdir)' do
@@ -151,7 +219,7 @@ module VCAP::CloudController
let(:params) { { 'bits_path' => '/tmp/foobar', 'bits_name' => 'buildpack.zip' } }
it 'returns the correct BuildpackUploadMessage' do
- message = BuildpackUploadMessage.create_from_params(params)
+ message = BuildpackUploadMessage.create_from_params(params, lifecycle)
expect(message).to be_a(BuildpackUploadMessage)
expect(message.bits_path).to eq('/tmp/foobar')
@@ -159,7 +227,7 @@ module VCAP::CloudController
end
it 'converts requested keys to symbols' do
- message = BuildpackUploadMessage.create_from_params(params)
+ message = BuildpackUploadMessage.create_from_params(params, lifecycle)
expect(message).to be_requested(:bits_path)
expect(message).to be_requested(:bits_name)