diff --git a/app/controllers/runtime/apps_controller.rb b/app/controllers/runtime/apps_controller.rb index d3c1b5d142a..5f3a514389c 100644 --- a/app/controllers/runtime/apps_controller.rb +++ b/app/controllers/runtime/apps_controller.rb @@ -72,7 +72,7 @@ def read_env(guid) staging_env_json: EnvironmentVariableGroup.staging.environment_json, running_env_json: EnvironmentVariableGroup.running.environment_json, environment_json: process.app.environment_variables, - system_env_json: SystemEnvPresenter.new(process.service_bindings).system_env, + system_env_json: SystemEnvPresenter.new(process).system_env, application_env_json: { 'VCAP_APPLICATION' => vcap_application } }, mode: :compat) ] diff --git a/app/models/runtime/process_model.rb b/app/models/runtime/process_model.rb index a931f493322..9e24c4f1a0f 100644 --- a/app/models/runtime/process_model.rb +++ b/app/models/runtime/process_model.rb @@ -177,6 +177,9 @@ def revisions_enabled? app.revisions_enabled end + delegate :service_binding_k8s_enabled, to: :app + delegate :file_based_vcap_services_enabled, to: :app + def package_hash # this caches latest_package for performance reasons package = latest_package diff --git a/app/presenters/system_environment/system_env_presenter.rb b/app/presenters/system_environment/system_env_presenter.rb index 7fbab00f106..b3f27e9fd66 100644 --- a/app/presenters/system_environment/system_env_presenter.rb +++ b/app/presenters/system_environment/system_env_presenter.rb @@ -2,11 +2,20 @@ require 'presenters/system_environment/service_binding_presenter' class SystemEnvPresenter - def initialize(service_bindings) - @service_bindings = service_bindings + def initialize(app_or_process) + @service_binding_k8s_enabled = app_or_process.service_binding_k8s_enabled + @file_based_vcap_services_enabled = app_or_process.file_based_vcap_services_enabled + @service_bindings = app_or_process.service_bindings end def system_env + return { SERVICE_BINDING_ROOT: '/etc/cf-service-bindings' } if @service_binding_k8s_enabled + return { VCAP_SERVICES_FILE_PATH: '/etc/cf-service-bindings/vcap_services' } if @file_based_vcap_services_enabled + + vcap_services + end + + def vcap_services { VCAP_SERVICES: service_binding_env_variables } end diff --git a/app/presenters/v3/app_env_presenter.rb b/app/presenters/v3/app_env_presenter.rb index aa253842436..80d8a043a09 100644 --- a/app/presenters/v3/app_env_presenter.rb +++ b/app/presenters/v3/app_env_presenter.rb @@ -25,7 +25,7 @@ def to_hash environment_variables: app.environment_variables, staging_env_json: EnvironmentVariableGroup.staging.environment_json, running_env_json: EnvironmentVariableGroup.running.environment_json, - system_env_json: redact_hash(SystemEnvPresenter.new(app.service_bindings).system_env), + system_env_json: redact_hash(SystemEnvPresenter.new(app).system_env), application_env_json: vcap_application } end diff --git a/lib/cloud_controller/backends/staging_environment_builder.rb b/lib/cloud_controller/backends/staging_environment_builder.rb index 775339492e7..9395c5d3c5c 100644 --- a/lib/cloud_controller/backends/staging_environment_builder.rb +++ b/lib/cloud_controller/backends/staging_environment_builder.rb @@ -27,7 +27,7 @@ def build(app, space, lifecycle, memory_limit, staging_disk_in_mb, vars_from_mes 'MEMORY_LIMIT' => "#{memory_limit}m" } ). - merge(SystemEnvPresenter.new(app.service_bindings).system_env.stringify_keys) + merge(SystemEnvPresenter.new(app).system_env.stringify_keys) end end end diff --git a/lib/cloud_controller/diego/app_recipe_builder.rb b/lib/cloud_controller/diego/app_recipe_builder.rb index f206e901349..0b8a3dc7046 100644 --- a/lib/cloud_controller/diego/app_recipe_builder.rb +++ b/lib/cloud_controller/diego/app_recipe_builder.rb @@ -6,6 +6,7 @@ require 'cloud_controller/diego/cnb/desired_lrp_builder' require 'cloud_controller/diego/process_guid' require 'cloud_controller/diego/ssh_key' +require 'cloud_controller/diego/service_binding_files_builder' require 'credhub/config_helpers' require 'models/helpers/health_check_types' require 'cloud_controller/diego/main_lrp_action_builder' @@ -100,7 +101,8 @@ def app_lrp_arguments organizational_unit: ["organization:#{process.organization.guid}", "space:#{process.space.guid}", "app:#{process.app_guid}"] ), image_username: process.desired_droplet.docker_receipt_username, - image_password: process.desired_droplet.docker_receipt_password + image_password: process.desired_droplet.docker_receipt_password, + volume_mounted_files: ServiceBindingFilesBuilder.build(process) }.compact end diff --git a/lib/cloud_controller/diego/environment.rb b/lib/cloud_controller/diego/environment.rb index 0a728918647..d5789782432 100644 --- a/lib/cloud_controller/diego/environment.rb +++ b/lib/cloud_controller/diego/environment.rb @@ -41,7 +41,7 @@ def common_json_and_merge(&blk) @initial_env. merge(process.environment_json || {}). merge(blk.call). - merge(SystemEnvPresenter.new(process.service_bindings).system_env) + merge(SystemEnvPresenter.new(process).system_env) diego_env = diego_env.merge(DATABASE_URL: process.database_uri) if process.database_uri diff --git a/lib/cloud_controller/diego/service_binding_files_builder.rb b/lib/cloud_controller/diego/service_binding_files_builder.rb new file mode 100644 index 00000000000..20ba186f4ef --- /dev/null +++ b/lib/cloud_controller/diego/service_binding_files_builder.rb @@ -0,0 +1,111 @@ +module VCAP::CloudController + module Diego + class ServiceBindingFilesBuilder + class IncompatibleBindings < StandardError; end + + MAX_ALLOWED_BYTESIZE = 1_000_000 + + def self.build(app_or_process) + new(app_or_process).build + end + + def initialize(app_or_process) + @app_or_process = app_or_process + @service_binding_k8s_enabled = app_or_process.service_binding_k8s_enabled + @file_based_vcap_services = app_or_process.file_based_vcap_services_enabled + @service_bindings = app_or_process.service_bindings + end + + def build + if @service_binding_k8s_enabled + build_service_binding_k8s + elsif @file_based_vcap_services + vcap_services = SystemEnvPresenter.new(@app_or_process).vcap_services[:VCAP_SERVICES] + build_vcap_service_file(vcap_services) + end + end + + private + + def build_service_binding_k8s + return nil unless @service_binding_k8s_enabled + + service_binding_files = {} + names = Set.new # to check for duplicate binding names + total_bytesize = 0 # to check the total bytesize + + @service_bindings.select(&:create_succeeded?).each do |service_binding| + sb_hash = ServiceBindingPresenter.new(service_binding, include_instance: true).to_hash + name = sb_hash[:name] + raise IncompatibleBindings.new("Invalid binding name: '#{name}'. Name must match #{binding_naming_convention.inspect}") unless valid_name?(name) + raise IncompatibleBindings.new("Duplicate binding name: #{name}") if names.add?(name).nil? + + # add the credentials first + sb_hash.delete(:credentials)&.each { |k, v| total_bytesize += add_file(service_binding_files, name, k.to_s, v) } + + # add the rest of the hash; already existing credential keys are overwritten + # VCAP_SERVICES attribute names are transformed (e.g. binding_guid -> binding-guid) + sb_hash.each { |k, v| total_bytesize += add_file(service_binding_files, name, transform_vcap_services_attribute(k.to_s), v) } + + # add the type and provider + label = sb_hash[:label] + total_bytesize += add_file(service_binding_files, name, 'type', label) + total_bytesize += add_file(service_binding_files, name, 'provider', label) + end + + raise IncompatibleBindings.new("Bindings exceed the maximum allowed bytesize of #{MAX_ALLOWED_BYTESIZE}: #{total_bytesize}") if total_bytesize > MAX_ALLOWED_BYTESIZE + + service_binding_files.values + end + + def build_vcap_service_file(vcap_services) + path = 'vcap_services' + vcap_services_string = Oj.dump(vcap_services, mode: :compat) + total_bytesize = vcap_services_string.bytesize + path.bytesize + + raise IncompatibleBindings.new("Bindings exceed the maximum allowed bytesize of #{MAX_ALLOWED_BYTESIZE}: #{total_bytesize}") if total_bytesize > MAX_ALLOWED_BYTESIZE + + [::Diego::Bbs::Models::File.new(path: path, content: vcap_services_string)] + end + + def binding_naming_convention + /^[a-z0-9\-.]{1,253}$/ + end + + # - adds a Diego::Bbs::Models::File object to the service_binding_files hash + # - binding name is used as the directory name, key is used as the file name + # - returns the bytesize of the path and content + # - skips (and returns 0) if the value is nil or an empty array or hash + # - serializes the value to JSON if it is a non-string object + def add_file(service_binding_files, name, key, value) + raise IncompatibleBindings.new("Invalid file name: #{key}") unless valid_name?(key) + + path = "#{name}/#{key}" + content = if value.nil? + return 0 + elsif value.is_a?(String) + value + else + return 0 if (value.is_a?(Array) || value.is_a?(Hash)) && value.empty? + + Oj.dump(value, mode: :compat) + end + + service_binding_files[path] = ::Diego::Bbs::Models::File.new(path:, content:) + path.bytesize + content.bytesize + end + + def valid_name?(name) + name.match?(binding_naming_convention) + end + + def transform_vcap_services_attribute(name) + if %w[binding_guid binding_name instance_guid instance_name syslog_drain_url volume_mounts].include?(name) + name.tr('_', '-') + else + name + end + end + end + end +end diff --git a/lib/cloud_controller/diego/task_environment.rb b/lib/cloud_controller/diego/task_environment.rb index df7b9f1c3c4..dad26816613 100644 --- a/lib/cloud_controller/diego/task_environment.rb +++ b/lib/cloud_controller/diego/task_environment.rb @@ -19,7 +19,7 @@ def build initial_envs. merge(app_env). merge('VCAP_APPLICATION' => vcap_application, 'MEMORY_LIMIT' => "#{task.memory_in_mb}m"). - merge(SystemEnvPresenter.new(app.service_bindings).system_env.stringify_keys) + merge(SystemEnvPresenter.new(app).system_env.stringify_keys) task_env = task_env.merge('VCAP_PLATFORM_OPTIONS' => credhub_url) if credhub_url.present? && cred_interpolation_enabled? diff --git a/lib/cloud_controller/diego/task_recipe_builder.rb b/lib/cloud_controller/diego/task_recipe_builder.rb index 3b6a5527430..4395d6d0b96 100644 --- a/lib/cloud_controller/diego/task_recipe_builder.rb +++ b/lib/cloud_controller/diego/task_recipe_builder.rb @@ -5,6 +5,7 @@ require 'cloud_controller/diego/bbs_environment_builder' require 'cloud_controller/diego/task_completion_callback_generator' require 'cloud_controller/diego/task_cpu_weight_calculator' +require 'cloud_controller/diego/service_binding_files_builder' module VCAP::CloudController module Diego @@ -52,7 +53,8 @@ def build_app_task(config, task) ] ), image_username: task.droplet.docker_receipt_username, - image_password: task.droplet.docker_receipt_password + image_password: task.droplet.docker_receipt_password, + volume_mounted_files: ServiceBindingFilesBuilder.build(task.app) }.compact) end @@ -90,7 +92,8 @@ def build_staging_task(config, staging_details) ] ), image_username: staging_details.package.docker_username, - image_password: staging_details.package.docker_password + image_password: staging_details.package.docker_password, + volume_mounted_files: ServiceBindingFilesBuilder.build(staging_details.package.app) }.compact) end diff --git a/spec/request/apps_spec.rb b/spec/request/apps_spec.rb index 2e8f95dbbf6..8afc4591ff0 100644 --- a/spec/request/apps_spec.rb +++ b/spec/request/apps_spec.rb @@ -1601,6 +1601,34 @@ end it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS + + context 'when k8s service bindings are enabled' do + let(:app_model_response_object) do + r = super() + r[:system_env_json] = { SERVICE_BINDING_ROOT: '/etc/cf-service-bindings' } + r + end + + before do + app_model.update(service_binding_k8s_enabled: true) + end + + it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS + end + + context 'when file-based VCAP service bindings are enabled' do + let(:app_model_response_object) do + r = super() + r[:system_env_json] = { VCAP_SERVICES_FILE_PATH: '/etc/cf-service-bindings/vcap_services' } + r + end + + before do + app_model.update(file_based_vcap_services_enabled: true) + end + + it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS + end end context 'when VCAP_SERVICES contains potentially sensitive information' do diff --git a/spec/unit/lib/cloud_controller/diego/app_recipe_builder_spec.rb b/spec/unit/lib/cloud_controller/diego/app_recipe_builder_spec.rb index bb7366609f8..734df1aacb1 100644 --- a/spec/unit/lib/cloud_controller/diego/app_recipe_builder_spec.rb +++ b/spec/unit/lib/cloud_controller/diego/app_recipe_builder_spec.rb @@ -52,6 +52,39 @@ module Diego expect(lrp.trusted_system_certificates_path).to eq(RUNNING_TRUSTED_SYSTEM_CERT_PATH) expect(lrp.PlacementTags).to eq(['placement-tag']) expect(lrp.certificate_properties).to eq(expected_certificate_properties) + + expect(lrp.volume_mounted_files).to be_empty + end + end + + shared_examples 'k8s service bindings' do + context 'when k8s service bindings are enabled' do + before do + app = process.app + app.update(service_binding_k8s_enabled: true) + VCAP::CloudController::ServiceBinding.make(service_instance: ManagedServiceInstance.make(space: app.space), app: app) + end + + it 'includes volume mounted files' do + lrp = builder.build_app_lrp + expect(lrp.volume_mounted_files.size).to be > 1 + end + end + end + + shared_examples 'file-based VCAP service bindings' do + context 'when file-based VCAP service bindings are enabled' do + before do + app = process.app + app.update(file_based_vcap_services_enabled: true) + VCAP::CloudController::ServiceBinding.make(service_instance: ManagedServiceInstance.make(space: app.space), app: app) + end + + it 'includes the vcap_services file' do + lrp = builder.build_app_lrp + expect(lrp.volume_mounted_files.size).to eq(1) + expect(lrp.volume_mounted_files[0].path).to eq('vcap_services') + end end end @@ -914,6 +947,9 @@ module Diego expect(lrp2.action).to eq(expected_action) end end + + include_examples 'k8s service bindings' + include_examples 'file-based VCAP service bindings' end context 'when the lifecycle_type is "cnb"' do @@ -1005,6 +1041,9 @@ module Diego })) end end + + include_examples 'k8s service bindings' + include_examples 'file-based VCAP service bindings' end context 'when the lifecycle_type is "docker"' do @@ -1348,6 +1387,9 @@ module Diego })) end end + + include_examples 'k8s service bindings' + include_examples 'file-based VCAP service bindings' end end diff --git a/spec/unit/lib/cloud_controller/diego/environment_spec.rb b/spec/unit/lib/cloud_controller/diego/environment_spec.rb index 25ccee2d9ca..28942bd544a 100644 --- a/spec/unit/lib/cloud_controller/diego/environment_spec.rb +++ b/spec/unit/lib/cloud_controller/diego/environment_spec.rb @@ -23,7 +23,7 @@ module VCAP::CloudController::Diego encoded_vcap_application_json = vcap_app.to_json vcap_services_key = :VCAP_SERVICES - system_env = SystemEnvPresenter.new(process.service_bindings).system_env + system_env = SystemEnvPresenter.new(process).system_env expect(system_env).to have_key(vcap_services_key) encoded_vcap_services_json = system_env[vcap_services_key].to_json diff --git a/spec/unit/lib/cloud_controller/diego/service_binding_files_builder_spec.rb b/spec/unit/lib/cloud_controller/diego/service_binding_files_builder_spec.rb new file mode 100644 index 00000000000..fe716bd526f --- /dev/null +++ b/spec/unit/lib/cloud_controller/diego/service_binding_files_builder_spec.rb @@ -0,0 +1,315 @@ +require 'spec_helper' +require 'cloud_controller/diego/service_binding_files_builder' + +module VCAP::CloudController::Diego + RSpec.shared_examples 'mapping of type and provider' do |label| + it 'sets type and provider to the service label' do + expect(service_binding_files.find { |f| f.path == "#{directory}/type" }).to have_attributes(content: label || 'service-name') + expect(service_binding_files.find { |f| f.path == "#{directory}/provider" }).to have_attributes(content: label || 'service-name') + expect(service_binding_files.find { |f| f.path == "#{directory}/label" }).to have_attributes(content: label || 'service-name') + end + end + + RSpec.shared_examples 'mapping of binding metadata' do |name| + it 'maps service binding metadata attributes to files' do + expect(service_binding_files.find { |f| f.path == "#{directory}/binding-guid" }).to have_attributes(content: binding.guid) + expect(service_binding_files.find { |f| f.path == "#{directory}/name" }).to have_attributes(content: name || 'binding-name') + expect(service_binding_files.find { |f| f.path == "#{directory}/binding-name" }).to have_attributes(content: 'binding-name') if name.nil? + end + end + + RSpec.shared_examples 'mapping of instance metadata' do |instance_name| + it 'maps service instance metadata attributes to files' do + expect(service_binding_files.find { |f| f.path == "#{directory}/instance-guid" }).to have_attributes(content: instance.guid) + expect(service_binding_files.find { |f| f.path == "#{directory}/instance-name" }).to have_attributes(content: instance_name || 'instance-name') + end + end + + RSpec.shared_examples 'mapping of plan metadata' do + it 'maps service plan metadata attributes to files' do + expect(service_binding_files.find { |f| f.path == "#{directory}/plan" }).to have_attributes(content: 'plan-name') + end + end + + RSpec.shared_examples 'mapping of tags' do |tags| + it 'maps (service tags merged with) instance tags to a file' do + expect(service_binding_files.find do |f| + f.path == "#{directory}/tags" + end).to have_attributes(content: tags || '["a-service-tag","another-service-tag","an-instance-tag","another-instance-tag"]') + end + end + + RSpec.shared_examples 'mapping of credentials' do |credential_files| + it 'maps service binding credentials to individual files' do + expected_credential_files = credential_files || { + string: 'a string', + number: '42', + boolean: 'true', + array: '["one","two","three"]', + hash: '{"key":"value"}' + } + expected_credential_files.each do |name, content| + expect(service_binding_files.find { |f| f.path == "#{directory}/#{name}" }).to have_attributes(content:) + end + end + end + + RSpec.shared_examples 'expected files' do |files| + it 'does not include other files' do + other_files = service_binding_files.reject do |file| + match = file.path.match(%r{^#{directory}/(.+)$}) + !match.nil? && !files.delete(match[1]).nil? + end + + expect(files).to be_empty + expect(other_files).to be_empty + end + end + + RSpec.describe ServiceBindingFilesBuilder do + let(:service) { VCAP::CloudController::Service.make(label: 'service-name', tags: %w[a-service-tag another-service-tag]) } + let(:plan) { VCAP::CloudController::ServicePlan.make(name: 'plan-name', service: service) } + let(:instance) { VCAP::CloudController::ManagedServiceInstance.make(name: 'instance-name', tags: %w[an-instance-tag another-instance-tag], service_plan: plan) } + let(:binding_name) { 'binding-name' } + let(:credentials) do + { + string: 'a string', + number: 42, + boolean: true, + array: %w[one two three], + hash: { + key: 'value' + } + } + end + let(:syslog_drain_url) { nil } + let(:volume_mounts) { nil } + let(:binding) do + VCAP::CloudController::ServiceBinding.make( + name: binding_name, + credentials: credentials, + service_instance: instance, + syslog_drain_url: syslog_drain_url, + volume_mounts: volume_mounts + ) + end + let(:app) { binding.app } + let(:directory) { 'binding-name' } + + describe '#build' do + subject(:build) { ServiceBindingFilesBuilder.build(app) } + + context 'when service-binding-k8s feature is enabled' do + before do + app.update(service_binding_k8s_enabled: true) + end + + it 'returns an array of Diego::Bbs::Models::File objects' do + expect(build).to be_an(Array) + expect(build).not_to be_empty + expect(build).to all(be_a(Diego::Bbs::Models::File)) + end + + describe 'mapping rules for service binding files' do + subject(:service_binding_files) { build } + + it 'puts all files into a directory named after the service binding' do + expect(service_binding_files).not_to be_empty + expect(service_binding_files).to all(have_attributes(path: match(%r{^binding-name/.+$}))) + end + + include_examples 'mapping of type and provider' + include_examples 'mapping of binding metadata' + include_examples 'mapping of instance metadata' + include_examples 'mapping of plan metadata' + include_examples 'mapping of tags' + include_examples 'mapping of credentials' + + it 'omits null or empty array attributes' do + expect(service_binding_files).not_to include(have_attributes(path: 'binding-name/syslog_drain_url')) + expect(service_binding_files).not_to include(have_attributes(path: 'binding-name/volume_mounts')) + end + + include_examples 'expected files', %w[type provider label binding-guid name binding-name instance-guid instance-name plan tags string number boolean array hash] + + context 'when binding_name is nil' do + let(:binding_name) { nil } + let(:directory) { 'instance-name' } + + include_examples 'mapping of type and provider' + include_examples 'mapping of binding metadata', 'instance-name' + include_examples 'mapping of instance metadata' + include_examples 'mapping of plan metadata' + include_examples 'mapping of tags' + include_examples 'mapping of credentials' + + include_examples 'expected files', %w[type provider label binding-guid name instance-guid instance-name plan tags string number boolean array hash] + end + + context 'when syslog_drain_url is set' do + let(:syslog_drain_url) { 'https://syslog.drain' } + + it 'maps the attribute to a file' do + expect(service_binding_files.find { |f| f.path == 'binding-name/syslog-drain-url' }).to have_attributes(content: 'https://syslog.drain') + end + + include_examples 'mapping of type and provider' + include_examples 'mapping of binding metadata' + include_examples 'mapping of instance metadata' + include_examples 'mapping of plan metadata' + include_examples 'mapping of tags' + include_examples 'mapping of credentials' + + include_examples 'expected files', + %w[type provider label binding-guid name binding-name instance-guid instance-name plan tags string number boolean array hash syslog-drain-url] + end + + context 'when volume_mounts is set' do + let(:volume_mounts) do + [{ + container_dir: 'dir1', + device_type: 'type1', + mode: 'mode1', + foo: 'bar' + }, { + container_dir: 'dir2', + device_type: 'type2', + mode: 'mode2', + foo: 'baz' + }] + end + + it 'maps the attribute to a file' do + expect(service_binding_files.find do |f| + f.path == 'binding-name/volume-mounts' + end).to have_attributes(content: '[{"container_dir":"dir1","device_type":"type1","mode":"mode1"},{"container_dir":"dir2","device_type":"type2","mode":"mode2"}]') + end + + include_examples 'mapping of type and provider' + include_examples 'mapping of binding metadata' + include_examples 'mapping of instance metadata' + include_examples 'mapping of plan metadata' + include_examples 'mapping of tags' + include_examples 'mapping of credentials' + + include_examples 'expected files', + %w[type provider label binding-guid name binding-name instance-guid instance-name plan tags string number boolean array hash volume-mounts] + end + + context 'when the instance is user-provided' do + let(:instance) { VCAP::CloudController::UserProvidedServiceInstance.make(name: 'upsi', tags: %w[an-upsi-tag another-upsi-tag]) } + + include_examples 'mapping of type and provider', 'user-provided' + include_examples 'mapping of binding metadata' + include_examples 'mapping of instance metadata', 'upsi' + include_examples 'mapping of tags', '["an-upsi-tag","another-upsi-tag"]' + include_examples 'mapping of credentials' + + include_examples 'expected files', %w[type provider label binding-guid name binding-name instance-guid instance-name tags string number boolean array hash] + end + + context 'when there are duplicate keys at different levels' do + let(:credentials) { { type: 'duplicate-type', name: 'duplicate-name', credentials: { password: 'secret' } } } + + include_examples 'mapping of type and provider' # no 'duplicate-type' + include_examples 'mapping of binding metadata' # no 'duplicate-name' + include_examples 'mapping of instance metadata' + include_examples 'mapping of plan metadata' + include_examples 'mapping of tags' + include_examples 'mapping of credentials', { credentials: '{"password":"secret"}' } + + include_examples 'expected files', %w[type provider label binding-guid name binding-name instance-guid instance-name plan tags credentials] + end + + context 'when there are duplicate binding names' do + let(:binding_name) { 'duplicate-name' } + + before do + VCAP::CloudController::ServiceBinding.make(app: app, + service_instance: VCAP::CloudController::UserProvidedServiceInstance.make( + space: app.space, name: 'duplicate-name' + )) + end + + it 'raises an exception' do + expect { service_binding_files }.to raise_error(ServiceBindingFilesBuilder::IncompatibleBindings, 'Duplicate binding name: duplicate-name') + end + end + + context 'when binding names violate the Service Binding Specification for Kubernetes' do + let(:binding_name) { 'binding_name' } + + it 'raises an exception' do + expect do + service_binding_files + end.to raise_error(ServiceBindingFilesBuilder::IncompatibleBindings, "Invalid binding name: 'binding_name'. Name must match /^[a-z0-9\\-.]{1,253}$/") + end + end + + context 'when the bindings exceed the maximum allowed bytesize' do + let(:xxl_credentials) do + c = {} + value = 'v' * 1000 + 1000.times do |i| + c["key#{i}"] = value + end + c + end + + before do + allow_any_instance_of(ServiceBindingPresenter).to receive(:to_hash).and_wrap_original do |original| + original.call.merge(credentials: xxl_credentials) + end + end + + it 'raises an exception' do + expect { service_binding_files }.to raise_error(ServiceBindingFilesBuilder::IncompatibleBindings, /^Bindings exceed the maximum allowed bytesize of 1000000: \d+/) + end + end + + context 'when credential keys violate the Service Binding Specification for Kubernetes for binding entry file names' do + let(:credentials) { { '../secret': 'hidden' } } + + it 'raises an exception' do + expect { service_binding_files }.to raise_error(ServiceBindingFilesBuilder::IncompatibleBindings, 'Invalid file name: ../secret') + end + end + end + end + + context 'when file-based-vcap-services feature is enabled' do + before do + app.update(file_based_vcap_services_enabled: true) + end + + it 'returns an array containing vcap_services file as Diego::Bbs::Models::File object' do + expect(build).to be_an(Array) + expect(build.size).to eq(1) + expect(build).to all(be_a(Diego::Bbs::Models::File)) + expect(build[0].path).to eq('vcap_services') + end + + context 'when the bindings exceed the maximum allowed bytesize' do + let(:xxl_credentials) do + c = {} + value = 'v' * 1000 + 1000.times do |i| + c["key#{i}"] = value + end + c + end + + before do + allow_any_instance_of(ServiceBindingPresenter).to receive(:to_hash).and_wrap_original do |original| + original.call.merge(credentials: xxl_credentials) + end + end + + it 'raises an exception' do + expect { build }.to raise_error(ServiceBindingFilesBuilder::IncompatibleBindings, /^Bindings exceed the maximum allowed bytesize of 1000000: \d+/) + end + end + end + end + end +end diff --git a/spec/unit/lib/cloud_controller/diego/task_recipe_builder_spec.rb b/spec/unit/lib/cloud_controller/diego/task_recipe_builder_spec.rb index b547a76cad0..521aba3a07a 100644 --- a/spec/unit/lib/cloud_controller/diego/task_recipe_builder_spec.rb +++ b/spec/unit/lib/cloud_controller/diego/task_recipe_builder_spec.rb @@ -177,6 +177,8 @@ module Diego expect(result.placement_tags).to eq(['potato-segment']) expect(result.max_pids).to eq(100) expect(result.certificate_properties).to eq(certificate_properties) + + expect(result.volume_mounted_files).to be_empty end it 'gives the task a TrustedSystemCertificatesPath' do @@ -198,6 +200,33 @@ module Diego expect(result.placement_tags).to eq([]) end end + + context 'when k8s service bindings are enabled' do + before do + app = staging_details.package.app + app.update(service_binding_k8s_enabled: true) + VCAP::CloudController::ServiceBinding.make(service_instance: ManagedServiceInstance.make(space: app.space), app: app) + end + + it 'includes volume mounted files' do + result = task_recipe_builder.build_staging_task(config, staging_details) + expect(result.volume_mounted_files.size).to be > 1 + end + end + + context 'when file-based VCAP service bindings are enabled' do + before do + app = staging_details.package.app + app.update(file_based_vcap_services_enabled: true) + VCAP::CloudController::ServiceBinding.make(service_instance: ManagedServiceInstance.make(space: app.space), app: app) + end + + it 'includes volume mounted files' do + result = task_recipe_builder.build_staging_task(config, staging_details) + expect(result.volume_mounted_files.size).to eq(1) + expect(result.volume_mounted_files[0].path).to eq('vcap_services') + end + end end context 'with a docker backend' do @@ -505,6 +534,8 @@ module Diego expect(result.metric_tags['organization_name'].static).to eq('MyOrg') expect(result.metric_tags['space_name'].static).to eq('MySpace') expect(result.metric_tags['app_name'].static).to eq('MyApp') + + expect(result.volume_mounted_files).to be_empty end context 'when a volume mount is provided' do @@ -583,6 +614,33 @@ module Diego expect(result.placement_tags).to eq([]) end end + + context 'when k8s service bindings are enabled' do + before do + app = task.app + app.update(service_binding_k8s_enabled: true) + VCAP::CloudController::ServiceBinding.make(service_instance: ManagedServiceInstance.make(space: app.space), app: app) + end + + it 'includes volume mounted files' do + result = task_recipe_builder.build_app_task(config, task) + expect(result.volume_mounted_files.size).to be > 1 + end + end + + context 'when file-based VCAP service bindings are enabled' do + before do + app = task.app + app.update(file_based_vcap_services_enabled: true) + VCAP::CloudController::ServiceBinding.make(service_instance: ManagedServiceInstance.make(space: app.space), app: app) + end + + it 'includes volume mounted files' do + result = task_recipe_builder.build_app_task(config, task) + expect(result.volume_mounted_files.size).to eq(1) + expect(result.volume_mounted_files[0].path).to eq('vcap_services') + end + end end context 'with a docker backend' do @@ -658,6 +716,8 @@ module Diego expect(result.image_username).to eq('dockerusername') expect(result.image_password).to eq('dockerpassword') + + expect(result.volume_mounted_files).to be_empty end context 'when a volume mount is provided' do @@ -736,6 +796,33 @@ module Diego expect(result.placement_tags).to eq([]) end end + + context 'when k8s service bindings are enabled' do + before do + app = task.app + app.update(service_binding_k8s_enabled: true) + VCAP::CloudController::ServiceBinding.make(service_instance: ManagedServiceInstance.make(space: app.space), app: app) + end + + it 'includes volume mounted files' do + result = task_recipe_builder.build_app_task(config, task) + expect(result.volume_mounted_files.size).to be > 1 + end + end + + context 'when file-based VCAP service bindings are enabled' do + before do + app = task.app + app.update(file_based_vcap_services_enabled: true) + VCAP::CloudController::ServiceBinding.make(service_instance: ManagedServiceInstance.make(space: app.space), app: app) + end + + it 'includes volume mounted files' do + result = task_recipe_builder.build_app_task(config, task) + expect(result.volume_mounted_files.size).to eq(1) + expect(result.volume_mounted_files[0].path).to eq('vcap_services') + end + end end end end diff --git a/spec/unit/presenters/runtime_environment/system_env_presenter_spec.rb b/spec/unit/presenters/runtime_environment/system_env_presenter_spec.rb deleted file mode 100644 index 188281ca4fd..00000000000 --- a/spec/unit/presenters/runtime_environment/system_env_presenter_spec.rb +++ /dev/null @@ -1,104 +0,0 @@ -require 'spec_helper' - -module VCAP::CloudController - RSpec.describe SystemEnvPresenter do - subject(:system_env_presenter) { SystemEnvPresenter.new(app.service_bindings) } - - describe '#system_env' do - context 'when there are no services' do - let(:app) { AppModel.make(environment_variables: { 'jesse' => 'awesome' }) } - - it 'contains an empty vcap_services' do - expect(system_env_presenter.system_env[:VCAP_SERVICES]).to eq({}) - end - end - - context 'when there are services' do - let(:space) { Space.make } - let(:app) { AppModel.make(environment_variables: { 'jesse' => 'awesome' }, space: space) } - let(:service) { Service.make(label: 'elephantsql-n/a') } - let(:service_alt) { Service.make(label: 'giraffesql-n/a') } - let(:service_plan) { ServicePlan.make(service:) } - let(:service_plan_alt) { ServicePlan.make(service: service_alt) } - let(:service_instance) { ManagedServiceInstance.make(space: space, service_plan: service_plan, name: 'elephantsql-vip-uat', tags: ['excellent']) } - let(:service_instance_same_label) { ManagedServiceInstance.make(space: space, service_plan: service_plan, name: 'elephantsql-2') } - let(:service_instance_diff_label) { ManagedServiceInstance.make(space: space, service_plan: service_plan_alt, name: 'giraffesql-vip-uat') } - let!(:service_binding) { ServiceBinding.make(app: app, service_instance: service_instance, syslog_drain_url: 'logs.go-here.com') } - - it 'contains a populated vcap_services' do - expect(system_env_presenter.system_env[:VCAP_SERVICES]).not_to eq({}) - expect(system_env_presenter.system_env[:VCAP_SERVICES]).to have_key(service.label.to_sym) - expect(system_env_presenter.system_env[:VCAP_SERVICES][service.label.to_sym]).to have(1).services - end - - it 'includes service binding and instance information' do - expect(system_env_presenter.system_env[:VCAP_SERVICES][service.label.to_sym]).to have(1).items - binding = system_env_presenter.system_env[:VCAP_SERVICES][service.label.to_sym].first.to_hash - - expect(binding[:credentials]).to eq(service_binding.credentials) - expect(binding[:name]).to eq('elephantsql-vip-uat') - end - - describe 'volume mounts' do - context 'when the service binding has volume mounts' do - let!(:service_binding) do - ServiceBinding.make( - app: app, - service_instance: service_instance, - syslog_drain_url: 'logs.go-here.com', - volume_mounts: [{ - container_dir: '/data/images', - mode: 'r', - device_type: 'shared', - device: { - driver: 'cephfs', - volume_id: 'abc', - mount_config: { - key: 'value' - } - } - }] - ) - end - - it 'includes only the public volume information' do - expect(system_env_presenter.system_env[:VCAP_SERVICES][service.label.to_sym][0].to_hash[:volume_mounts]).to eq([{ 'container_dir' => '/data/images', - 'mode' => 'r', - 'device_type' => 'shared' }]) - end - end - - context 'when the service binding has no volume mounts' do - it 'is an empty array' do - expect(system_env_presenter.system_env[:VCAP_SERVICES][service.label.to_sym][0].to_hash[:volume_mounts]).to eq([]) - end - end - end - - context 'when the service is user-provided' do - let(:service_instance) { UserProvidedServiceInstance.make(space: space, name: 'elephantsql-vip-uat') } - - it 'includes service binding and instance information' do - expect(system_env_presenter.system_env[:VCAP_SERVICES][:'user-provided']).to have(1).items - binding = system_env_presenter.system_env[:VCAP_SERVICES][:'user-provided'].first.to_hash - expect(binding[:credentials]).to eq(service_binding.credentials) - expect(binding[:name]).to eq('elephantsql-vip-uat') - end - end - - describe 'grouping' do - before do - ServiceBinding.make(app: app, service_instance: service_instance_same_label) - ServiceBinding.make(app: app, service_instance: service_instance_diff_label) - end - - it 'groups services by label' do - expect(system_env_presenter.system_env[:VCAP_SERVICES]).to have(2).groups - expect(system_env_presenter.system_env[:VCAP_SERVICES][service.label.to_sym]).to have(2).services - expect(system_env_presenter.system_env[:VCAP_SERVICES][service_alt.label.to_sym]).to have(1).service - end - end - end - end - end -end diff --git a/spec/unit/presenters/system_environment/system_env_presenter_spec.rb b/spec/unit/presenters/system_environment/system_env_presenter_spec.rb index 87efc9c2bc2..7b734293a8e 100644 --- a/spec/unit/presenters/system_environment/system_env_presenter_spec.rb +++ b/spec/unit/presenters/system_environment/system_env_presenter_spec.rb @@ -2,7 +2,39 @@ module VCAP::CloudController RSpec.describe SystemEnvPresenter do - subject(:system_env_presenter) { SystemEnvPresenter.new(app.service_bindings) } + subject(:system_env_presenter) { SystemEnvPresenter.new(app) } + + shared_examples 'k8s service bindings' do + context 'when k8s service bindings are enabled' do + before do + app.update(service_binding_k8s_enabled: true) + end + + it 'does not contain vcap_services' do + expect(system_env_presenter.system_env).not_to have_key(:VCAP_SERVICES) + end + + it 'contains service_binding_root' do + expect(system_env_presenter.system_env[:SERVICE_BINDING_ROOT]).to eq('/etc/cf-service-bindings') + end + end + end + + shared_examples 'file-based VCAP service bindings' do + context 'when file-based VCAP service bindings are enabled' do + before do + app.update(file_based_vcap_services_enabled: true) + end + + it 'does not contain vcap_services' do + expect(system_env_presenter.system_env).not_to have_key(:VCAP_SERVICES) + end + + it 'contains service_binding_root' do + expect(system_env_presenter.system_env[:VCAP_SERVICES_FILE_PATH]).to eq('/etc/cf-service-bindings/vcap_services') + end + end + end describe '#system_env' do context 'when there are no services' do @@ -11,6 +43,9 @@ module VCAP::CloudController it 'contains an empty vcap_services' do expect(system_env_presenter.system_env[:VCAP_SERVICES]).to eq({}) end + + include_examples 'k8s service bindings' + include_examples 'file-based VCAP service bindings' end context 'when there are services' do @@ -154,6 +189,9 @@ module VCAP::CloudController expect(system_env_presenter.system_env[:VCAP_SERVICES][service_alt.label.to_sym]).to have(1).service end end + + include_examples 'k8s service bindings' + include_examples 'file-based VCAP service bindings' end end end