spec/unit/lib/cloud_controller/packager/registry_bits_packer_spec.rb
require 'spec_helper'
require 'cloud_controller/packager/registry_bits_packer'
module CloudController::Packager
RSpec.describe RegistryBitsPacker do
subject(:packer) { RegistryBitsPacker.new }
let(:uploaded_files_path) { File.join(local_tmp_dir, 'good.zip') }
let(:input_zip) { File.join(Paths::FIXTURES, 'good.zip') }
let(:blobstore_dir) { Dir.mktmpdir }
let(:local_tmp_dir) { Dir.mktmpdir }
let(:registry_buddy_client) { instance_double(RegistryBuddy::Client) }
let(:package_guid) { 'im-a-package-guid' }
let(:registry) { 'hub.example.com/user' }
let(:min_size) { 4 }
let(:max_size) { 8 }
let(:global_app_bits_cache) do
CloudController::Blobstore::FogClient.new(
connection_config: { provider: 'Local', local_root: blobstore_dir },
directory_key: 'global_app_bits_cache',
min_size: min_size,
max_size: max_size
)
end
let(:fingerprints) do
path = File.join(local_tmp_dir, 'content')
sha = 'some_fake_sha'
File.write(path, 'content')
global_app_bits_cache.cp_to_blobstore(path, sha)
[{ 'fn' => 'path/to/content.txt', 'size' => 123, 'sha1' => sha }]
end
before do
TestConfig.override(directories: { tmpdir: local_tmp_dir }, packages: { image_registry: { base_path: registry } })
allow(CloudController::DependencyLocator.instance).to receive(:global_app_bits_cache).and_return(global_app_bits_cache)
allow(packer).to receive(:max_package_size).and_return(max_package_size)
allow(RegistryBuddy::Client).to receive(:new).and_return(registry_buddy_client)
allow(registry_buddy_client).to receive(:post_package).and_return(
'hash' => { 'algorithm' => 'sha256', 'hex' => 'sha-2-5-6-hex' }
)
FileUtils.cp(input_zip, local_tmp_dir)
FileUtils.mkdir(File.join(local_tmp_dir, '/packages/'))
begin
FileUtils.chmod(0o400, uploaded_files_path)
rescue StandardError
nil
end
Fog.unmock!
end
after do
Fog.mock!
FileUtils.remove_entry_secure local_tmp_dir
FileUtils.remove_entry_secure blobstore_dir
end
describe '#send_package_to_blobstore' do
let(:max_package_size) { nil }
let(:package_guid) { 'package-guid' }
let(:cached_files_fingerprints) { [] }
it 'uploads to the registry and returns the uploaded file hash' do
expect(registry_buddy_client).to receive(:post_package).
with(package_guid, %r{#{local_tmp_dir}/packages/registry_bits_packer}, registry).
and_return('hash' => { 'algorithm' => 'sha256', 'hex' => 'sha-2-5-6-hex' })
result_hash = packer.send_package_to_blobstore(package_guid, uploaded_files_path, [])
expect(result_hash).to eq({ sha1: nil, sha256: 'sha-2-5-6-hex' })
end
context 'when uploading the package to the bits service fails' do
let(:expected_exception) { StandardError.new('some error') }
it 'raises the exception' do
allow(registry_buddy_client).to receive(:post_package).and_raise(expected_exception)
expect do
packer.send_package_to_blobstore(package_guid, uploaded_files_path, [])
end.to raise_error(expected_exception)
end
end
context 'when the package zip file path is nil' do
let(:uploaded_files_path) { nil }
context 'and there are NO cached files' do
let(:cached_files_fingerprints) { [] }
it 'raises an error' do
expect do
packer.send_package_to_blobstore(package_guid, uploaded_files_path, cached_files_fingerprints)
end.to raise_error(CloudController::Errors::ApiError, /Invalid zip/)
end
end
context 'and there are cached files' do
let(:cached_files_fingerprints) { fingerprints }
it 'packs a zip with the cached files' do
expect(registry_buddy_client).to receive(:post_package).
with(package_guid, %r{#{local_tmp_dir}/packages/registry_bits_packer}, registry).
and_return('hash' => { 'algorithm' => 'sha256', 'hex' => 'sha-2-5-6-hex' })
packer.send_package_to_blobstore(package_guid, uploaded_files_path, cached_files_fingerprints)
end
context 'and the combined matched resources are too large' do
let(:max_package_size) { 1 }
it 'raises an exception' do
expect do
packer.send_package_to_blobstore(package_guid, uploaded_files_path, cached_files_fingerprints)
end.to raise_error(CloudController::Errors::ApiError, /may not be larger than/)
end
end
end
end
context 'when the package zip file is missing' do
let(:uploaded_files_path) { File.join(local_tmp_dir, 'file_that_does_not_exist.zip') }
context 'and there are NO cached files' do
let(:cached_files_fingerprints) { [] }
it 'raises an error' do
expect do
packer.send_package_to_blobstore(package_guid, uploaded_files_path, cached_files_fingerprints)
end.to raise_error(CloudController::Errors::ApiError, /Invalid zip/)
end
end
context 'and there are cached files' do
let(:cached_files_fingerprints) { fingerprints }
it 'packs a zip with the cached files' do
expect(registry_buddy_client).to receive(:post_package).
with(package_guid, %r{#{local_tmp_dir}/packages/registry_bits_packer}, registry).
and_return('hash' => { 'algorithm' => 'sha256', 'hex' => 'sha-2-5-6-hex' })
packer.send_package_to_blobstore(package_guid, uploaded_files_path, cached_files_fingerprints)
end
end
end
context 'when the zip file is invalid' do
let(:input_zip) { File.join(Paths::FIXTURES, 'bad.zip') }
let(:uploaded_files_path) { File.join(local_tmp_dir, 'bad.zip') }
it 'raises an informative error' do
expect do
packer.send_package_to_blobstore(package_guid, uploaded_files_path, cached_files_fingerprints)
end.to raise_error(CloudController::Errors::ApiError, /invalid/)
end
end
context 'when the app bits are too large' do
let(:max_package_size) { 1 }
it 'raises an exception' do
expect do
packer.send_package_to_blobstore(package_guid, uploaded_files_path, cached_files_fingerprints)
end.to raise_error(CloudController::Errors::ApiError, /may not be larger than/)
end
it 'does not populate the cache' do
begin
packer.send_package_to_blobstore(package_guid, uploaded_files_path, cached_files_fingerprints)
rescue StandardError
nil
end
sha_of_bye_file_in_good_zip = 'ee9e51458f4642f48efe956962058245ee7127b1'
expect(global_app_bits_cache.exists?(sha_of_bye_file_in_good_zip)).to be false
end
end
describe 'bit caching' do
let(:cached_files_fingerprints) { fingerprints }
it 'uploads the new app bits to the app bit cache' do
packer.send_package_to_blobstore(package_guid, uploaded_files_path, cached_files_fingerprints)
sha_of_bye_file_in_good_zip = 'ee9e51458f4642f48efe956962058245ee7127b1'
expect(global_app_bits_cache.exists?(sha_of_bye_file_in_good_zip)).to be true
end
it 'initializes the fingerprints collection to be scoped to the temporary working directory' do
expect(CloudController::Blobstore::FingerprintsCollection).to receive(:new).with(fingerprints, %r{#{local_tmp_dir}/packages/registry_bits_packer}).and_return([])
packer.send_package_to_blobstore(package_guid, uploaded_files_path, cached_files_fingerprints)
end
context 'when there is an unreadable directory in the zip' do
let(:input_zip) { File.join(Paths::FIXTURES, 'app_packager_zips', 'unreadable_dir.zip') }
let(:input_zip_file_path) { File.join(local_tmp_dir, 'unreadable_dir.zip') }
it 'is able to clean up all files regardless of their permissions in the zip' do
expect do
packer.send_package_to_blobstore(package_guid, input_zip_file_path, [])
end.not_to(change do
Dir.entries(local_tmp_dir)
end)
end
end
context 'when there is an undeletable directory in the zip' do
let(:input_zip) { File.join(Paths::FIXTURES, 'app_packager_zips', 'undeletable_dir.zip') }
let(:input_zip_file_path) { File.join(local_tmp_dir, 'undeletable_dir.zip') }
it 'is able to clean up all files regardless of their permissions in the zip' do
expect do
packer.send_package_to_blobstore(package_guid, input_zip_file_path, [])
end.not_to(change do
Dir.entries(local_tmp_dir)
end)
end
end
context 'when there is an untraversable directory in the zip' do
let(:input_zip) { File.join(Paths::FIXTURES, 'app_packager_zips', 'untraversable_dir.zip') }
let(:input_zip_file_path) { File.join(local_tmp_dir, 'untraversable_dir.zip') }
it 'is able to clean up all files regardless of their permissions in the zip' do
expect do
packer.send_package_to_blobstore(package_guid, input_zip_file_path, [])
end.not_to(change do
Dir.entries(local_tmp_dir)
end)
end
end
context 'when some of the files are symlinks' do
let(:input_zip) { File.join(Paths::FIXTURES, 'express-app.zip') }
let(:uploaded_files_path) { File.join(local_tmp_dir, 'express-app.zip') }
let(:min_size) { 0 }
let(:max_size) { 1_000_000 }
it 'they are not uploaded to the cache but the real files are' do
tempfile = Tempfile.new('external_file.txt')
File.open(tempfile.path, 'w') { |fd| fd.puts 'text goes here' }
symlink_path = File.join(local_tmp_dir, 'link-to-temp.txt')
FileUtils.ln_s(tempfile.path, symlink_path)
# Make the zipfile writable so we can dynamically insert the symlink
FileUtils.chmod(0o600, uploaded_files_path)
`zip -r --symlinks "#{uploaded_files_path}" "#{symlink_path}"`
packer.send_package_to_blobstore(package_guid, uploaded_files_path, [])
expect(global_app_bits_cache.files_for('').size).to be 2
sha_of_cli_js = 'da39a3ee5e6b4b0d3255bfef95601890afd80709'
sha_of_target1_txt = 'f572d396fae9206628714fb2ce00f72e94f2258f'
absolute_link_sha1 = Digester.new.digest_path(tempfile.path)
expect(global_app_bits_cache.exists?(sha_of_cli_js)).to be true
expect(global_app_bits_cache.exists?(sha_of_target1_txt)).to be true
expect(global_app_bits_cache.exists?(absolute_link_sha1)).to be false
end
end
context 'when one of the files exceeds the configured maximum_size' do
it 'is not uploaded to the cache but the others are' do
packer.send_package_to_blobstore(package_guid, uploaded_files_path, cached_files_fingerprints)
sha_of_greetings_file_in_good_zip = '82693f9b3a4857415aeffccd535c375891d96f74'
sha_of_bye_file_in_good_zip = 'ee9e51458f4642f48efe956962058245ee7127b1'
expect(global_app_bits_cache.exists?(sha_of_bye_file_in_good_zip)).to be true
expect(global_app_bits_cache.exists?(sha_of_greetings_file_in_good_zip)).to be false
end
end
context 'when one of the files is less than the configured minimum_size' do
it 'is not uploaded to the cache but the others are' do
packer.send_package_to_blobstore(package_guid, uploaded_files_path, cached_files_fingerprints)
sha_of_hi_file_in_good_zip = '55ca6286e3e4f4fba5d0448333fa99fc5a404a73'
sha_of_bye_file_in_good_zip = 'ee9e51458f4642f48efe956962058245ee7127b1'
expect(global_app_bits_cache.exists?(sha_of_bye_file_in_good_zip)).to be true
expect(global_app_bits_cache.exists?(sha_of_hi_file_in_good_zip)).to be false
end
end
describe 'cached/old app bits' do
context 'when specific file permissions are requested' do
let(:cached_files_fingerprints) do
fingerprints.tap { |prints| prints[0]['mode'] = mode }
end
let(:mode) { '0653' }
describe 'bad file permissions' do
context 'when the write permissions are too-restrictive' do
let(:mode) { '344' }
it 'errors' do
expect do
packer.send_package_to_blobstore(package_guid, uploaded_files_path, cached_files_fingerprints)
end.to raise_error do |error|
expect(error.name).to eq 'AppResourcesFileModeInvalid'
expect(error.response_code).to eq 400
end
end
end
context 'when the permissions are nonsense' do
let(:mode) { 'banana' }
it 'errors' do
expect do
packer.send_package_to_blobstore(package_guid, uploaded_files_path, cached_files_fingerprints)
end.to raise_error do |error|
expect(error.name).to eq 'AppResourcesFileModeInvalid'
expect(error.response_code).to eq 400
end
end
end
end
end
end
end
end
end
end