spec/unit/lib/cloud_controller/diego/buildpack/staging_action_builder_spec.rb
require 'spec_helper'
module VCAP::CloudController
module Diego
module Buildpack
RSpec.describe StagingActionBuilder do
subject(:builder) { StagingActionBuilder.new(config, staging_details, lifecycle_data) }
let(:droplet) { DropletModel.make(:buildpack) }
let(:enable_declarative_asset_downloads) { false }
let(:legacy_md5_buildpack_paths_enabled) { false }
let(:config) do
Config.new({
skip_cert_verify: false,
diego: {
cc_uploader_url: 'http://cc-uploader.example.com',
file_server_url: 'http://file-server.example.com',
lifecycle_bundles: {
'buildpack/buildpack-stack': 'the-buildpack-bundle'
},
enable_declarative_asset_downloads: enable_declarative_asset_downloads
},
staging: {
legacy_md5_buildpack_paths_enabled: legacy_md5_buildpack_paths_enabled,
minimum_staging_file_descriptor_limit: 4,
timeout_in_seconds: 90
}
})
end
let(:staging_details) do
StagingDetails.new.tap do |details|
details.staging_guid = droplet.guid
details.environment_variables = env
end
end
let(:env) { double(:env) }
let(:stack) { 'buildpack-stack' }
let(:lifecycle_data) do
{
app_bits_download_uri: 'http://app_bits_download_uri.example.com/path/to/bits',
build_artifacts_cache_download_uri: 'http://build_artifacts_cache_download_uri.example.com/path/to/bits',
build_artifacts_cache_upload_uri: 'http://build_artifacts_cache_upload_uri.example.com/path/to/bits',
buildpacks: buildpacks,
droplet_upload_uri: 'http://droplet_upload_uri.example.com/path/to/bits',
stack: stack,
buildpack_cache_checksum: 'bp-cache-checksum',
app_bits_checksum: { type: 'sha256', value: 'package-checksum' }
}
end
let(:buildpacks) { [] }
let(:generated_environment) { [::Diego::Bbs::Models::EnvironmentVariable.new(name: 'generated-environment', value: 'generated-value')] }
before do
allow(LifecycleBundleUriGenerator).to receive(:uri).with('the-buildpack-bundle').and_return('generated-uri')
allow(BbsEnvironmentBuilder).to receive(:build).with(env).and_return(generated_environment)
TestConfig.override(credhub_api: nil)
Stack.create(name: 'buildpack-stack')
end
describe '#action' do
let(:download_app_package_action) do
::Diego::Bbs::Models::DownloadAction.new(
artifact: 'app package',
from: 'http://app_bits_download_uri.example.com/path/to/bits',
to: '/tmp/app',
user: 'vcap',
checksum_algorithm: 'sha256',
checksum_value: 'package-checksum'
)
end
let(:download_build_artifacts_cache_action) do
::Diego::Bbs::Models::DownloadAction.new(
artifact: 'build artifacts cache',
from: 'http://build_artifacts_cache_download_uri.example.com/path/to/bits',
to: '/tmp/cache',
user: 'vcap',
checksum_algorithm: 'sha256',
checksum_value: 'bp-cache-checksum'
)
end
let(:droplet_upload_url) { CGI.escape('http://droplet_upload_uri.example.com/path/to/bits') }
let(:upload_droplet_action) do
::Diego::Bbs::Models::UploadAction.new(
artifact: 'droplet',
from: '/tmp/droplet',
to: "http://cc-uploader.example.com/v1/droplet/#{droplet.guid}?cc-droplet-upload-uri=#{droplet_upload_url}&timeout=90",
user: 'vcap'
)
end
let(:cache_upload_url) { CGI.escape('http://build_artifacts_cache_upload_uri.example.com/path/to/bits') }
let(:upload_build_artifacts_cache_action) do
::Diego::Bbs::Models::UploadAction.new(
artifact: 'build artifacts cache',
from: '/tmp/output-cache',
to: "http://cc-uploader.example.com/v1/build_artifacts/#{droplet.guid}?cc-build-artifacts-upload-uri=#{cache_upload_url}&timeout=90",
user: 'vcap'
)
end
let(:run_staging_action) do
::Diego::Bbs::Models::RunAction.new(
path: '/tmp/lifecycle/builder',
args: [
'-buildpackOrder=buildpack-1-key,buildpack-2-key',
'-skipCertVerify=false',
'-skipDetect=false',
'-buildDir=/tmp/app',
'-outputDroplet=/tmp/droplet',
'-outputMetadata=/tmp/result.json',
'-outputBuildArtifactsCache=/tmp/output-cache',
'-buildpacksDir=/tmp/buildpacks',
'-buildArtifactsCacheDir=/tmp/cache'
],
user: 'vcap',
resource_limits: ::Diego::Bbs::Models::ResourceLimits.new(nofile: 4),
env: generated_environment
)
end
let(:buildpacks) do
[
{ name: 'buildpack-1', key: 'buildpack-1-key', url: 'buildpack-1-url', skip_detect: false },
{ name: 'buildpack-2', key: 'buildpack-2-key', url: 'buildpack-2-url', skip_detect: false }
]
end
it 'returns the correct buildpack staging action structure' do
result = builder.action
serial_action = result.serial_action
actions = serial_action.actions
parallel_download_action = actions[0].parallel_action
expect(parallel_download_action.actions[0].download_action).to eq(download_app_package_action)
expect(parallel_download_action.actions[1].try_action.action.download_action).to eq(download_build_artifacts_cache_action)
expect(actions[1].run_action).to eq(run_staging_action)
emit_progress_action = actions[2].emit_progress_action
expect(emit_progress_action.start_message).to eq('Uploading droplet, build artifacts cache...')
expect(emit_progress_action.success_message).to eq('Uploading complete')
expect(emit_progress_action.failure_message_prefix).to eq('Uploading failed')
parallel_upload_action = actions[2].emit_progress_action.action
expect(parallel_upload_action.parallel_action).not_to be_nil
upload_actions = parallel_upload_action.parallel_action.actions
expect(upload_actions[0].upload_action).to eq(upload_droplet_action)
expect(upload_actions[1].upload_action).to eq(upload_build_artifacts_cache_action)
end
describe 'credhub' do
let(:credhub_url) { TestConfig.config_instance.get(:credhub_api, :internal_url) }
let(:expected_platform_options) do
[
::Diego::Bbs::Models::EnvironmentVariable.new(
name: 'VCAP_PLATFORM_OPTIONS',
value: '{"credhub-uri":"https://credhub.capi.internal:8844"}'
)
]
end
let(:expected_credhub_arg) do
{ 'VCAP_PLATFORM_OPTIONS' => { 'credhub-uri' => credhub_url } }
end
context 'when credhub url is present' do
before do
TestConfig.override(credhub_api: { internal_url: 'http:credhub.capi.land:8844' })
end
context 'when the interpolation of service bindings is enabled' do
before do
TestConfig.override(credential_references: { interpolate_service_bindings: true })
end
it 'sends the credhub_url in the environment variables' do
result = builder.action
actions = result.serial_action.actions
expect(actions[1].run_action.env).to eq(generated_environment + expected_platform_options)
end
end
context 'when the interpolation of service bindings is disabled' do
before do
TestConfig.override(credential_references: { interpolate_service_bindings: false })
end
it 'does not send the credhub_url in the environment variables' do
result = builder.action
actions = result.serial_action.actions
expect(actions[1].run_action.env).to eq(generated_environment)
end
end
end
context 'when credhub url is not present' do
before do
TestConfig.override(credhub_api: nil)
end
it 'does not send the credhub_url in the environment variables' do
result = builder.action
actions = result.serial_action.actions
expect(actions[1].run_action.env).to eq(generated_environment)
end
end
end
context 'when there is no buildpack cache' do
before do
lifecycle_data[:build_artifacts_cache_download_uri] = nil
end
it 'does not include the builpack cache download action' do
result = builder.action
serial_action = result.serial_action
actions = serial_action.actions
parallel_download_action = actions[0].parallel_action
expect(parallel_download_action.actions.count).to eq(1)
expect(parallel_download_action.actions.first.download_action).not_to eq(download_build_artifacts_cache_action)
end
end
context 'when there is no buildpack cache checksum' do
before do
lifecycle_data[:buildpack_cache_checksum] = ''
end
it 'does not include the builpack cache download action' do
result = builder.action
serial_action = result.serial_action
actions = serial_action.actions
parallel_download_action = actions[0].parallel_action
expect(parallel_download_action.actions.count).to eq(1)
expect(parallel_download_action.actions.first.download_action).not_to eq(download_build_artifacts_cache_action)
end
end
context 'when any specified buildpack has "skip_detect" set to true' do
let(:buildpacks) do
[
{ name: 'buildpack-1', key: 'buildpack-1-key', url: 'buildpack-1-url', skip_detect: false },
{ name: 'buildpack-2', key: 'buildpack-2-key', url: 'buildpack-2-url', skip_detect: true }
]
end
it 'sets skipDetect to true' do
result = builder.action
serial_action = result.serial_action
actions = serial_action.actions
expect(actions[1].run_action.args).to include('-skipDetect=true')
end
end
context 'when enable_declarative_asset_downloads is true' do
let(:enable_declarative_asset_downloads) { true }
it 'returns the buildpack staging action without download actions' do
result = builder.action
serial_action = result.serial_action
actions = serial_action.actions
expect(actions[0].parallel_action).to be_nil
end
context 'when the app package does not have a sha256 checksum' do
# this test can be removed once all app packages have sha256 checksums
before do
lifecycle_data[:app_bits_checksum][:type] = 'sha1'
download_app_package_action['checksum_algorithm'] = 'sha1'
end
it 'includes the app package download in the staging action' do
result = builder.action
serial_action = result.serial_action
actions = serial_action.actions
parallel_download_action = actions[0].parallel_action
expect(parallel_download_action.actions.count).to eq(1)
expect(parallel_download_action.actions[0].download_action).to eq(download_app_package_action)
end
end
end
end
describe '#cached_dependencies' do
it 'always returns the buildpack lifecycle bundle dependency' do
result = builder.cached_dependencies
expect(result).to include(
::Diego::Bbs::Models::CachedDependency.new(
from: 'generated-uri',
to: '/tmp/lifecycle',
cache_key: 'buildpack-buildpack-stack-lifecycle'
)
)
end
context 'when enable_declarative_asset_downloads is true' do
let(:enable_declarative_asset_downloads) { true }
it 'returns no cached dependencies' do
expect(builder.cached_dependencies).to be_nil
end
end
context 'when there are buildpacks' do
let(:buildpacks) do
[
{ name: 'buildpack-1', key: 'buildpack-1-key', url: 'buildpack-1-url', sha256: 'checksum' },
{ name: 'buildpack-2', key: 'buildpack-2-key', url: 'buildpack-2-url', sha256: 'checksum' }
]
end
it 'includes buildpack dependencies' do
buildpack_entry_1 = ::Diego::Bbs::Models::CachedDependency.new(
name: 'buildpack-1',
from: 'buildpack-1-url',
to: "/tmp/buildpacks/#{Digest::XXH64.hexdigest('buildpack-1-key')}",
cache_key: 'buildpack-1-key',
checksum_algorithm: 'sha256',
checksum_value: 'checksum'
)
buildpack_entry_2 = ::Diego::Bbs::Models::CachedDependency.new(
name: 'buildpack-2',
from: 'buildpack-2-url',
to: "/tmp/buildpacks/#{Digest::XXH64.hexdigest('buildpack-2-key')}",
cache_key: 'buildpack-2-key',
checksum_algorithm: 'sha256',
checksum_value: 'checksum'
)
result = builder.cached_dependencies
expect(result).to include(buildpack_entry_1, buildpack_entry_2)
end
context 'when legacy_md5_buildpack_paths_enabled is true' do
let(:legacy_md5_buildpack_paths_enabled) { true }
it 'includes buildpack dependencies' do
buildpack_entry_1 = ::Diego::Bbs::Models::CachedDependency.new(
name: 'buildpack-1',
from: 'buildpack-1-url',
to: "/tmp/buildpacks/#{OpenSSL::Digest.hexdigest('MD5', 'buildpack-1-key')}",
cache_key: 'buildpack-1-key',
checksum_algorithm: 'sha256',
checksum_value: 'checksum'
)
buildpack_entry_2 = ::Diego::Bbs::Models::CachedDependency.new(
name: 'buildpack-2',
from: 'buildpack-2-url',
to: "/tmp/buildpacks/#{OpenSSL::Digest.hexdigest('MD5', 'buildpack-2-key')}",
cache_key: 'buildpack-2-key',
checksum_algorithm: 'sha256',
checksum_value: 'checksum'
)
result = builder.cached_dependencies
expect(result).to include(buildpack_entry_1, buildpack_entry_2)
end
end
context 'when enable_declarative_asset_downloads is true' do
let(:enable_declarative_asset_downloads) { true }
it 'returns no cached dependencies' do
expect(builder.cached_dependencies).to be_nil
end
end
context 'and some do not include checksums' do
let(:buildpacks) do
[
{ name: 'buildpack-1', key: 'buildpack-1-key', url: 'buildpack-1-url', sha256: 'checksum' },
{ name: 'buildpack-2', key: 'buildpack-2-key', url: 'buildpack-2-url', sha256: nil }
]
end
it 'does not ask for checksum validation' do
buildpack_entry_1 = ::Diego::Bbs::Models::CachedDependency.new(
name: 'buildpack-1',
from: 'buildpack-1-url',
to: "/tmp/buildpacks/#{Digest::XXH64.hexdigest('buildpack-1-key')}",
cache_key: 'buildpack-1-key',
checksum_algorithm: 'sha256',
checksum_value: 'checksum'
)
buildpack_entry_2 = ::Diego::Bbs::Models::CachedDependency.new(
name: 'buildpack-2',
from: 'buildpack-2-url',
to: "/tmp/buildpacks/#{Digest::XXH64.hexdigest('buildpack-2-key')}",
cache_key: 'buildpack-2-key'
)
result = builder.cached_dependencies
expect(result).to include(buildpack_entry_1, buildpack_entry_2)
end
context 'when enable_declarative_asset_downloads is true' do
let(:enable_declarative_asset_downloads) { true }
it 'returns no cached dependencies' do
expect(builder.cached_dependencies).to be_nil
end
end
end
end
context 'when there are custom buildpacks' do
let(:buildpacks) do
[
{ name: 'buildpack-1', key: 'buildpack-1-key', url: 'buildpack-1-url', sha256: 'checksum' },
{ name: 'custom', key: 'custom-key', url: 'custom-url' }
]
end
it 'does not include the custom buildpacks' do
buildpack_entry_1 = ::Diego::Bbs::Models::CachedDependency.new(
name: 'buildpack-1',
from: 'buildpack-1-url',
to: "/tmp/buildpacks/#{Digest::XXH64.hexdigest('buildpack-1-key')}",
cache_key: 'buildpack-1-key',
checksum_algorithm: 'sha256',
checksum_value: 'checksum'
)
buildpack_entry_2 = ::Diego::Bbs::Models::CachedDependency.new(
name: 'custom',
from: 'custom-url',
to: "/tmp/buildpacks/#{Digest::XXH64.hexdigest('custom-key')}",
cache_key: 'custom-key'
)
result = builder.cached_dependencies
expect(result).to include(buildpack_entry_1)
expect(result).not_to include(buildpack_entry_2)
end
end
end
describe '#image_layers' do
it 'returns no image layers' do
expect(builder.image_layers).to be_empty
end
context 'when enable_declarative_asset_downloads is true' do
let(:enable_declarative_asset_downloads) { true }
it 'returns the lifecycle as an image layer' do
expect(builder.image_layers).to include(
::Diego::Bbs::Models::ImageLayer.new(
name: 'buildpack-buildpack-stack-lifecycle',
url: 'generated-uri',
destination_path: '/tmp/lifecycle',
layer_type: ::Diego::Bbs::Models::ImageLayer::Type::SHARED,
media_type: ::Diego::Bbs::Models::ImageLayer::MediaType::TGZ
)
)
end
it 'returns the app package as an image layer' do
expect(builder.image_layers).to include(
::Diego::Bbs::Models::ImageLayer.new(
name: 'app package',
url: 'http://app_bits_download_uri.example.com/path/to/bits',
destination_path: '/tmp/app',
layer_type: ::Diego::Bbs::Models::ImageLayer::Type::EXCLUSIVE,
media_type: ::Diego::Bbs::Models::ImageLayer::MediaType::ZIP,
digest_algorithm: ::Diego::Bbs::Models::ImageLayer::DigestAlgorithm::SHA256,
digest_value: 'package-checksum'
)
)
end
context 'when the app package does not have sha256 checksum' do
# this test can be removed once all app packages have sha256 checksums
before do
lifecycle_data[:app_bits_checksum][:type] = 'sha1'
end
it 'does not include the app package as an image layer' do
expect(builder.image_layers.any? { |l| l['name'] == 'app package' }).to be false
end
end
it 'returns the buildpack cache as an image layer' do
expect(builder.image_layers).to include(
::Diego::Bbs::Models::ImageLayer.new(
name: 'build artifacts cache',
url: 'http://build_artifacts_cache_download_uri.example.com/path/to/bits',
destination_path: '/tmp/cache',
layer_type: ::Diego::Bbs::Models::ImageLayer::Type::EXCLUSIVE,
media_type: ::Diego::Bbs::Models::ImageLayer::MediaType::ZIP,
digest_algorithm: ::Diego::Bbs::Models::ImageLayer::DigestAlgorithm::SHA256,
digest_value: 'bp-cache-checksum'
)
)
end
context 'when there is no buildpack cache' do
before do
lifecycle_data[:build_artifacts_cache_download_uri] = nil
end
it 'does not include the buildpack cache as an image layer' do
expect(builder.image_layers.any? { |l| l['name'] == 'build artifacts cache' }).to be false
end
end
context 'when there is no buildpack cache checksum' do
before do
lifecycle_data[:buildpack_cache_checksum] = ''
end
it 'does not include the buildpack cache as an image layer' do
expect(builder.image_layers.any? { |l| l['name'] == 'build artifacts cache' }).to be false
end
end
context 'when there are buildpacks' do
let(:buildpacks) do
[
{ name: 'buildpack-1', key: 'buildpack-1-key', url: 'buildpack-1-url', sha256: 'checksum' },
{ name: 'buildpack-2', key: 'buildpack-2-key', url: 'buildpack-2-url', sha256: 'checksum' }
]
end
it 'returns the buildpacks as an image layer' do
expect(builder.image_layers).to include(
::Diego::Bbs::Models::ImageLayer.new(
name: 'buildpack-1',
url: 'buildpack-1-url',
destination_path: "/tmp/buildpacks/#{Digest::XXH64.hexdigest('buildpack-1-key')}",
digest_algorithm: ::Diego::Bbs::Models::ImageLayer::DigestAlgorithm::SHA256,
digest_value: 'checksum',
layer_type: ::Diego::Bbs::Models::ImageLayer::Type::SHARED,
media_type: ::Diego::Bbs::Models::ImageLayer::MediaType::ZIP
)
)
expect(builder.image_layers).to include(
::Diego::Bbs::Models::ImageLayer.new(
name: 'buildpack-2',
url: 'buildpack-2-url',
destination_path: "/tmp/buildpacks/#{Digest::XXH64.hexdigest('buildpack-2-key')}",
digest_algorithm: ::Diego::Bbs::Models::ImageLayer::DigestAlgorithm::SHA256,
digest_value: 'checksum',
layer_type: ::Diego::Bbs::Models::ImageLayer::Type::SHARED,
media_type: ::Diego::Bbs::Models::ImageLayer::MediaType::ZIP
)
)
end
context 'and some do not include checksums' do
let(:buildpacks) do
[
{ name: 'buildpack-1', key: 'buildpack-1-key', url: 'buildpack-1-url', sha256: 'checksum' },
{ name: 'buildpack-2', key: 'buildpack-2-key', url: 'buildpack-2-url', sha256: nil }
]
end
it 'returns the buildpacks without checksum information' do
expect(builder.image_layers).to include(
::Diego::Bbs::Models::ImageLayer.new(
name: 'buildpack-2',
url: 'buildpack-2-url',
destination_path: "/tmp/buildpacks/#{Digest::XXH64.hexdigest('buildpack-2-key')}",
layer_type: ::Diego::Bbs::Models::ImageLayer::Type::SHARED,
media_type: ::Diego::Bbs::Models::ImageLayer::MediaType::ZIP
)
)
end
end
end
context 'when there are custom buildpacks' do
let(:buildpacks) do
[
{ name: 'buildpack-1', key: 'buildpack-1-key', url: 'buildpack-1-url', sha256: 'checksum' },
{ name: 'custom', key: 'custom-key', url: 'custom-url' }
]
end
it 'does not returns the custom buildpack as an image layer' do
expect(builder.image_layers).not_to include(
::Diego::Bbs::Models::ImageLayer.new(
name: 'custom',
url: 'custom-url',
destination_path: "/tmp/buildpacks/#{Digest::XXH64.hexdigest('custom-key')}",
layer_type: ::Diego::Bbs::Models::ImageLayer::Type::SHARED,
media_type: ::Diego::Bbs::Models::ImageLayer::MediaType::ZIP
)
)
end
end
end
end
describe '#stack' do
before do
Stack.create(name: 'separate-build-and-run', run_rootfs_image: 'run-image', build_rootfs_image: 'build-image')
end
it 'returns the stack' do
expect(builder.stack).to eq('preloaded:buildpack-stack')
end
context 'when the stack does not exist' do
let(:stack) { 'does-not-exist' }
it 'raises an error' do
expect do
builder.stack
end.to raise_error CloudController::Errors::ApiError, /The stack could not be found/
end
end
context 'when the stack has separate build and run rootfs images' do
let(:stack) { 'separate-build-and-run' }
it 'returns the build rootfs image' do
expect(builder.stack).to eq('preloaded:build-image')
end
end
end
describe '#task_environment_variables' do
it 'returns LANG' do
lang_env = ::Diego::Bbs::Models::EnvironmentVariable.new(name: 'LANG', value: 'en_US.UTF-8')
expect(builder.task_environment_variables).to contain_exactly(lang_env)
end
end
end
end
end
end