From 000999b53dc8ba925b2b4711b3eafc13fc001ee0 Mon Sep 17 00:00:00 2001 From: Pavel Busko Date: Wed, 19 Jun 2024 16:45:00 +0200 Subject: [PATCH] Enable using protected CNBs Co-authored-by: Johannes Dillmann Co-authored-by: Ralf Pannemans Co-authored-by: Nicolas Bender --- app/messages/app_manifest_message.rb | 4 +- .../buildpack_lifecycle_data_message.rb | 13 ++- .../runtime/cnb_lifecycle_data_model.rb | 16 ++- ...619161000_add_cnb_lifecycle_credentials.rb | 17 +++ .../diego/buildpack/lifecycle_data.rb | 43 +++++++ .../diego/buildpack/lifecycle_protocol.rb | 6 +- .../diego/cnb/lifecycle_data.rb | 46 ++++++++ .../diego/cnb/lifecycle_protocol.rb | 9 +- .../diego/cnb/staging_action_builder.rb | 4 +- lib/cloud_controller/diego/lifecycle_data.rb | 41 ------- .../diego/lifecycle_protocol.rb | 3 +- .../diego/lifecycles/app_cnb_lifecycle.rb | 11 ++ .../diego/lifecycles/cnb_lifecycle.rb | 11 +- .../controllers/v3/apps_controller_spec.rb | 26 +++++ .../diego/cnb/lifecycle_data_spec.rb | 110 ++++++++++++++++++ .../diego/cnb/lifecycle_protocol_spec.rb | 12 +- .../diego/cnb/staging_action_builder_spec.rb | 46 +++++++- .../diego/lifecycle_data_spec.rb | 2 +- .../diego/lifecycles/cnb_lifecycle_spec.rb | 108 ++++++++--------- .../errands/rotate_database_key_spec.rb | 1 + .../messages/app_manifest_message_spec.rb | 15 +++ 21 files changed, 422 insertions(+), 122 deletions(-) create mode 100644 db/migrations/20240619161000_add_cnb_lifecycle_credentials.rb create mode 100644 lib/cloud_controller/diego/buildpack/lifecycle_data.rb create mode 100644 lib/cloud_controller/diego/cnb/lifecycle_data.rb delete mode 100644 lib/cloud_controller/diego/lifecycle_data.rb create mode 100644 spec/unit/lib/cloud_controller/diego/cnb/lifecycle_data_spec.rb diff --git a/app/messages/app_manifest_message.rb b/app/messages/app_manifest_message.rb index edecd756160..4e51b70ff43 100644 --- a/app/messages/app_manifest_message.rb +++ b/app/messages/app_manifest_message.rb @@ -42,6 +42,7 @@ class AppManifestMessage < BaseMessage sidecars stack timeout + cnb_credentials ] HEALTH_CHECK_TYPE_MAPPING = { HealthCheckTypes::NONE => HealthCheckTypes::PROCESS }.freeze @@ -296,7 +297,8 @@ def cnb_lifecycle_data type: Lifecycles::CNB, data: { buildpacks: requested_buildpacks, - stack: @stack + stack: @stack, + credentials: @cnb_credentials }.compact } end diff --git a/app/messages/buildpack_lifecycle_data_message.rb b/app/messages/buildpack_lifecycle_data_message.rb index b2b3cff20fd..66bf5ed234a 100644 --- a/app/messages/buildpack_lifecycle_data_message.rb +++ b/app/messages/buildpack_lifecycle_data_message.rb @@ -1,6 +1,6 @@ module VCAP::CloudController class BuildpackLifecycleDataMessage < BaseMessage - register_allowed_keys %i[buildpacks stack] + register_allowed_keys %i[buildpacks stack credentials] validates_with NoAdditionalKeysValidator @@ -13,7 +13,12 @@ class BuildpackLifecycleDataMessage < BaseMessage array: true, allow_nil: true + validates :credentials, + hash: true, + allow_nil: true + validate :buildpacks_content + validate :credentials_content def buildpacks_content return unless buildpacks.is_a?(Array) @@ -31,5 +36,11 @@ def buildpacks_content errors.add(:buildpacks, 'can only contain strings') if non_string errors.add(:buildpacks, 'entries must be between 1 and 4096 characters') if length_error end + + def credentials_content + return unless credentials.is_a?(Hash) + + errors.add(:credentials, 'credentials value must be a hash') if credentials.any? { |_, v| !v.is_a?(Hash) } + end end end diff --git a/app/models/runtime/cnb_lifecycle_data_model.rb b/app/models/runtime/cnb_lifecycle_data_model.rb index f7da81d2a6f..5befd051bd5 100644 --- a/app/models/runtime/cnb_lifecycle_data_model.rb +++ b/app/models/runtime/cnb_lifecycle_data_model.rb @@ -1,8 +1,9 @@ require 'cloud_controller/diego/lifecycles/lifecycles' - +require 'presenters/helpers/censorship' module VCAP::CloudController class CNBLifecycleDataModel < Sequel::Model(:cnb_lifecycle_data) LIFECYCLE_TYPE = Lifecycles::CNB + set_field_as_encrypted :registry_credentials_json, salt: :credentials_salt, column: :encrypted_registry_credentials_json many_to_one :droplet, class: '::VCAP::CloudController::DropletModel', @@ -72,7 +73,8 @@ def attributes_from_buildpack(buildpack_name) def to_hash { buildpacks: buildpacks.map { |buildpack| CloudController::UrlSecretObfuscator.obfuscate(buildpack) }, - stack: stack + stack: stack, + credentials: credentials && Presenters::Censorship::REDACTED_CREDENTIAL } end @@ -81,5 +83,15 @@ def validate errors.add(:lifecycle_data, 'Must be associated with an app OR a build+droplet, but not both') end + + def credentials + return unless registry_credentials_json + + Oj.load(registry_credentials_json) + end + + def credentials=(creds) + self.registry_credentials_json = Oj.dump(creds) + end end end diff --git a/db/migrations/20240619161000_add_cnb_lifecycle_credentials.rb b/db/migrations/20240619161000_add_cnb_lifecycle_credentials.rb new file mode 100644 index 00000000000..256d47b2bc6 --- /dev/null +++ b/db/migrations/20240619161000_add_cnb_lifecycle_credentials.rb @@ -0,0 +1,17 @@ +Sequel.migration do + up do + # rubocop:disable Migration/IncludeStringSize + add_column :cnb_lifecycle_data, :encrypted_registry_credentials_json, String, if_not_exists: true + # rubocop:enable Migration/IncludeStringSize + add_column :cnb_lifecycle_data, :credentials_salt, String, size: 255, if_not_exists: true + add_column :cnb_lifecycle_data, :encryption_key_label, String, size: 255, if_not_exists: true + add_column :cnb_lifecycle_data, :encryption_iterations, Integer, default: 2048, null: false, if_not_exists: true + end + + down do + drop_column :cnb_lifecycle_data, :encrypted_registry_credentials_json, if_exists: true + drop_column :cnb_lifecycle_data, :credentials_salt, if_exists: true + drop_column :cnb_lifecycle_data, :encryption_key_label, if_exists: true + drop_column :cnb_lifecycle_data, :encryption_iterations, if_exists: true + end +end diff --git a/lib/cloud_controller/diego/buildpack/lifecycle_data.rb b/lib/cloud_controller/diego/buildpack/lifecycle_data.rb new file mode 100644 index 00000000000..1ae2c1e67d5 --- /dev/null +++ b/lib/cloud_controller/diego/buildpack/lifecycle_data.rb @@ -0,0 +1,43 @@ +module VCAP::CloudController + module Diego + module Buildpack + class LifecycleData + attr_accessor :app_bits_download_uri, :build_artifacts_cache_download_uri, :build_artifacts_cache_upload_uri, + :buildpacks, :app_bits_checksum, :droplet_upload_uri, :stack, :buildpack_cache_checksum + + def message + message = { + app_bits_download_uri:, + build_artifacts_cache_upload_uri:, + droplet_upload_uri:, + buildpacks:, + stack:, + app_bits_checksum: + } + message[:build_artifacts_cache_download_uri] = build_artifacts_cache_download_uri if build_artifacts_cache_download_uri + message[:buildpack_cache_checksum] = buildpack_cache_checksum if buildpack_cache_checksum + + schema.validate(message) + message + end + + private + + def schema + @schema ||= Membrane::SchemaParser.parse do + { + app_bits_download_uri: String, + optional(:build_artifacts_cache_download_uri) => String, + optional(:buildpack_cache_checksum) => String, + build_artifacts_cache_upload_uri: String, + droplet_upload_uri: String, + buildpacks: Array, + stack: String, + app_bits_checksum: Hash + } + end + end + end + end + end +end diff --git a/lib/cloud_controller/diego/buildpack/lifecycle_protocol.rb b/lib/cloud_controller/diego/buildpack/lifecycle_protocol.rb index b73a68bb1d4..8122ac7c03e 100644 --- a/lib/cloud_controller/diego/buildpack/lifecycle_protocol.rb +++ b/lib/cloud_controller/diego/buildpack/lifecycle_protocol.rb @@ -1,6 +1,6 @@ -require 'cloud_controller/diego/lifecycle_data' require 'cloud_controller/diego/droplet_url_generator' require 'cloud_controller/diego/lifecycle_protocol' +require 'cloud_controller/diego/buildpack/lifecycle_data' require 'cloud_controller/diego/buildpack/staging_action_builder' module VCAP @@ -19,6 +19,10 @@ def task_action_builder(config, task) def desired_lrp_builder(config, process) DesiredLrpBuilder.new(config, builder_opts(process)) end + + def new_lifecycle_data(_) + LifecycleData.new + end end end end diff --git a/lib/cloud_controller/diego/cnb/lifecycle_data.rb b/lib/cloud_controller/diego/cnb/lifecycle_data.rb new file mode 100644 index 00000000000..c238c863322 --- /dev/null +++ b/lib/cloud_controller/diego/cnb/lifecycle_data.rb @@ -0,0 +1,46 @@ +module VCAP::CloudController + module Diego + module CNB + class LifecycleData + attr_accessor :app_bits_download_uri, :build_artifacts_cache_download_uri, :build_artifacts_cache_upload_uri, + :buildpacks, :app_bits_checksum, :droplet_upload_uri, :stack, :buildpack_cache_checksum, + :credentials + + def message + message = { + app_bits_download_uri:, + build_artifacts_cache_upload_uri:, + droplet_upload_uri:, + buildpacks:, + stack:, + app_bits_checksum: + } + message[:build_artifacts_cache_download_uri] = build_artifacts_cache_download_uri if build_artifacts_cache_download_uri + message[:buildpack_cache_checksum] = buildpack_cache_checksum if buildpack_cache_checksum + message[:credentials] = credentials if credentials + + schema.validate(message) + message + end + + private + + def schema + @schema ||= Membrane::SchemaParser.parse do + { + app_bits_download_uri: String, + optional(:build_artifacts_cache_download_uri) => String, + optional(:buildpack_cache_checksum) => String, + build_artifacts_cache_upload_uri: String, + droplet_upload_uri: String, + buildpacks: Array, + stack: String, + app_bits_checksum: Hash, + optional(:credentials) => String + } + end + end + end + end + end +end diff --git a/lib/cloud_controller/diego/cnb/lifecycle_protocol.rb b/lib/cloud_controller/diego/cnb/lifecycle_protocol.rb index 63eef202499..7f9507987c3 100644 --- a/lib/cloud_controller/diego/cnb/lifecycle_protocol.rb +++ b/lib/cloud_controller/diego/cnb/lifecycle_protocol.rb @@ -1,7 +1,7 @@ require 'cloud_controller/diego/buildpack_entry_generator' require 'cloud_controller/diego/droplet_url_generator' -require 'cloud_controller/diego/lifecycle_data' require 'cloud_controller/diego/lifecycle_protocol' +require 'cloud_controller/diego/cnb/lifecycle_data' require 'cloud_controller/diego/cnb/staging_action_builder' require 'cloud_controller/diego/buildpack/task_action_builder' @@ -21,6 +21,13 @@ def task_action_builder(config, task) def desired_lrp_builder(config, process) DesiredLrpBuilder.new(config, builder_opts(process)) end + + def new_lifecycle_data(staging_details) + lifecycle_data = LifecycleData.new + lifecycle_data.credentials = staging_details.lifecycle.credentials + + lifecycle_data + end end end end diff --git a/lib/cloud_controller/diego/cnb/staging_action_builder.rb b/lib/cloud_controller/diego/cnb/staging_action_builder.rb index c7c39010279..28f6fb2f6a5 100644 --- a/lib/cloud_controller/diego/cnb/staging_action_builder.rb +++ b/lib/cloud_controller/diego/cnb/staging_action_builder.rb @@ -24,12 +24,14 @@ def cached_dependencies end def task_environment_variables - [ + env = [ ::Diego::Bbs::Models::EnvironmentVariable.new(name: 'CNB_USER_ID', value: '2000'), ::Diego::Bbs::Models::EnvironmentVariable.new(name: 'CNB_GROUP_ID', value: '2000'), ::Diego::Bbs::Models::EnvironmentVariable.new(name: 'CNB_STACK_ID', value: lifecycle_stack), ::Diego::Bbs::Models::EnvironmentVariable.new(name: 'LANG', value: STAGING_DEFAULT_LANG) ] + env.push(::Diego::Bbs::Models::EnvironmentVariable.new(name: 'CNB_REGISTRY_CREDS', value: lifecycle_data[:credentials])) if lifecycle_data[:credentials] + env end private diff --git a/lib/cloud_controller/diego/lifecycle_data.rb b/lib/cloud_controller/diego/lifecycle_data.rb deleted file mode 100644 index d5bbdad1be9..00000000000 --- a/lib/cloud_controller/diego/lifecycle_data.rb +++ /dev/null @@ -1,41 +0,0 @@ -module VCAP::CloudController - module Diego - class LifecycleData - attr_accessor :app_bits_download_uri, :build_artifacts_cache_download_uri, :build_artifacts_cache_upload_uri, - :buildpacks, :app_bits_checksum, :droplet_upload_uri, :stack, :buildpack_cache_checksum - - def message - message = { - app_bits_download_uri:, - build_artifacts_cache_upload_uri:, - droplet_upload_uri:, - buildpacks:, - stack:, - app_bits_checksum: - } - message[:build_artifacts_cache_download_uri] = build_artifacts_cache_download_uri if build_artifacts_cache_download_uri - message[:buildpack_cache_checksum] = buildpack_cache_checksum if buildpack_cache_checksum - - schema.validate(message) - message - end - - private - - def schema - @schema ||= Membrane::SchemaParser.parse do - { - app_bits_download_uri: String, - optional(:build_artifacts_cache_download_uri) => String, - optional(:buildpack_cache_checksum) => String, - build_artifacts_cache_upload_uri: String, - droplet_upload_uri: String, - buildpacks: Array, - stack: String, - app_bits_checksum: Hash - } - end - end - end - end -end diff --git a/lib/cloud_controller/diego/lifecycle_protocol.rb b/lib/cloud_controller/diego/lifecycle_protocol.rb index 6366bd92617..1c08b85d6d5 100644 --- a/lib/cloud_controller/diego/lifecycle_protocol.rb +++ b/lib/cloud_controller/diego/lifecycle_protocol.rb @@ -25,7 +25,7 @@ def initialize(blobstore_url_generator=::CloudController::DependencyLocator.inst def lifecycle_data(staging_details) stack = staging_details.lifecycle.staging_stack - lifecycle_data = Diego::LifecycleData.new + lifecycle_data = new_lifecycle_data(staging_details) lifecycle_data.app_bits_download_uri = @blobstore_url_generator.package_download_url(staging_details.package) lifecycle_data.app_bits_checksum = staging_details.package.checksum_info lifecycle_data.buildpack_cache_checksum = staging_details.package.app.buildpack_cache_sha256_checksum @@ -34,6 +34,7 @@ def lifecycle_data(staging_details) lifecycle_data.droplet_upload_uri = @blobstore_url_generator.droplet_upload_url(staging_details.staging_guid) lifecycle_data.buildpacks = @buildpack_entry_generator.buildpack_entries(staging_details.lifecycle.buildpack_infos, stack) lifecycle_data.stack = stack + lifecycle_data.credentials = staging_details.lifecycle.credentials lifecycle_data.message rescue Membrane::SchemaValidationError => e diff --git a/lib/cloud_controller/diego/lifecycles/app_cnb_lifecycle.rb b/lib/cloud_controller/diego/lifecycles/app_cnb_lifecycle.rb index 8524b5625e6..442f40a83c6 100644 --- a/lib/cloud_controller/diego/lifecycles/app_cnb_lifecycle.rb +++ b/lib/cloud_controller/diego/lifecycles/app_cnb_lifecycle.rb @@ -10,6 +10,7 @@ def create_lifecycle_data_model(app) CNBLifecycleDataModel.create( buildpacks:, stack:, + credentials:, app: ) end @@ -22,8 +23,18 @@ def errors [] end + def update_lifecycle_data_credentials(app) + return unless message.buildpack_data.requested?(:credentials) + + app.lifecycle_data.credentials = message.buildpack_data.credentials + end + def type Lifecycles::CNB end + + def credentials + message.buildpack_data.credentials + end end end diff --git a/lib/cloud_controller/diego/lifecycles/cnb_lifecycle.rb b/lib/cloud_controller/diego/lifecycles/cnb_lifecycle.rb index 8af7aa89748..edb47e3c63f 100644 --- a/lib/cloud_controller/diego/lifecycles/cnb_lifecycle.rb +++ b/lib/cloud_controller/diego/lifecycles/cnb_lifecycle.rb @@ -13,7 +13,8 @@ def create_lifecycle_data_model(build) VCAP::CloudController::CNBLifecycleDataModel.create( buildpacks: Array(buildpacks_to_use), stack: staging_stack, - build: build + build: build, + credentials: credentials_to_use ) end @@ -21,10 +22,18 @@ def staging_environment_variables {} end + def credentials + Oj.dump(credentials_to_use) + end + private def app_stack @package.app.cnb_lifecycle_data.try(:stack) end + + def credentials_to_use + staging_message.buildpack_data.credentials || @package.app.lifecycle_data.credentials + end end end diff --git a/spec/unit/controllers/v3/apps_controller_spec.rb b/spec/unit/controllers/v3/apps_controller_spec.rb index 4162cc69e84..92bf64cee01 100644 --- a/spec/unit/controllers/v3/apps_controller_spec.rb +++ b/spec/unit/controllers/v3/apps_controller_spec.rb @@ -458,6 +458,32 @@ end end + context 'cnb' do + before do + VCAP::CloudController::FeatureFlag.make(name: 'diego_cnb', enabled: true, error_message: nil) + end + + context 'when lifecycle data contains credentials' do + let(:request_body) do + { + name: 'some-name', + relationships: { space: { data: { guid: space.guid } } }, + lifecycle: { type: 'cnb', data: { buildpacks: ['http://buildpack.com'], credentials: { registry: { user: 'password' } } } } + } + end + + it 'returns a 201 and the app' do + post :create, params: request_body, as: :json + + response_body = parsed_body + lifecycle_data = response_body['lifecycle']['data'] + + expect(response).to have_http_status :created + expect(lifecycle_data).to eq({ 'buildpacks' => ['http://buildpack.com'], 'stack' => 'default-stack-name', 'credentials' => '***' }) + end + end + end + context 'when the space does not exist' do before do request_body[:relationships][:space][:data][:guid] = 'made-up' diff --git a/spec/unit/lib/cloud_controller/diego/cnb/lifecycle_data_spec.rb b/spec/unit/lib/cloud_controller/diego/cnb/lifecycle_data_spec.rb new file mode 100644 index 00000000000..ebb24b9384d --- /dev/null +++ b/spec/unit/lib/cloud_controller/diego/cnb/lifecycle_data_spec.rb @@ -0,0 +1,110 @@ +require 'spec_helper' +require 'cloud_controller/diego/buildpack/lifecycle_data' + +module VCAP::CloudController + module Diego + module CNB + RSpec.describe LifecycleData do + let(:lifecycle_data) do + data = LifecycleData.new + data.app_bits_download_uri = 'app_bits_download' + data.build_artifacts_cache_download_uri = 'build_artifact_download' + data.build_artifacts_cache_upload_uri = 'build_artifact_upload' + data.droplet_upload_uri = 'droplet_upload' + data.buildpacks = ['docker://gcr.io/paketo-buildpacks/nodejs'] + data.stack = 'stack' + data.buildpack_cache_checksum = 'bp-cache-checksum' + data.app_bits_checksum = { type: 'sha256', value: 'package-checksum' } + data.credentials = '{"registry":{"username":"password"}}' + data + end + + let(:lifecycle_payload) do + { + app_bits_download_uri: 'app_bits_download', + build_artifacts_cache_download_uri: 'build_artifact_download', + build_artifacts_cache_upload_uri: 'build_artifact_upload', + droplet_upload_uri: 'droplet_upload', + buildpacks: ['docker://gcr.io/paketo-buildpacks/nodejs'], + stack: 'stack', + buildpack_cache_checksum: 'bp-cache-checksum', + app_bits_checksum: { type: 'sha256', value: 'package-checksum' }, + credentials: '{"registry":{"username":"password"}}' + } + end + + it 'populates the fields' do + expect(lifecycle_data.message).to eq(lifecycle_payload) + end + + describe 'validation' do + let(:optional_keys) { %i[build_artifacts_cache_download_uri buildpack_cache_checksum credentials] } + + context 'when build artifacts cache download uri is missing' do + before do + lifecycle_data.build_artifacts_cache_download_uri = nil + end + + it 'does not raise an error' do + expect do + lifecycle_data.message + end.not_to raise_error + end + + it 'omits buildpack artifacts cache download uri from the message' do + expect(lifecycle_data.message.keys).not_to include(:build_artifacts_cache_download_uri) + end + end + + context 'when buildpack_cache_checksum is missing' do + before do + lifecycle_data.buildpack_cache_checksum = nil + end + + it 'does not raise an error' do + expect do + lifecycle_data.message + end.not_to raise_error + end + + it 'omits buildpack_cache_checksum from the message' do + expect(lifecycle_data.message.keys).not_to include(:buildpack_cache_checksum) + end + end + + context 'when credentials are missing' do + before do + lifecycle_data.credentials = nil + end + + it 'does not raise an error' do + expect do + lifecycle_data.message + end.not_to raise_error + end + + it 'omits credentials from the message' do + expect(lifecycle_data.message.keys).not_to include(:credentials) + end + end + + context 'when anything else is missing' do + let(:required_keys) { lifecycle_payload.keys - optional_keys } + + it 'fails with a schema validation error' do + required_keys.each do |key| + data = lifecycle_data.clone + data.public_send("#{key}=", nil) + expect do + data.message + end.to raise_error( + Membrane::SchemaValidationError, /{ #{key} => Expected instance of (String|Array|Hash), given an instance of NilClass }/ + ) + end + end + end + end + end + end + end +end diff --git a/spec/unit/lib/cloud_controller/diego/cnb/lifecycle_protocol_spec.rb b/spec/unit/lib/cloud_controller/diego/cnb/lifecycle_protocol_spec.rb index 53def9cff64..2fcd24b6cc9 100644 --- a/spec/unit/lib/cloud_controller/diego/cnb/lifecycle_protocol_spec.rb +++ b/spec/unit/lib/cloud_controller/diego/cnb/lifecycle_protocol_spec.rb @@ -29,7 +29,8 @@ module CNB Diego::StagingDetails.new.tap do |details| details.staging_guid = droplet.guid details.package = package - details.lifecycle = instance_double(CNBLifecycle, staging_stack: 'potato-stack', buildpack_infos: buildpack_infos) + details.lifecycle = instance_double(CNBLifecycle, staging_stack: 'potato-stack', buildpack_infos: buildpack_infos, + credentials: '{"registry":{"username":"password"}}') end end let(:buildpack_infos) { [BuildpackInfo.new('http://some-buildpack.url', nil)] } @@ -59,7 +60,8 @@ module CNB details.environment_variables = { 'nightshade_fruit' => 'potato' } details.staging_memory_in_mb = 42 details.staging_disk_in_mb = 51 - details.lifecycle = instance_double(CNBLifecycle, staging_stack: 'potato-stack', buildpack_infos: buildpack_infos) + details.lifecycle = instance_double(CNBLifecycle, staging_stack: 'potato-stack', buildpack_infos: buildpack_infos, + credentials: '{"registry":{"username":"password"}}') end end @@ -110,7 +112,8 @@ module CNB let(:droplet) { DropletModel.make(:cnb) } let(:staging_details) do StagingDetails.new.tap do |details| - details.lifecycle = instance_double(CNBLifecycle, staging_stack: 'potato-stack', buildpack_infos: 'some buildpack info') + details.lifecycle = instance_double(CNBLifecycle, staging_stack: 'potato-stack', buildpack_infos: 'some buildpack info', + credentials: '{"registry":{"username":"password"}}') details.package = package details.staging_guid = droplet.guid end @@ -135,7 +138,8 @@ module CNB build_artifacts_cache_upload_uri: 'cache-upload-url', droplet_upload_uri: 'droplet-upload-url', buildpack_cache_checksum: 'bp-cache-checksum', - app_bits_checksum: package.checksum_info + app_bits_checksum: package.checksum_info, + credentials: '{"registry":{"username":"password"}}' })) end end diff --git a/spec/unit/lib/cloud_controller/diego/cnb/staging_action_builder_spec.rb b/spec/unit/lib/cloud_controller/diego/cnb/staging_action_builder_spec.rb index 4606e4377a9..3da080341f4 100644 --- a/spec/unit/lib/cloud_controller/diego/cnb/staging_action_builder_spec.rb +++ b/spec/unit/lib/cloud_controller/diego/cnb/staging_action_builder_spec.rb @@ -34,7 +34,18 @@ module CNB details.environment_variables = env end end - let(:env) { double(:env) } + let(:env) do + { + FOO: 'bar', + BAR: 'baz' + } + end + let(:bbs_env) do + [ + ::Diego::Bbs::Models::EnvironmentVariable.new(name: 'FOO', value: 'bar'), + ::Diego::Bbs::Models::EnvironmentVariable.new(name: 'BAR', value: 'baz') + ] + end let(:stack) { 'buildpack-stack' } let(:lifecycle_data) do { @@ -49,11 +60,17 @@ module CNB } end let(:buildpacks) { [] } - let(:generated_environment) { [::Diego::Bbs::Models::EnvironmentVariable.new(name: 'generated-environment', value: 'generated-value')] } + let(:generated_environment) do + [ + ::Diego::Bbs::Models::EnvironmentVariable.new(name: 'CNB_USER_ID', value: '2000'), + ::Diego::Bbs::Models::EnvironmentVariable.new(name: 'CNB_GROUP_ID', value: '2000'), + ::Diego::Bbs::Models::EnvironmentVariable.new(name: 'CNB_REGISTRY_CREDS', value: '{"auth": {}}') + ] + end before do allow(LifecycleBundleUriGenerator).to receive(:uri).with('the-buildpack-bundle').and_return('generated-uri') - allow(BbsEnvironmentBuilder).to receive(:build).with(env).and_return(generated_environment) + allow(BbsEnvironmentBuilder).to receive(:build).with(env).and_return(bbs_env) TestConfig.override(credhub_api: nil) Stack.create(name: 'buildpack-stack') @@ -107,8 +124,8 @@ module CNB path: '/tmp/lifecycle/builder', user: 'vcap', args: ['--cache-dir', '/tmp/cache', '--cache-output', '/tmp/cache-output.tgz', '--buildpack', 'gcr.io/paketo-buildpacks/node-start', '--buildpack', - 'gcr.io/paketo-buildpacks/node-engine', '--pass-env-var', 'generated-environment'], - env: generated_environment + 'gcr.io/paketo-buildpacks/node-engine', '--pass-env-var', 'FOO', '--pass-env-var', 'BAR'], + env: bbs_env ) end @@ -276,6 +293,25 @@ module CNB group_env = ::Diego::Bbs::Models::EnvironmentVariable.new(name: 'CNB_GROUP_ID', value: '2000') expect(builder.task_environment_variables).to include(group_env) end + + it 'does not contain CNB_REGISTRY_CREDS' do + builder.task_environment_variables.each do |env| + expect(env.name).not_to eql('CNB_REGISTRY_CREDS') + end + end + + context 'when the lifecycle contains credentials' do + let(:lifecycle_data) do + { + credentials: '{"registry":{"username":"password"}}' + } + end + + it 'contains CNB_REGISTRY_CREDS' do + group_env = ::Diego::Bbs::Models::EnvironmentVariable.new(name: 'CNB_REGISTRY_CREDS', value: '{"registry":{"username":"password"}}') + expect(builder.task_environment_variables).to include(group_env) + end + end end end end diff --git a/spec/unit/lib/cloud_controller/diego/lifecycle_data_spec.rb b/spec/unit/lib/cloud_controller/diego/lifecycle_data_spec.rb index 6aa3595ad4b..c534c0b1f48 100644 --- a/spec/unit/lib/cloud_controller/diego/lifecycle_data_spec.rb +++ b/spec/unit/lib/cloud_controller/diego/lifecycle_data_spec.rb @@ -1,5 +1,5 @@ require 'spec_helper' -require 'cloud_controller/diego/lifecycle_data' +require 'cloud_controller/diego/buildpack/lifecycle_data' module VCAP::CloudController module Diego 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 index e584128d01c..1ad626ab78b 100644 --- a/spec/unit/lib/cloud_controller/diego/lifecycles/cnb_lifecycle_spec.rb +++ b/spec/unit/lib/cloud_controller/diego/lifecycles/cnb_lifecycle_spec.rb @@ -3,7 +3,7 @@ module VCAP::CloudController RSpec.describe CNBLifecycle do - let(:app) { AppModel.create(name: 'some-app', space: Space.make) } + let(:app) { AppModel.make(:cnb, name: 'some-app') } let!(:package) { PackageModel.make(type: PackageModel::BITS_TYPE, app: app) } let(:staging_message) { BuildCreateMessage.new(lifecycle: { data: request_data, type: 'cnb' }) } let(:request_data) { {} } @@ -16,17 +16,12 @@ module VCAP::CloudController context 'when the user specifies buildpacks' do let(:request_data) do { - buildpacks: %w[cool-buildpack rad-buildpack] + buildpacks: %w[docker://cool-buildpack docker://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 + build = BuildModel.make(:cnb) expect do cnb_lifecycle.create_lifecycle_data_model(build) @@ -34,24 +29,22 @@ module VCAP::CloudController data_model = VCAP::CloudController::CNBLifecycleDataModel.last - expect(data_model.buildpacks).to eq(%w[cool-buildpack rad-buildpack]) + expect(data_model.buildpacks).to eq(%w[docker://cool-buildpack docker://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(:app) { AppModel.make(:cnb, name: 'some-app') } 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]) + app.lifecycle_data.update(buildpacks: %w[docker://cool-buildpack docker://rad-buildpack]) end it 'uses the buildpacks on the app' do - build = BuildModel.make + build = BuildModel.make(:cnb) expect do cnb_lifecycle.create_lifecycle_data_model(build) @@ -59,14 +52,14 @@ module VCAP::CloudController data_model = VCAP::CloudController::CNBLifecycleDataModel.last - expect(data_model.buildpacks).to eq(%w[cool-buildpack rad-buildpack]) + expect(data_model.buildpacks).to eq(%w[docker://cool-buildpack docker://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 + build = BuildModel.make(:cnb) expect do cnb_lifecycle.create_lifecycle_data_model(build) @@ -80,13 +73,38 @@ module VCAP::CloudController end end + context 'when the user specifies credentials' do + let(:request_data) do + { credentials: '{"auth": {}}' } + end + + it 'uses those credentials' do + data_model = cnb_lifecycle.create_lifecycle_data_model(BuildModel.make(:cnb)) + expect(data_model.credentials).to eq('{"auth": {}}') + end + end + + context 'when the user does not specify credentials' do + let(:app) { AppModel.make(:cnb, name: 'some-app', space: Space.make) } + let(:request_data) { {} } + + before do + app.lifecycle_data.update(credentials: '{"auth": {}}') + end + + it 'uses credentials from package' do + data_model = cnb_lifecycle.create_lifecycle_data_model(BuildModel.make(:cnb)) + expect(data_model.credentials).to eq('{"auth": {}}') + 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) + data_model = cnb_lifecycle.create_lifecycle_data_model(BuildModel.make(:cnb)) expect(data_model.stack).to eq('cool-stack') end end @@ -96,19 +114,19 @@ module VCAP::CloudController context 'when the app has a stack' do before do - CNBLifecycleDataModel.make(app: app, stack: 'best-stack') + app.cnb_lifecycle_data = CNBLifecycleDataModel.make(stack: 'best-stack') end it 'uses the stack from the app' do - data_model = cnb_lifecycle.create_lifecycle_data_model(BuildModel.make) + data_model = cnb_lifecycle.create_lifecycle_data_model(BuildModel.make(:cnb, app:)) 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) + data_model = cnb_lifecycle.create_lifecycle_data_model(BuildModel.make(:cnb, app:)) + expect(data_model.stack).to eq(app.lifecycle_data.stack) end end end @@ -128,7 +146,7 @@ module VCAP::CloudController 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') + app.cnb_lifecycle_data = CNBLifecycleDataModel.make(app: app, stack: 'cooler-stack') end it 'uses the value set on the app' do @@ -138,59 +156,25 @@ module VCAP::CloudController 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) + expect(cnb_lifecycle.staging_stack).to eq(app.lifecycle_data.stack) end end end end describe '#buildpack_infos' do - let(:stubbed_data) { { stack: Stack.default.name, buildpack_infos: [instance_double(BuildpackInfo)] } } + let(:stubbed_data) { { stack: app.lifecycle_data.stack, buildpack_infos: [instance_double(BuildpackInfo)] } } let(:request_data) do { - buildpacks: %w[cool-buildpack rad-buildpack] + buildpacks: %w[docker://cool-buildpack docker://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(cnb_lifecycle.buildpack_infos).to have(2).items - expect(validator).to have_received(:errors) + expect(cnb_lifecycle.buildpack_infos[0].buildpack_url).to eq('docker://cool-buildpack') + expect(cnb_lifecycle.buildpack_infos[1].buildpack_url).to eq('docker://rad-buildpack') end end end diff --git a/spec/unit/lib/cloud_controller/errands/rotate_database_key_spec.rb b/spec/unit/lib/cloud_controller/errands/rotate_database_key_spec.rb index c5c5f9c3fdc..fbaf00e35e6 100644 --- a/spec/unit/lib/cloud_controller/errands/rotate_database_key_spec.rb +++ b/spec/unit/lib/cloud_controller/errands/rotate_database_key_spec.rb @@ -35,6 +35,7 @@ module VCAP::CloudController 'VCAP::CloudController::AppModel' => app, 'VCAP::CloudController::PackageModel' => PackageModel.make(:docker, package_hash: Sham.guid, error: 'a-error', docker_image: 'image', docker_username: 'user'), 'VCAP::CloudController::DropletModel' => DropletModel.make(:all_fields), + 'VCAP::CloudController::CNBLifecycleDataModel' => CNBLifecycleDataModel.make(:all_fields), 'VCAP::CloudController::BuildpackLifecycleDataModel' => BuildpackLifecycleDataModel.make(:all_fields), 'VCAP::CloudController::BuildpackLifecycleBuildpackModel' => BuildpackLifecycleBuildpackModel.make(:all_fields), 'VCAP::CloudController::TaskModel' => task, diff --git a/spec/unit/messages/app_manifest_message_spec.rb b/spec/unit/messages/app_manifest_message_spec.rb index 2f568624ccb..27ccc6c1562 100644 --- a/spec/unit/messages/app_manifest_message_spec.rb +++ b/spec/unit/messages/app_manifest_message_spec.rb @@ -2096,6 +2096,21 @@ module VCAP::CloudController expect(message.app_update_message.lifecycle_type).to eq(Lifecycles::CNB) expect(message.app_update_message.buildpack_data.buildpacks).to eq(%w[nodejs java]) expect(message.app_update_message.buildpack_data.stack).to eq(stack.name) + expect(message.app_update_message.buildpack_data.credentials).to be_nil + end + + context 'when cnb_credentials key is specified' do + let(:parsed_yaml) { { name: 'cnb', lifecycle: 'cnb', buildpacks: %w[nodejs java], stack: stack.name, cnb_credentials: { registry: { username: 'password' } } } } + + it 'adds credentials to the lifecycle_data' do + message = AppManifestMessage.create_from_yml(parsed_yaml) + + expect(message).to be_valid + expect(message.app_update_message.lifecycle_type).to eq(Lifecycles::CNB) + expect(message.app_update_message.buildpack_data.buildpacks).to eq(%w[nodejs java]) + expect(message.app_update_message.buildpack_data.stack).to eq(stack.name) + expect(message.app_update_message.buildpack_data.credentials).to eq({ registry: { username: 'password' } }) + end end end