From 25a3f54b60ea2f35e31af819f9ea31bd2328359c Mon Sep 17 00:00:00 2001 From: Pavel Busko Date: Mon, 22 Apr 2024 13:51:29 +0200 Subject: [PATCH] Fix tests and validate Docker URI (#310) * Fix tests * check if docker URI is valid --------- Co-authored-by: Johannes Dillmann --- app/actions/build_create.rb | 1 - .../diego/cnb/lifecycle_protocol.rb | 1 - .../diego/cnb/staging_action_builder.rb | 2 - .../diego/cnb/task_action_builder.rb | 8 +- .../diego/docker/docker_uri_converter.rb | 48 +-- .../lifecycles/app_lifecycle_provider.rb | 1 - lib/utils/uri_utils.rb | 49 ++- .../documentation/feature_flags_api_spec.rb | 2 +- .../controllers/v3/builds_controller_spec.rb | 69 ++++ .../cnb/buildpack_entry_generator_spec.rb | 127 +++++++ .../diego/cnb/desired_lrp_builder_spec.rb | 341 ++++++++++++++++++ .../diego/cnb/droplet_url_generator_spec.rb | 49 +++ .../cnb/staging_completion_handler_spec.rb | 12 +- .../diego/cnb/task_action_builder_spec.rb | 246 +++++++++++++ .../diego/docker/docker_uri_converter_spec.rb | 3 +- .../diego/lifecycles/cnb_lifecycle_spec.rb | 197 ++++++++++ .../lifecycles/lifecycle_provider_spec.rb | 16 + .../lib/cloud_controller/diego/stager_spec.rb | 15 + spec/unit/lib/utils/uri_utils_spec.rb | 13 + 19 files changed, 1137 insertions(+), 63 deletions(-) create mode 100644 spec/unit/lib/cloud_controller/diego/cnb/buildpack_entry_generator_spec.rb create mode 100644 spec/unit/lib/cloud_controller/diego/cnb/desired_lrp_builder_spec.rb create mode 100644 spec/unit/lib/cloud_controller/diego/cnb/droplet_url_generator_spec.rb create mode 100644 spec/unit/lib/cloud_controller/diego/cnb/task_action_builder_spec.rb create mode 100644 spec/unit/lib/cloud_controller/diego/lifecycles/cnb_lifecycle_spec.rb diff --git a/app/actions/build_create.rb b/app/actions/build_create.rb index dacce2ab94c..eabb3a1e64b 100644 --- a/app/actions/build_create.rb +++ b/app/actions/build_create.rb @@ -96,7 +96,6 @@ def create_and_stage(package:, lifecycle:, metadata: nil, start_after_staging: f def requested_buildpacks_disabled!(lifecycle) return if lifecycle.type == Lifecycles::DOCKER - # TODO: do we need to to something? return if lifecycle.type == Lifecycles::CNB admin_buildpack_records = lifecycle.buildpack_infos.map(&:buildpack_record).compact diff --git a/lib/cloud_controller/diego/cnb/lifecycle_protocol.rb b/lib/cloud_controller/diego/cnb/lifecycle_protocol.rb index a674c59603e..d57cadc46fe 100644 --- a/lib/cloud_controller/diego/cnb/lifecycle_protocol.rb +++ b/lib/cloud_controller/diego/cnb/lifecycle_protocol.rb @@ -38,7 +38,6 @@ def lifecycle_data(staging_details) raise InvalidDownloadUri.new("Failed to get blobstore download url for package #{staging_details.package.guid}") end - # Called from the task_recipe_builder def staging_action_builder(config, staging_details) StagingActionBuilder.new(config, staging_details, lifecycle_data(staging_details)) end diff --git a/lib/cloud_controller/diego/cnb/staging_action_builder.rb b/lib/cloud_controller/diego/cnb/staging_action_builder.rb index 00e7ae4d483..0087efee10e 100644 --- a/lib/cloud_controller/diego/cnb/staging_action_builder.rb +++ b/lib/cloud_controller/diego/cnb/staging_action_builder.rb @@ -47,7 +47,6 @@ def image_layers ] if lifecycle_data[:app_bits_checksum][:type] == 'sha256' - # Type is exclusive, will be converted to DownloadActions layers << ::Diego::Bbs::Models::ImageLayer.new({ name: 'app package', url: lifecycle_data[:app_bits_download_uri], @@ -61,7 +60,6 @@ def image_layers return unless lifecycle_data[:build_artifacts_cache_download_uri] && lifecycle_data[:buildpack_cache_checksum].present? - # Type is exclusive, will be converted to DownloadActions layers << ::Diego::Bbs::Models::ImageLayer.new({ name: 'build artifacts cache', url: lifecycle_data[:build_artifacts_cache_download_uri], diff --git a/lib/cloud_controller/diego/cnb/task_action_builder.rb b/lib/cloud_controller/diego/cnb/task_action_builder.rb index 336f9937df0..f4705798f99 100644 --- a/lib/cloud_controller/diego/cnb/task_action_builder.rb +++ b/lib/cloud_controller/diego/cnb/task_action_builder.rb @@ -31,8 +31,8 @@ def image_layers return [] unless @config.get(:diego, :enable_declarative_asset_downloads) [::Diego::Bbs::Models::ImageLayer.new( - name: 'docker-lifecycle', - url: LifecycleBundleUriGenerator.uri(config.get(:diego, :lifecycle_bundles)[:docker]), + name: "cnb-#{lifecycle_stack}-lifecycle", + url: LifecycleBundleUriGenerator.uri(config.get(:diego, :lifecycle_bundles)[lifecycle_bundle_key]), destination_path: '/tmp/lifecycle', layer_type: ::Diego::Bbs::Models::ImageLayer::Type::SHARED, media_type: ::Diego::Bbs::Models::ImageLayer::MediaType::TGZ @@ -51,7 +51,7 @@ def stack end def lifecycle_bundle_key - :docker + :"cnb/#{lifecycle_stack}" end def cached_dependencies @@ -61,7 +61,7 @@ def cached_dependencies [::Diego::Bbs::Models::CachedDependency.new( from: LifecycleBundleUriGenerator.uri(bundle), to: '/tmp/lifecycle', - cache_key: 'docker-lifecycle' + cache_key: "cnb-#{lifecycle_stack}-lifecycle" )] end diff --git a/lib/cloud_controller/diego/docker/docker_uri_converter.rb b/lib/cloud_controller/diego/docker/docker_uri_converter.rb index 1d71d06bfa9..9f7e5bfa3e2 100644 --- a/lib/cloud_controller/diego/docker/docker_uri_converter.rb +++ b/lib/cloud_controller/diego/docker/docker_uri_converter.rb @@ -1,52 +1,12 @@ +require 'utils/uri_utils' + module VCAP::CloudController class DockerURIConverter - DOCKER_INDEX_SERVER = 'docker.io'.freeze - - class InvalidDockerURI < StandardError; end - def convert(docker_uri) - raise InvalidDockerURI.new "Docker URI [#{docker_uri}] should not contain scheme" if docker_uri.include? '://' + raise UriUtils::InvalidDockerURI.new "Docker URI [#{docker_uri}] should not contain scheme" if docker_uri.include? '://' - host, path, tag = parse_docker_repo_url(docker_uri) + host, path, tag = UriUtils.parse_docker_uri(docker_uri) Addressable::URI.new(scheme: 'docker', host: host, path: path, fragment: tag).to_s end - - private - - def parse_docker_repo_url(docker_uri) - name_parts = docker_uri.split('/', 2) - - host = name_parts[0] - path = name_parts[1] - - if missing_registry(name_parts) - host = '' - path = docker_uri - end - - path = 'library/' + path if (official_docker_registry(name_parts[0]) || missing_registry(name_parts)) && path.exclude?('/') - - path, tag = parse_docker_repository_tag(path) - - [host, path, tag] - end - - def official_docker_registry(host) - host == DOCKER_INDEX_SERVER - end - - def missing_registry(name_parts) - host = name_parts[0] - name_parts.length == 1 || - (host.exclude?('.') && host.exclude?(':') && host != 'localhost') - end - - def parse_docker_repository_tag(path) - path, tag = path.split(':', 2) - - return [path, tag] unless tag && tag.include?('/') - - [path, ''] - end end end diff --git a/lib/cloud_controller/diego/lifecycles/app_lifecycle_provider.rb b/lib/cloud_controller/diego/lifecycles/app_lifecycle_provider.rb index ee8e66f0ed7..39c9e9d5085 100644 --- a/lib/cloud_controller/diego/lifecycles/app_lifecycle_provider.rb +++ b/lib/cloud_controller/diego/lifecycles/app_lifecycle_provider.rb @@ -20,7 +20,6 @@ def self.provide_for_update(message, app) end def self.provide(message, app) - # Retrieving CNB lifecycle type from message is easy, how to get it from the app (model) in case of provide_for_update? type = if message.lifecycle_type.present? message.lifecycle_type elsif !app.nil? diff --git a/lib/utils/uri_utils.rb b/lib/utils/uri_utils.rb index 3c05251d5fc..8b5a0822cb9 100644 --- a/lib/utils/uri_utils.rb +++ b/lib/utils/uri_utils.rb @@ -3,6 +3,9 @@ module UriUtils SSH_REGEX = %r{ \A (?:ssh://)? git@ .+? : .+? \.git \z }x GIT_REGEX = %r{ \A git:// .+? : .+? \.git \z }x + DOCKER_INDEX_SERVER = 'docker.io'.freeze + + class InvalidDockerURI < StandardError; end def self.is_uri?(candidate) !!(candidate.is_a?(String) && /\A#{URI::DEFAULT_PARSER.make_regexp}\Z/ =~ candidate && URI(candidate)) @@ -19,9 +22,12 @@ def self.is_buildpack_uri?(candidate) def self.is_cnb_buildpack_uri?(candidate) return false unless candidate.is_a?(String) + return is_uri?(candidate) if candidate.start_with?(%r{\Ahttp(s)?://}x) + return !!parse_docker_uri(candidate.split('://').last) if candidate.start_with?('docker://') - ['http://', 'https://', 'docker://'].any? { |prefix| candidate.start_with?(prefix) } - # TODO: Should we validate the uri? + false + rescue StandardError + false end def self.is_uri_path?(candidate) @@ -43,4 +49,43 @@ def self.uri_escape(uri) end.join('&') [parts[0].tr(' ', '+'), query].join('?') end + + def self.parse_docker_uri(docker_uri) + name_parts = docker_uri.split('/', 2) + + host = name_parts[0] + path = name_parts[1] + + if missing_registry(name_parts) + host = '' + path = docker_uri + end + + path = 'library/' + path if (official_docker_registry(name_parts[0]) || missing_registry(name_parts)) && path.exclude?('/') + + path, tag = parse_docker_repository_tag(path) + + raise InvalidDockerURI.new "Invalid image name [#{path}]" unless %r{\A[a-z0-9_\-\.\/]{2,255}\Z} =~ path + raise InvalidDockerURI.new "Invalid image tag [#{tag}]" if tag && !(/\A[a-zA-Z0-9_\-\.]{1,128}(@sha256:[a-z0-9]{64})?\Z/ =~ tag) + + [host, path, tag] + end + + private_class_method def self.official_docker_registry(host) + host == DOCKER_INDEX_SERVER + end + + private_class_method def self.missing_registry(name_parts) + host = name_parts[0] + name_parts.length == 1 || + (host.exclude?('.') && host.exclude?(':') && host != 'localhost') + end + + private_class_method def self.parse_docker_repository_tag(path) + path, tag = path.split(':', 2) + + return [path, tag] unless tag && tag.include?('/') + + [path, 'latest'] + end end diff --git a/spec/api/documentation/feature_flags_api_spec.rb b/spec/api/documentation/feature_flags_api_spec.rb index 5bdb3091fe2..ce55fbd2c6b 100644 --- a/spec/api/documentation/feature_flags_api_spec.rb +++ b/spec/api/documentation/feature_flags_api_spec.rb @@ -18,7 +18,7 @@ client.get '/v2/config/feature_flags', {}, headers expect(status).to eq(200) - expect(parsed_response.length).to eq(17) + expect(parsed_response.length).to eq(18) expect(parsed_response).to include( { 'name' => 'user_org_creation', diff --git a/spec/unit/controllers/v3/builds_controller_spec.rb b/spec/unit/controllers/v3/builds_controller_spec.rb index 381f39fb300..01d48c8180b 100644 --- a/spec/unit/controllers/v3/builds_controller_spec.rb +++ b/spec/unit/controllers/v3/builds_controller_spec.rb @@ -460,6 +460,75 @@ end end + describe 'cnb lifecycle' do + let(:cnb_app_model) { VCAP::CloudController::AppModel.make(:cnb, space:) } + let(:package) do + VCAP::CloudController::PackageModel.make(:cnb, + app_guid: cnb_app_model.guid, + type: VCAP::CloudController::PackageModel::BITS_TYPE, + state: VCAP::CloudController::PackageModel::READY_STATE) + end + + let(:cnb_lifecycle) do + { type: 'cnb', data: {} } + end + + let(:req_body) do + { + package: { + guid: package.guid + }, + lifecycle: cnb_lifecycle + } + end + + before do + expect(cnb_app_model.lifecycle_type).to eq('cnb') + end + + context 'when diego_cnb is enabled' do + before do + VCAP::CloudController::FeatureFlag.make(name: 'diego_cnb', enabled: true, error_message: nil) + end + + it 'returns a 201 Created response and creates a build model with an associated package' do + expect { post :create, params: req_body, as: :json }. + to change(VCAP::CloudController::BuildModel, :count).from(0).to(1) + build = VCAP::CloudController::BuildModel.last + expect(build.package.guid).to eq(package.guid) + + expect(response).to have_http_status :created + end + + context 'when the user adds additional body parameters' do + let(:cnb_lifecycle) do + { type: 'cnb', data: 'foobar' } + end + + it 'raises a 422' do + post :create, params: req_body, as: :json + + expect(response).to have_http_status(:unprocessable_entity) + expect(response.body).to include('UnprocessableEntity') + end + end + end + + context 'when diego_cnb feature flag is disabled' do + before do + VCAP::CloudController::FeatureFlag.make(name: 'diego_cnb', enabled: false, error_message: nil) + end + + it 'raises 403' do + post :create, params: req_body, as: :json + + expect(response).to have_http_status(:forbidden) + expect(response.body).to include('FeatureDisabled') + expect(response.body).to include('diego_cnb') + end + end + end + describe 'staging_details' do let(:memory_in_mb) { TestConfig.config[:staging][:minimum_staging_memory_mb] + 1 } let(:disk_in_mb) { TestConfig.config[:staging][:minimum_staging_disk_mb] + 1 } diff --git a/spec/unit/lib/cloud_controller/diego/cnb/buildpack_entry_generator_spec.rb b/spec/unit/lib/cloud_controller/diego/cnb/buildpack_entry_generator_spec.rb new file mode 100644 index 00000000000..090bee115e0 --- /dev/null +++ b/spec/unit/lib/cloud_controller/diego/cnb/buildpack_entry_generator_spec.rb @@ -0,0 +1,127 @@ +require 'spec_helper' +require 'cloud_controller/diego/buildpack/buildpack_entry_generator' + +module VCAP::CloudController + module Diego + module CNB + RSpec.describe BuildpackEntryGenerator do + subject(:buildpack_entry_generator) { BuildpackEntryGenerator.new(blobstore_url_generator) } + + let(:admin_buildpack_download_url) { 'http://admin-buildpack.example.com' } + let(:app_package_download_url) { 'http://app-package.example.com' } + let(:build_artifacts_cache_download_uri) { 'http://buildpack-artifacts-cache.example.com' } + + let(:blobstore_url_generator) { double('fake url generator') } + let(:stack) { Stack.make } + let(:stack2) { Stack.make } + + let!(:java_buildpack) do + VCAP::CloudController::Buildpack.create(name: 'java', stack: stack.name, key: 'java-buildpack-key', position: 1, sha256_checksum: 'checksum') + end + let!(:ruby_buildpack) do + VCAP::CloudController::Buildpack.create(name: 'ruby', stack: stack.name, key: 'ruby-buildpack-key', position: 2, sha256_checksum: 'checksum') + end + let!(:ruby_buildpack_other_stack) do + VCAP::CloudController::Buildpack.create(name: 'ruby', stack: stack2.name, key: 'ruby-buildpack-stack2-key', position: 3, sha256_checksum: 'checksum') + end + + before do + allow(blobstore_url_generator).to receive_messages(app_package_download_url: app_package_download_url, admin_buildpack_download_url: admin_buildpack_download_url, + buildpack_cache_download_url: build_artifacts_cache_download_uri) + + allow(EM).to receive(:add_timer) + allow(EM).to receive(:defer).and_yield + end + + describe '#buildpack_entries' do + let(:v3_app) { AppModel.make } + let(:package) { PackageModel.make(app_guid: v3_app.guid) } + let(:buildpack_info) { BuildpackInfo.new(buildpack, VCAP::CloudController::Buildpack.find(name: buildpack)) } + let(:buildpack_infos) { [buildpack_info] } + + context 'when the user has requested a custom and admin buildpack' do + let(:custom_buildpack_info) { BuildpackInfo.new('http://example.com/my_buildpack_url.zip', nil) } + let(:admin_buildpack_info) { BuildpackInfo.new('java', VCAP::CloudController::Buildpack.find(name: 'java')) } + let(:buildpack_infos) { [custom_buildpack_info, admin_buildpack_info] } + + it 'returns both buildpacks for the stack' do + expect(buildpack_entry_generator.buildpack_entries(buildpack_infos, stack.name)).to eq([ + { name: 'custom', key: 'http://example.com/my_buildpack_url.zip', url: 'http://example.com/my_buildpack_url.zip', skip_detect: true }, + { name: 'java', key: 'java-buildpack-key', url: admin_buildpack_download_url, skip_detect: true, sha256: 'checksum' } + ]) + end + end + + context 'when the user has requested a custom buildpack' do + context 'when the buildpack_uri ends with .zip' do + let(:buildpack) { 'http://example.com/my_buildpack_url.zip' } + + it "uses the buildpack_uri and name it 'custom', and use the url as the key" do + expect(buildpack_entry_generator.buildpack_entries(buildpack_infos, stack.name)).to eq([ + { name: 'custom', key: 'http://example.com/my_buildpack_url.zip', url: 'http://example.com/my_buildpack_url.zip', skip_detect: true } + ]) + end + end + + context 'when the buildpack_uri does not end with .zip' do + let(:buildpack) { 'http://example.com/my_buildpack_url' } + + it "uses the buildpack_uri and name it 'custom', and use the url as the key" do + expect(buildpack_entry_generator.buildpack_entries(buildpack_infos, stack.name)).to eq([ + { name: 'custom', key: 'http://example.com/my_buildpack_url', url: 'http://example.com/my_buildpack_url', skip_detect: true } + ]) + end + end + end + + context 'when the user has requested an admin buildpack' do + let(:buildpack) { 'java' } + + it 'uses that buildpack' do + expect(buildpack_entry_generator.buildpack_entries(buildpack_infos, stack.name)).to eq([ + { name: 'java', key: 'java-buildpack-key', url: admin_buildpack_download_url, skip_detect: true, sha256: 'checksum' } + ]) + end + + context 'when the buildpack is disabled' do + before do + java_buildpack.update(enabled: false) + end + + it 'fails fast with a clear error' do + expect { buildpack_entry_generator.buildpack_entries(buildpack_infos, stack.name) }.to raise_error(/Unsupported buildpack type/) + end + end + end + + context 'when the user has not requested a buildpack' do + let(:buildpack_infos) { [] } + + it 'uses the list of admin buildpacks for the specified stack' do + expect(buildpack_entry_generator.buildpack_entries(buildpack_infos, stack.name)).to eq([ + { name: 'java', key: 'java-buildpack-key', url: admin_buildpack_download_url, sha256: 'checksum', skip_detect: false }, + { name: 'ruby', key: 'ruby-buildpack-key', url: admin_buildpack_download_url, sha256: 'checksum', skip_detect: false } + ]) + end + + it 'uses the list of all admin buildpacks if no stack is specified' do + expect(buildpack_entry_generator.buildpack_entries(buildpack_infos, nil)).to eq([ + { name: 'java', key: 'java-buildpack-key', url: admin_buildpack_download_url, sha256: 'checksum', skip_detect: false }, + { name: 'ruby', key: 'ruby-buildpack-key', url: admin_buildpack_download_url, sha256: 'checksum', skip_detect: false }, + { name: 'ruby', key: 'ruby-buildpack-stack2-key', url: admin_buildpack_download_url, sha256: 'checksum', skip_detect: false } + ]) + end + end + + context 'when an invalid buildpack type is returned for some reason' do + let(:buildpack) { '???' } + + it 'fails fast with a clear error' do + expect { buildpack_entry_generator.buildpack_entries(buildpack_infos, stack.name) }.to raise_error(/Unsupported buildpack type/) + end + end + end + end + end + end +end diff --git a/spec/unit/lib/cloud_controller/diego/cnb/desired_lrp_builder_spec.rb b/spec/unit/lib/cloud_controller/diego/cnb/desired_lrp_builder_spec.rb new file mode 100644 index 00000000000..04a5f00789a --- /dev/null +++ b/spec/unit/lib/cloud_controller/diego/cnb/desired_lrp_builder_spec.rb @@ -0,0 +1,341 @@ +require 'spec_helper' + +module VCAP::CloudController + module Diego + module CNB + RSpec.describe DesiredLrpBuilder do + subject(:builder) { DesiredLrpBuilder.new(config, opts) } + before do + Stack.create(name: 'potato-stack') + Stack.create(name: 'stack-thats-not-in-config') + end + + let(:stack) { 'potato-stack' } + let(:opts) do + { + stack: stack, + droplet_uri: 'http://droplet-uri.com:1234?token=&@home--->', + droplet_hash: 'droplet-hash', + process_guid: 'p-guid', + ports: ports, + checksum_algorithm: 'checksum-algorithm', + checksum_value: 'checksum-value', + start_command: 'dd if=/dev/random of=/dev/null' + } + end + let(:ports) { [1111, 2222, 3333] } + let(:config) do + Config.new({ + diego: { + file_server_url: 'http://file-server.example.com', + lifecycle_bundles: lifecycle_bundles, + droplet_destinations: droplet_destinations, + use_privileged_containers_for_running: use_privileged_containers_for_running, + enable_declarative_asset_downloads: enable_declarative_asset_downloads + } + }) + end + let(:lifecycle_bundles) do + { "cnb/#{stack}": '/path/to/lifecycle.tgz' } + end + let(:droplet_destinations) do + { stack.to_sym => '/value/from/config/based/on/stack' } + end + let(:use_privileged_containers_for_running) { false } + let(:enable_declarative_asset_downloads) { false } + + describe '#start_command' do + it 'returns the passed in start command' do + expect(builder.start_command).to eq('dd if=/dev/random of=/dev/null') + end + end + + describe '#root_fs' do + it 'returns a constructed root_fs' do + expect(builder.root_fs).to eq('preloaded:potato-stack') + end + + context 'when enable_declarative_asset_downloads is true' do + let(:enable_declarative_asset_downloads) { true } + + it 'returns a constructed root_fs' do + expect(builder.root_fs).to eq('preloaded:potato-stack') + end + end + + context 'when the stack does not exist' do + let(:stack) { 'does-not-exist' } + + it 'raises an error' do + expect do + builder.root_fs + end.to raise_error CloudController::Errors::ApiError, /The stack could not be found/ + end + end + + context 'when the stack has separate run and build root_fs images' do + let(:stack) { 'two-images-stack' } + + before do + Stack.create( + name: stack, + description: 'a stack with separate build and run rootfses', + run_rootfs_image: 'run-image', + build_rootfs_image: 'build-image' + ) + end + + it 'returns the run root_fs' do + expect(builder.root_fs).to eq('preloaded:run-image') + end + end + end + + describe '#cached_dependencies' do + before do + allow(LifecycleBundleUriGenerator).to receive(:uri).and_return('foo://bar.baz') + end + + it 'returns an array of CachedDependency objects' do + expect(builder.cached_dependencies).to eq([ + ::Diego::Bbs::Models::CachedDependency.new( + from: 'foo://bar.baz', + to: '/tmp/lifecycle', + cache_key: 'cnb-potato-stack-lifecycle' + ) + ]) + expect(LifecycleBundleUriGenerator).to have_received(:uri).with('/path/to/lifecycle.tgz') + end + + context 'when searching for a nonexistant stack' do + let(:lifecycle_bundles) do + { 'hot-potato': '/path/to/lifecycle.tgz' } + end + let(:stack) { 'stack-thats-not-in-config' } + + it 'errors nicely' do + expect { builder.cached_dependencies }.to raise_error("no compiler defined for requested stack 'stack-thats-not-in-config'") + end + end + + context 'when enable_declarative_asset_downloads is true' do + let(:enable_declarative_asset_downloads) { true } + + it 'returns nil' do + expect(builder.cached_dependencies).to be_nil + end + end + end + + describe '#setup' do + it 'creates a setup action to download the droplet' do + expect(builder.setup).to eq( + ::Diego::Bbs::Models::Action.new( + serial_action: ::Diego::Bbs::Models::SerialAction.new( + actions: [ + ::Diego::Bbs::Models::Action.new( + download_action: ::Diego::Bbs::Models::DownloadAction.new( + artifact: 'droplet', + to: '.', + user: 'vcap', + from: 'http://droplet-uri.com:1234?token=&@home--->', + cache_key: 'droplets-p-guid', + checksum_algorithm: 'checksum-algorithm', + checksum_value: 'checksum-value' + ) + ) + ] + ) + ) + ) + end + + context 'when enable_declarative_asset_downloads is true' do + let(:enable_declarative_asset_downloads) { true } + + context 'and the droplet does not have a sha256 checksum (it is a legacy droplet with a sha1 checksum)' do + # this test can be removed once legacy sha1 checksummed droplets are obsolete + let(:opts) { super().merge(checksum_algorithm: 'sha1') } + + it 'creates a setup action to download the droplet' do + expect(builder.setup).to eq( + ::Diego::Bbs::Models::Action.new( + serial_action: ::Diego::Bbs::Models::SerialAction.new( + actions: [ + ::Diego::Bbs::Models::Action.new( + download_action: ::Diego::Bbs::Models::DownloadAction.new( + artifact: 'droplet', + to: '.', + user: 'vcap', + from: 'http://droplet-uri.com:1234?token=&@home--->', + cache_key: 'droplets-p-guid', + checksum_algorithm: 'sha1', + checksum_value: 'checksum-value' + ) + ) + ] + ) + ) + ) + end + end + + context 'when checksum is sha256' do + let(:opts) { super().merge(checksum_algorithm: 'sha256') } + + it 'returns nil' do + expect(builder.setup).to be_nil + end + end + end + end + + describe '#image_layers' do + before do + allow(LifecycleBundleUriGenerator).to receive(:uri).and_return('foo://bar.baz') + end + + it 'returns empty array' do + expect(builder.image_layers).to be_empty + end + + context 'when enable_declarative_asset_downloads is true' do + let(:enable_declarative_asset_downloads) { true } + + context 'and the droplet does not have a sha256 checksum (it is a legacy droplet with a sha1 checksum)' do + # this test can be removed once legacy sha1 checksummed droplets are obsolete + let(:opts) { super().merge(checksum_algorithm: 'sha1') } + + it 'creates a image layer for each cached dependency' do + expect(builder.image_layers).to eq([ + ::Diego::Bbs::Models::ImageLayer.new( + name: 'cnb-potato-stack-lifecycle', + url: 'foo://bar.baz', + destination_path: '/tmp/lifecycle', + layer_type: ::Diego::Bbs::Models::ImageLayer::Type::SHARED, + media_type: ::Diego::Bbs::Models::ImageLayer::MediaType::TGZ + ) + ]) + end + end + + context 'and the droplet has a sha256 checksum' do + let(:opts) { super().merge(checksum_algorithm: 'sha256') } + + it 'creates a image layer for each cached dependency' do + expect(builder.image_layers).to include( + ::Diego::Bbs::Models::ImageLayer.new( + name: 'cnb-potato-stack-lifecycle', + url: 'foo://bar.baz', + destination_path: '/tmp/lifecycle', + layer_type: ::Diego::Bbs::Models::ImageLayer::Type::SHARED, + media_type: ::Diego::Bbs::Models::ImageLayer::MediaType::TGZ + ) + ) + end + + it 'creates a image layer for the droplet' do + expect(builder.image_layers).to include( + ::Diego::Bbs::Models::ImageLayer.new( + name: 'droplet', + url: 'http://droplet-uri.com:1234?token=&%40home---%3E', + destination_path: '/value/from/config/based/on/stack', + layer_type: ::Diego::Bbs::Models::ImageLayer::Type::EXCLUSIVE, + media_type: ::Diego::Bbs::Models::ImageLayer::MediaType::TGZ, + digest_value: 'checksum-value', + digest_algorithm: ::Diego::Bbs::Models::ImageLayer::DigestAlgorithm::SHA256 + ) + ) + end + + context "when searching for a lifecycle associated with a stack that is not configured in the Cloud Controller's lifecycle_bundles config" do + let(:lifecycle_bundles) do + { 'hot-potato': '/path/to/lifecycle.tgz' } + end + let(:stack) { 'stack-thats-not-in-config' } + + it 'errors nicely' do + expect { builder.image_layers }.to raise_error("no compiler defined for requested stack 'stack-thats-not-in-config'") + end + end + + context "when searching for a droplet destination associated with a stack that is not configured in the Cloud Controller's droplet_destinations config" do + let(:droplet_destinations) do + { 'hot-potato': '/value/from/config/based/on/stack' } + end + let(:stack) { 'stack-thats-not-in-config' } + + it 'errors nicely' do + expect { builder.image_layers }.to raise_error("no droplet destination defined for requested stack 'stack-thats-not-in-config'") + end + end + end + end + end + + describe '#global_environment_variables' do + it 'returns a list' do + expect(builder.global_environment_variables).to contain_exactly(::Diego::Bbs::Models::EnvironmentVariable.new(name: 'LANG', value: DEFAULT_LANG), + ::Diego::Bbs::Models::EnvironmentVariable.new(name: 'CNB_LAYERS_DIR', value: '/home/vcap/layers'), + ::Diego::Bbs::Models::EnvironmentVariable.new(name: 'CNB_APP_DIR', value: '/home/vcap/workspace')) + end + end + + describe '#privileged?' do + context 'when the config is true' do + before do + config.set(:diego, config.get(:diego).merge(use_privileged_containers_for_running: true)) + end + + it 'returns true' do + expect(builder.privileged?).to be(true) + end + end + + context 'when the config is false' do + before do + config.set(:diego, config.get(:diego).merge(use_privileged_containers_for_running: false)) + end + + it 'returns false' do + expect(builder.privileged?).to be(false) + end + end + end + + describe '#ports' do + it 'returns the ports array' do + expect(builder.ports).to eq([1111, 2222, 3333]) + end + + context 'when the ports array is nil' do + let(:ports) { nil } + + it 'returns an array of the default' do + expect(builder.ports).to eq([DEFAULT_APP_PORT]) + end + end + + context 'when the ports array is empty' do + let(:ports) { [] } + + it 'returns an array of the default' do + expect(builder.ports).to eq([DEFAULT_APP_PORT]) + end + end + end + + describe '#port_environment_variables' do + let(:ports) { [11, 22, 33] } + + it 'returns the array of environment variables' do + env_var1 = ::Diego::Bbs::Models::EnvironmentVariable.new(name: 'PORT', value: '11') + expected_env_vars = [env_var1] + + expect(builder.port_environment_variables).to match_array(expected_env_vars) + end + end + end + end + end +end diff --git a/spec/unit/lib/cloud_controller/diego/cnb/droplet_url_generator_spec.rb b/spec/unit/lib/cloud_controller/diego/cnb/droplet_url_generator_spec.rb new file mode 100644 index 00000000000..80dbeb01e91 --- /dev/null +++ b/spec/unit/lib/cloud_controller/diego/cnb/droplet_url_generator_spec.rb @@ -0,0 +1,49 @@ +require 'spec_helper' +require 'cloud_controller/diego/buildpack/droplet_url_generator' + +module VCAP::CloudController + module Diego + module CNB + RSpec.describe DropletUrlGenerator do + let(:hostname) { 'api.internal.cf' } + let(:external_port) { 8181 } + let(:tls_port) { 8182 } + let(:mtls) { false } + + subject(:generator) do + DropletUrlGenerator.new( + internal_service_hostname: hostname, + external_port: external_port, + tls_port: tls_port, + mtls: mtls + ) + end + + describe '#perma_droplet_download_url' do + let(:app_guid) { 'random-guid' } + let(:droplet_checksum) { '12345' } + + it 'gives out a url to the cloud controller' do + download_url = "http://api.internal.cf:8181/internal/v2/droplets/#{app_guid}/#{droplet_checksum}/download" + expect(generator.perma_droplet_download_url(app_guid, droplet_checksum)).to eql(download_url) + end + + context 'when no droplet_hash' do + it 'returns nil if no droplet_hash' do + expect(generator.perma_droplet_download_url(app_guid, nil)).to be_nil + end + end + + context 'when mTLS is enabled' do + let(:mtls) { true } + + it 'gives out a url to the cloud controller using mTLS' do + download_url = "https://api.internal.cf:8182/internal/v4/droplets/#{app_guid}/#{droplet_checksum}/download" + expect(generator.perma_droplet_download_url(app_guid, droplet_checksum)).to eql(download_url) + end + end + end + end + end + end +end diff --git a/spec/unit/lib/cloud_controller/diego/cnb/staging_completion_handler_spec.rb b/spec/unit/lib/cloud_controller/diego/cnb/staging_completion_handler_spec.rb index 255ed27e953..21364fcb828 100644 --- a/spec/unit/lib/cloud_controller/diego/cnb/staging_completion_handler_spec.rb +++ b/spec/unit/lib/cloud_controller/diego/cnb/staging_completion_handler_spec.rb @@ -342,7 +342,7 @@ module CNB subject.staging_complete(success_response) expect(logger).to have_received(:error).with( - 'diego.staging.buildpack.saving-staging-result-failed', + 'diego.staging.cnb.saving-staging-result-failed', hash_including( staging_guid: build.guid, response: success_response, @@ -354,7 +354,7 @@ module CNB it 'does not attempt to start the app' do expect(runner).not_to receive(:start) expect(logger).not_to receive(:error).with( - 'diego.staging.buildpack.starting-process-failed', anything + 'diego.staging.cnb.starting-process-failed', anything ) subject.staging_complete(success_response, true) @@ -526,7 +526,7 @@ module CNB it 'logs an error for the CF operator' do expect(logger).to have_received(:error).with( - 'diego.staging.buildpack.success.invalid-message', + 'diego.staging.cnb.success.invalid-message', staging_guid: build.guid, payload: malformed_success_response, error: '{ result => Missing key }' @@ -548,7 +548,7 @@ module CNB it 'logs a helpful error' do expect(logger).to have_received(:error).with( - 'diego.staging.buildpack.success.invalid-message', + 'diego.staging.cnb.success.invalid-message', staging_guid: build.guid, payload: malformed_success_response, error: '{ result => unexpected format }' @@ -582,7 +582,7 @@ module CNB end.to raise_error(CloudController::Errors::ApiError) expect(logger).to have_received(:error).with( - 'diego.staging.buildpack.failure.invalid-message', + 'diego.staging.cnb.failure.invalid-message', staging_guid: build.guid, payload: malformed_fail_response, error: '{ error => { message => Missing key } }' @@ -601,7 +601,7 @@ module CNB subject.staging_complete(fail_response) expect(logger).to have_received(:error).with( - 'diego.staging.buildpack.saving-staging-result-failed', + 'diego.staging.cnb.saving-staging-result-failed', hash_including( staging_guid: build.guid, response: fail_response, diff --git a/spec/unit/lib/cloud_controller/diego/cnb/task_action_builder_spec.rb b/spec/unit/lib/cloud_controller/diego/cnb/task_action_builder_spec.rb new file mode 100644 index 00000000000..e4817379567 --- /dev/null +++ b/spec/unit/lib/cloud_controller/diego/cnb/task_action_builder_spec.rb @@ -0,0 +1,246 @@ +require 'spec_helper' + +module VCAP::CloudController + module Diego + module CNB + RSpec.describe TaskActionBuilder do + subject(:builder) { TaskActionBuilder.new(config, task, lifecycle_data) } + + let(:enable_declarative_asset_downloads) { false } + let(:config) do + Config.new({ + diego: { + enable_declarative_asset_downloads: enable_declarative_asset_downloads, + lifecycle_bundles: { + 'cnb/potato-stack': 'http://file-server.service.cf.internal:8080/v1/static/potato_lifecycle_bundle_url' + }, + droplet_destinations: droplet_destinations + } + }) + end + let(:droplet_destinations) do + { stack.to_sym => '/value/from/config/based/on/stack' } + end + let(:task) { TaskModel.make command: command, name: 'my-task' } + let(:command) { 'echo "hello"' } + + let(:generated_environment) do + [ + ::Diego::Bbs::Models::EnvironmentVariable.new(name: 'VCAP_APPLICATION', value: '{"greg":"pants"}'), + ::Diego::Bbs::Models::EnvironmentVariable.new(name: 'MEMORY_LIMIT', value: '256m'), + ::Diego::Bbs::Models::EnvironmentVariable.new(name: 'VCAP_SERVICES', value: '{}') + ] + end + + let(:download_uri) { 'http://download_droplet.example.com' } + let(:lifecycle_data) do + { + droplet_uri: download_uri, + stack: stack + } + end + let(:stack) { 'potato-stack' } + + before do + allow(VCAP::CloudController::Diego::TaskEnvironmentVariableCollector).to receive(:for_task).and_return(generated_environment) + TestConfig.override(credhub_api: nil) + end + + describe '#action' do + let(:download_app_droplet_action) do + ::Diego::Bbs::Models::DownloadAction.new( + from: download_uri, + to: '.', + cache_key: '', + user: 'vcap', + checksum_algorithm: 'sha256', + checksum_value: task.droplet.sha256_checksum + ) + end + + let(:run_task_action) do + ::Diego::Bbs::Models::RunAction.new( + path: '/tmp/lifecycle/launcher', + args: ['app', command, '{}'], + log_source: 'APP/TASK/my-task', + user: 'root', + resource_limits: ::Diego::Bbs::Models::ResourceLimits.new, + env: generated_environment + ) + end + + context 'when the droplet does not have a sha256 checksum calculated' do + let(:download_app_droplet_action) do + ::Diego::Bbs::Models::DownloadAction.new( + from: download_uri, + to: '.', + cache_key: '', + user: 'vcap', + checksum_algorithm: 'sha1', + checksum_value: task.droplet.droplet_hash + ) + end + + before do + task.droplet.sha256_checksum = nil + task.droplet.save + end + end + + context 'when enable_declarative_asset_downloads is true' do + let(:enable_declarative_asset_downloads) { true } + + it 'does not include the download step in the action' do + result = builder.action + expect(result.run_action).to eq(run_task_action) + end + + context 'and the droplet does not have a sha256 checksum (it is a legacy droplet with a sha1 checksum)' do + # this test can be removed once legacy sha1 checksummed droplets are obsolete + let(:download_app_droplet_action) do + ::Diego::Bbs::Models::DownloadAction.new( + from: download_uri, + to: '.', + cache_key: '', + user: 'vcap', + checksum_algorithm: 'sha1', + checksum_value: task.droplet.droplet_hash + ) + end + + before do + task.droplet.sha256_checksum = nil + task.droplet.save + end + end + end + end + + describe '#image_layers' do + it 'returns empty array' do + expect(builder.image_layers).to eq [] + end + + context 'when enable_declarative_asset_downloads is true' do + let(:enable_declarative_asset_downloads) { true } + + context 'and the droplet does not have a sha256 checksum (it is a legacy droplet with a sha1 checksum)' do + # this test can be removed once legacy sha1 checksummed droplets are obsolete + before do + task.droplet.sha256_checksum = nil + task.droplet.save + end + + it 'creates a image layer for each cached dependency' do + expect(builder.image_layers).to eq([ + ::Diego::Bbs::Models::ImageLayer.new( + name: 'cnb-potato-stack-lifecycle', + url: 'http://file-server.service.cf.internal:8080/v1/static/potato_lifecycle_bundle_url', + destination_path: '/tmp/lifecycle', + layer_type: ::Diego::Bbs::Models::ImageLayer::Type::SHARED, + media_type: ::Diego::Bbs::Models::ImageLayer::MediaType::TGZ + ) + ]) + end + end + + it 'creates a image layer for each cached dependency' do + expect(builder.image_layers).to include( + ::Diego::Bbs::Models::ImageLayer.new( + name: 'cnb-potato-stack-lifecycle', + url: 'http://file-server.service.cf.internal:8080/v1/static/potato_lifecycle_bundle_url', + destination_path: '/tmp/lifecycle', + layer_type: ::Diego::Bbs::Models::ImageLayer::Type::SHARED, + media_type: ::Diego::Bbs::Models::ImageLayer::MediaType::TGZ + ) + ) + end + + context 'when the requested stack is not in the configured lifecycle bundles' do + let(:stack) { 'leek-stack' } + + it 'returns an error' do + expect do + builder.image_layers + end.to raise_error('no compiler defined for requested stack') + end + end + + context 'when the requested stack is not in the configured droplet destinations' do + let(:stack) { 'leek-stack' } + let(:droplet_destinations) do + { 'yucca' => '/value/from/config/based/on/stack' } + end + end + end + end + + describe '#task_environment_variables' do + it 'returns task environment variables' do + expect(builder.task_environment_variables).to match_array(generated_environment) + expect(VCAP::CloudController::Diego::TaskEnvironmentVariableCollector).to have_received(:for_task).with(task) + end + end + + describe '#stack' do + before do + Stack.create(name: 'potato-stack') + 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:potato-stack') + end + + context 'when the stack does not exist in the database' 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 run image name' do + expect(builder.stack).to eq('preloaded:run-image') + end + end + end + + describe '#cached_dependencies' do + it 'returns a cached dependency for the correct lifecycle given the stack' do + expect(builder.cached_dependencies).to eq([ + ::Diego::Bbs::Models::CachedDependency.new( + from: 'http://file-server.service.cf.internal:8080/v1/static/potato_lifecycle_bundle_url', + to: '/tmp/lifecycle', + cache_key: 'cnb-potato-stack-lifecycle' + ) + ]) + end + + context 'when enable_declarative_asset_downloads is true' do + let(:enable_declarative_asset_downloads) { true } + + it 'returns nil' do + expect(builder.cached_dependencies).to be_nil + end + end + + context 'when the requested stack is not in the configured lifecycle bundles' do + let(:stack) { 'leek-stack' } + + it 'returns an error' do + expect do + builder.cached_dependencies + end.to raise_error VCAP::CloudController::Diego::LifecycleBundleUriGenerator::InvalidStack + end + end + end + end + end + end +end diff --git a/spec/unit/lib/cloud_controller/diego/docker/docker_uri_converter_spec.rb b/spec/unit/lib/cloud_controller/diego/docker/docker_uri_converter_spec.rb index 44a096e067f..96b76fa75af 100644 --- a/spec/unit/lib/cloud_controller/diego/docker/docker_uri_converter_spec.rb +++ b/spec/unit/lib/cloud_controller/diego/docker/docker_uri_converter_spec.rb @@ -1,5 +1,6 @@ require 'spec_helper' require 'cloud_controller/diego/docker/docker_uri_converter' +require 'utils/uri_utils' module VCAP::CloudController RSpec.describe DockerURIConverter do @@ -129,7 +130,7 @@ module VCAP::CloudController it('errors') do expect do converter.convert image_url - end.to raise_error(DockerURIConverter::InvalidDockerURI, 'Docker URI [https://docker.io/repo] should not contain scheme') + end.to raise_error(UriUtils::InvalidDockerURI, 'Docker URI [https://docker.io/repo] should not contain scheme') end end end diff --git a/spec/unit/lib/cloud_controller/diego/lifecycles/cnb_lifecycle_spec.rb b/spec/unit/lib/cloud_controller/diego/lifecycles/cnb_lifecycle_spec.rb new file mode 100644 index 00000000000..e584128d01c --- /dev/null +++ b/spec/unit/lib/cloud_controller/diego/lifecycles/cnb_lifecycle_spec.rb @@ -0,0 +1,197 @@ +require 'spec_helper' +require_relative 'lifecycle_shared' + +module VCAP::CloudController + RSpec.describe CNBLifecycle do + let(:app) { AppModel.create(name: 'some-app', space: Space.make) } + let!(:package) { PackageModel.make(type: PackageModel::BITS_TYPE, app: app) } + let(:staging_message) { BuildCreateMessage.new(lifecycle: { data: request_data, type: 'cnb' }) } + let(:request_data) { {} } + + subject(:cnb_lifecycle) { CNBLifecycle.new(package, staging_message) } + + it_behaves_like 'a lifecycle' + + describe '#create_lifecycle_data_model' do + context 'when the user specifies buildpacks' do + let(:request_data) do + { + buildpacks: %w[cool-buildpack rad-buildpack] + } + end + + before do + Buildpack.make(name: 'cool-buildpack') + Buildpack.make(name: 'rad-buildpack') + end + + it 'uses the buildpacks from the user' do + build = BuildModel.make + + expect do + cnb_lifecycle.create_lifecycle_data_model(build) + end.to change(VCAP::CloudController::CNBLifecycleDataModel, :count).by(1) + + data_model = VCAP::CloudController::CNBLifecycleDataModel.last + + expect(data_model.buildpacks).to eq(%w[cool-buildpack rad-buildpack]) + expect(data_model.build).to eq(build) + end + end + + context 'when the user does not specify buildpacks' do + let(:app) { AppModel.make(:buildpack, name: 'some-app', space: Space.make) } + let(:request_data) { {} } + + context 'when the app has buildpacks' do + before do + Buildpack.make(name: 'cool-buildpack') + Buildpack.make(name: 'rad-buildpack') + app.lifecycle_data.update(buildpacks: %w[cool-buildpack rad-buildpack]) + end + + it 'uses the buildpacks on the app' do + build = BuildModel.make + + expect do + cnb_lifecycle.create_lifecycle_data_model(build) + end.to change(VCAP::CloudController::CNBLifecycleDataModel, :count).by(1) + + data_model = VCAP::CloudController::CNBLifecycleDataModel.last + + expect(data_model.buildpacks).to eq(%w[cool-buildpack rad-buildpack]) + expect(data_model.build).to eq(build) + end + end + + context 'when the app does not have buildpacks' do + it 'does not assign any buildpacks' do + build = BuildModel.make + + expect do + cnb_lifecycle.create_lifecycle_data_model(build) + end.to change(VCAP::CloudController::CNBLifecycleDataModel, :count).by(1) + + data_model = VCAP::CloudController::CNBLifecycleDataModel.last + + expect(data_model.buildpacks).to be_empty + expect(data_model.build).to eq(build) + end + end + end + + context 'when the user specifies a stack' do + let(:request_data) do + { stack: 'cool-stack' } + end + + it 'uses that stack' do + data_model = cnb_lifecycle.create_lifecycle_data_model(BuildModel.make) + expect(data_model.stack).to eq('cool-stack') + end + end + + context 'when the user does not specify a stack' do + let(:request_data) { {} } + + context 'when the app has a stack' do + before do + CNBLifecycleDataModel.make(app: app, stack: 'best-stack') + end + + it 'uses the stack from the app' do + data_model = cnb_lifecycle.create_lifecycle_data_model(BuildModel.make) + expect(data_model.stack).to eq('best-stack') + end + end + + context 'when the app does not have a stack' do + it 'uses the default stack' do + data_model = cnb_lifecycle.create_lifecycle_data_model(BuildModel.make) + expect(data_model.stack).to eq(Stack.default.name) + end + end + end + end + + describe '#staging_stack' do + context 'when the user specifies a stack' do + before do + staging_message.buildpack_data.stack = 'cool-stack' + end + + it 'is whatever has been requested in the staging message' do + expect(cnb_lifecycle.staging_stack).to eq('cool-stack') + end + end + + context 'when the user does not specify a stack' do + context 'and the app has a stack' do + before do + CNBLifecycleDataModel.make(app: app, stack: 'cooler-stack') + end + + it 'uses the value set on the app' do + expect(cnb_lifecycle.staging_stack).to eq('cooler-stack') + end + end + + context 'when the app does not have a stack' do + it 'uses the default value for stack' do + expect(cnb_lifecycle.staging_stack).to eq(Stack.default.name) + end + end + end + end + + describe '#buildpack_infos' do + let(:stubbed_data) { { stack: Stack.default.name, buildpack_infos: [instance_double(BuildpackInfo)] } } + let(:request_data) do + { + buildpacks: %w[cool-buildpack rad-buildpack] + } + end + + before do + allow(BuildpackLifecycleFetcher).to receive(:fetch).and_return(stubbed_data) + end + + it 'returns the expected value' do + expect(cnb_lifecycle.buildpack_infos).to eq(stubbed_data[:buildpack_infos]) + + expect(BuildpackLifecycleFetcher).to have_received(:fetch).with(%w[cool-buildpack rad-buildpack], Stack.default.name) + end + end + + describe 'validation' do + let(:validator) { instance_double(BuildpackLifecycleDataValidator) } + let(:stubbed_fetcher_data) { { stack: 'foo', buildpack_infos: 'bar' } } + + before do + allow(validator).to receive(:valid?) + allow(validator).to receive(:errors) + + allow(BuildpackLifecycleFetcher).to receive(:fetch).and_return(stubbed_fetcher_data) + allow(BuildpackLifecycleDataValidator).to receive(:new).and_return(validator) + end + + it 'constructs the validator correctly' do + cnb_lifecycle.valid? + + expect(BuildpackLifecycleDataValidator).to have_received(:new).with(buildpack_infos: 'bar', stack: 'foo') + end + + it 'delegates #valid? to a BuildpackLifecycleDataValidator' do + cnb_lifecycle.valid? + + expect(validator).to have_received(:valid?) + end + + it 'delegates #errors to a BuildpackLifecycleDataValidator' do + cnb_lifecycle.errors + + expect(validator).to have_received(:errors) + end + end + end +end diff --git a/spec/unit/lib/cloud_controller/diego/lifecycles/lifecycle_provider_spec.rb b/spec/unit/lib/cloud_controller/diego/lifecycles/lifecycle_provider_spec.rb index 00657cfe70c..8de0674418d 100644 --- a/spec/unit/lib/cloud_controller/diego/lifecycles/lifecycle_provider_spec.rb +++ b/spec/unit/lib/cloud_controller/diego/lifecycles/lifecycle_provider_spec.rb @@ -23,6 +23,14 @@ module VCAP::CloudController expect(LifecycleProvider.provide(package, message)).to be_a(BuildpackLifecycle) end end + + context 'cnb type' do + let(:type) { 'cnb' } + + it 'returns a BuildpackLifecycle' do + expect(LifecycleProvider.provide(package, message)).to be_a(CNBLifecycle) + end + end end context 'when lifecycle type is not requested on the message' do @@ -44,6 +52,14 @@ module VCAP::CloudController expect(LifecycleProvider.provide(package, message)).to be_a(DockerLifecycle) end end + + context 'when the app defaults to cnb' do + let(:app) { AppModel.make(:cnb) } + + it 'returns a CNBLifecycle' do + expect(LifecycleProvider.provide(package, message)).to be_a(CNBLifecycle) + end + end end end end diff --git a/spec/unit/lib/cloud_controller/diego/stager_spec.rb b/spec/unit/lib/cloud_controller/diego/stager_spec.rb index c242a984238..824b090fc0b 100644 --- a/spec/unit/lib/cloud_controller/diego/stager_spec.rb +++ b/spec/unit/lib/cloud_controller/diego/stager_spec.rb @@ -18,12 +18,15 @@ module Diego let(:buildpack_completion_handler) { instance_double(Diego::Buildpack::StagingCompletionHandler) } let(:docker_completion_handler) { instance_double(Diego::Docker::StagingCompletionHandler) } + let(:cnb_completion_handler) { instance_double(Diego::CNB::StagingCompletionHandler) } before do allow(Diego::Buildpack::StagingCompletionHandler).to receive(:new).with(build).and_return(buildpack_completion_handler) allow(Diego::Docker::StagingCompletionHandler).to receive(:new).with(build).and_return(docker_completion_handler) + allow(Diego::CNB::StagingCompletionHandler).to receive(:new).with(build).and_return(cnb_completion_handler) allow(buildpack_completion_handler).to receive(:staging_complete) allow(docker_completion_handler).to receive(:staging_complete) + allow(cnb_completion_handler).to receive(:staging_complete) allow(Diego::Messenger).to receive(:new).and_return(messenger) end @@ -103,6 +106,18 @@ module Diego expect(docker_completion_handler).to have_received(:staging_complete).with(staging_response, boolean) end end + + context 'cnb' do + let(:build) { BuildModel.make } + let!(:lifecycle_data_model) { CNBLifecycleDataModel.make(build:) } + + it 'delegates to a cnb staging completion handler' do + stager.staging_complete(build, staging_response) + expect(buildpack_completion_handler).not_to have_received(:staging_complete) + expect(docker_completion_handler).not_to have_received(:staging_complete) + expect(cnb_completion_handler).to have_received(:staging_complete).with(staging_response, boolean) + end + end end describe '#stop_stage' do diff --git a/spec/unit/lib/utils/uri_utils_spec.rb b/spec/unit/lib/utils/uri_utils_spec.rb index eda880c9c82..9ad0f59620c 100644 --- a/spec/unit/lib/utils/uri_utils_spec.rb +++ b/spec/unit/lib/utils/uri_utils_spec.rb @@ -1,3 +1,4 @@ +require 'spec_helper' require 'utils/uri_utils' RSpec.describe UriUtils do @@ -86,6 +87,18 @@ expect(UriUtils.is_cnb_buildpack_uri?('docker://nginx:latest')).to be true end + it 'is true if it is a uri with docker scheme with registry, port and tag' do + expect(UriUtils.is_cnb_buildpack_uri?('docker://registry.corp:1111/nginx:latest')).to be true + end + + it 'returns false if it is an invalid https uri' do + expect(UriUtils.is_cnb_buildpack_uri?('https://nginx:latest')).to be false + end + + it 'returns false if it is an invalid docker uri' do + expect(UriUtils.is_cnb_buildpack_uri?('docker://nginx?latest')).to be false + end + it 'is false if it is a uri without any scheme' do expect(UriUtils.is_cnb_buildpack_uri?('nginx')).to be false end