Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions app/actions/app_create.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ class AppCreate

class InvalidApp < StandardError; end

attr_reader :warnings

def initialize(user_audit_info)
@user_audit_info = user_audit_info
@logger = Steno.logger('cc.action.app_create')
Expand All @@ -24,6 +26,7 @@ def create(message, lifecycle)
)

lifecycle.create_lifecycle_data_model(app)
@warnings = validate_stack_state(app, lifecycle)
validate_buildpacks_are_ready(app)

MetadataUpdate.update(app, message)
Expand All @@ -47,6 +50,8 @@ def create(message, lifecycle)
rescue Sequel::ValidationFailed => e
v3_api_error!(:UniquenessError, e.message) if e.errors.on(%i[space_guid name])

raise InvalidApp.new(e.message)
rescue StackStateValidator::DisabledStackError, StackStateValidator::RestrictedStackError => e
raise InvalidApp.new(e.message)
end

Expand Down Expand Up @@ -74,5 +79,17 @@ def validate_buildpacks_are_ready(app)
end
end
end

def validate_stack_state(app, lifecycle)
return [] if lifecycle.type == Lifecycles::DOCKER

stack_name = app.lifecycle_data.try(:stack)
return [] unless stack_name

stack = Stack.find(name: stack_name)
return [] unless stack

StackStateValidator.validate_for_new_app!(stack)
end
end
end
22 changes: 21 additions & 1 deletion app/actions/app_update.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ class AppUpdate
class DropletNotFound < StandardError; end
class InvalidApp < StandardError; end

attr_reader :warnings

def initialize(user_audit_info, manifest_triggered: false, runners: nil)
@user_audit_info = user_audit_info
@logger = Steno.logger('cc.action.app_update')
Expand Down Expand Up @@ -53,8 +55,12 @@ def update(app, message, lifecycle)
end
end

@warnings = validate_stack_state(app, message, lifecycle)

app
rescue Sequel::ValidationFailed => e
rescue Sequel::ValidationFailed,
StackStateValidator::DisabledStackError,
StackStateValidator::RestrictedStackError => e
raise InvalidApp.new(e.message)
end

Expand All @@ -81,5 +87,19 @@ def validate_not_changing_lifecycle_type!(app, lifecycle)
def existing_environment_variables_for(app)
app.environment_variables.nil? ? {} : app.environment_variables.symbolize_keys
end

def validate_stack_state(app, message, lifecycle)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question Do we need to check that they are not trying to update from an active stack to a restricted stack?

e.g.

# push brand new app with active stack
cf push my-app --stack cflinuxfs4

# update app with restricted stack
cf push my-app --stack cf-linuxfs3

return [] if lifecycle.type == Lifecycles::DOCKER
return [] unless message.requested?(:lifecycle) && message.buildpack_data.requested?(:stack)

stack = Stack.find(name: message.buildpack_data.stack)
return [] unless stack

if app.builds_dataset.count.zero?
StackStateValidator.validate_for_new_app!(stack)
else
StackStateValidator.validate_for_restaging!(stack)
end
end
end
end
27 changes: 26 additions & 1 deletion app/actions/build_create.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class LogRateLimitOrgQuotaExceeded < BuildError
class StagingInProgress < BuildError
end

attr_reader :staging_response
attr_reader :staging_response, :warnings

def initialize(user_audit_info: UserAuditInfo.from_context(SecurityContext),
memory_limit_calculator: QuotaValidatingStagingMemoryCalculator.new,
Expand All @@ -41,6 +41,7 @@ def initialize(user_audit_info: UserAuditInfo.from_context(SecurityContext),

def create_and_stage(package:, lifecycle:, metadata: nil, start_after_staging: false)
logger.info("creating build for package #{package.guid}")
@warnings = validate_stack_state!(lifecycle, package.app)
staging_in_progress! if package.app.staging_in_progress?
raise InvalidPackage.new('Cannot stage package whose state is not ready.') if package.state != PackageModel::READY_STATE

Expand Down Expand Up @@ -179,5 +180,29 @@ def stagers
def staging_in_progress!
raise StagingInProgress
end

def validate_stack_state!(lifecycle, app)
return [] if lifecycle.type == Lifecycles::DOCKER

stack = Stack.find(name: lifecycle.staging_stack)
return [] unless stack

warnings = if first_build_for_app?(app)
StackStateValidator.validate_for_new_app!(stack)
else
StackStateValidator.validate_for_restaging!(stack)
end
warnings.each { |warning| logger.warn(warning) }
warnings
rescue StackStateValidator::DisabledStackError, StackStateValidator::RestrictedStackError => e
raise CloudController::Errors::ApiError.new_from_details(
'StackValidationFailed',
e.message
)
end

def first_build_for_app?(app)
app.builds_dataset.count.zero?
end
end
end
3 changes: 2 additions & 1 deletion app/actions/stack_create.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ def initialize(user_audit_info)
def create(message)
stack = VCAP::CloudController::Stack.create(
name: message.name,
description: message.description
description: message.description,
state: message.state
)

MetadataUpdate.update(stack, message)
Expand Down
1 change: 1 addition & 0 deletions app/actions/stack_update.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ def initialize(user_audit_info)

def update(stack, message)
stack.db.transaction do
stack.update(state: message.state) if message.requested?(:state)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question Do we want to allow disabling/restricting the default stack?

MetadataUpdate.update(stack, message)
Repositories::StackEventRepository.new.record_stack_update(stack, @user_audit_info, message.audit_hash)
end
Expand Down
16 changes: 16 additions & 0 deletions app/actions/v2/app_create.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
module VCAP::CloudController
module V2
class AppCreate
attr_reader :warnings

def initialize(access_validator:)
@access_validator = access_validator
end
Expand Down Expand Up @@ -46,6 +48,8 @@ def create(request_attrs)
@access_validator.validate_access(:create, process, request_attrs)
end

@warnings = validate_stack_state(request_attrs)

process
end

Expand Down Expand Up @@ -106,6 +110,18 @@ def validate_package_exists!(process, request_attrs)

raise CloudController::Errors::ApiError.new_from_details('AppPackageInvalid', 'bits have not been uploaded')
end

def validate_stack_state(request_attrs)
return [] if request_attrs.key?('docker_image')

stack_name = get_stack_name(request_attrs['stack_guid'])
stack = Stack.find(name: stack_name)
return [] unless stack

StackStateValidator.validate_for_new_app!(stack)
rescue StackStateValidator::DisabledStackError, StackStateValidator::RestrictedStackError => e
raise CloudController::Errors::ApiError.new_from_details('StackValidationFailed', e.message)
end
end
end
end
6 changes: 6 additions & 0 deletions app/actions/v2/app_stage.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
module VCAP::CloudController
module V2
class AppStage
attr_reader :warnings

def initialize(stagers:)
@stagers = stagers
@warnings = []
end

def stage(process)
Expand All @@ -25,6 +28,9 @@ def stage(process)
lifecycle: lifecycle,
start_after_staging: true
)

@warnings = build_creator.warnings || []

TelemetryLogger.v2_emit(
'create-build',
{
Expand Down
25 changes: 24 additions & 1 deletion app/actions/v2/app_update.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@
module VCAP::CloudController
module V2
class AppUpdate
attr_reader :warnings

def initialize(access_validator:, stagers:)
@access_validator = access_validator
@stagers = stagers
@warnings = []
end

def update(app, process, request_attrs)
Expand Down Expand Up @@ -36,6 +39,7 @@ def update(app, process, request_attrs)
prepare_to_stage(app) if staging_necessary?(process, request_attrs)
end

validate_stack_state(app, request_attrs)
stage(process) if staging_necessary?(process, request_attrs)
end

Expand Down Expand Up @@ -116,7 +120,9 @@ def prepare_to_stage(app)
end

def stage(process)
V2::AppStage.new(stagers: @stagers).stage(process)
app_stage = V2::AppStage.new(stagers: @stagers)
app_stage.stage(process)
@warnings = app_stage.warnings
end

def start_or_stop(app, request_attrs)
Expand Down Expand Up @@ -179,6 +185,23 @@ def staging_necessary?(process, request_attrs)
def v2_api_staging_disabled?
!!VCAP::CloudController::Config.config.get(:temporary_disable_v2_staging)
end

def validate_stack_state(app, request_attrs)
return unless request_attrs.key?('stack_guid')
return if request_attrs.key?('docker_image')

stack = Stack.find(guid: request_attrs['stack_guid'])
return unless stack

stack_warnings = if app.builds_dataset.count.zero?
StackStateValidator.validate_for_new_app!(stack)
else
StackStateValidator.validate_for_restaging!(stack)
end
@warnings.concat(stack_warnings)
rescue StackStateValidator::DisabledStackError, StackStateValidator::RestrictedStackError => e
raise CloudController::Errors::ApiError.new_from_details('StackValidationFailed', e.message)
end
end
end
end
3 changes: 3 additions & 0 deletions app/controllers/runtime/apps_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,7 @@ def update(guid)

updater = V2::AppUpdate.new(access_validator: self, stagers: @stagers)
updater.update(app, process, request_attrs)
updater.warnings.each { |warning| add_warning(warning) }

after_update(process)

Expand Down Expand Up @@ -357,6 +358,8 @@ def create
creator = V2::AppCreate.new(access_validator: self)
process = creator.create(request_attrs)

creator.warnings&.each { |warning| add_warning(warning) }

@app_event_repository.record_app_create(
process,
process.space,
Expand Down
5 changes: 4 additions & 1 deletion app/controllers/runtime/restages_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,10 @@ def restage(guid)
process.app.update(droplet_guid: nil)
AppStart.start_without_event(process.app, create_revision: false)
end
V2::AppStage.new(stagers: @stagers).stage(process)
# V2::AppStage.new(stagers: @stagers).stage(process)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit Remove comment

app_stage = V2::AppStage.new(stagers: @stagers)
app_stage.stage(process)
app_stage.warnings.each { |warning| add_warning(warning) }

@app_event_repository.record_app_restage(process, UserAuditInfo.from_context(SecurityContext))

Expand Down
10 changes: 8 additions & 2 deletions app/controllers/v3/apps_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,8 @@ def create
FeatureFlag.raise_unless_enabled!(:diego_cnb) if lifecycle.type == VCAP::CloudController::Lifecycles::CNB
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question Not sure how aggressive we want to be in restriction. But do we also need to validate stacks on droplet copying? https://v3-apidocs.cloudfoundry.org/version/3.209.0/index.html#copy-a-droplet

E.g.

  1. Existing App A with restricted stack restaged.
  2. New App B created with active stack
  3. Copy droplet from App A to App B bypassing restriction

unprocessable!(lifecycle.errors.full_messages) unless lifecycle.valid?

app = AppCreate.new(user_audit_info).create(message, lifecycle)
app_creator = AppCreate.new(user_audit_info)
app = app_creator.create(message, lifecycle)
TelemetryLogger.v3_emit(
'create-app',
{
Expand All @@ -107,6 +108,8 @@ def create
}
)

add_warning_headers(app_creator.warnings) if app_creator.warnings&.any?

render status: :created, json: Presenters::V3::AppPresenter.new(app)
rescue AppCreate::InvalidApp => e
unprocessable!(e.message)
Expand All @@ -125,7 +128,8 @@ def update
lifecycle = AppLifecycleProvider.provide_for_update(message, app)
unprocessable!(lifecycle.errors.full_messages) unless lifecycle.valid?

app = AppUpdate.new(user_audit_info).update(app, message, lifecycle)
app_updater = AppUpdate.new(user_audit_info)
app = app_updater.update(app, message, lifecycle)
TelemetryLogger.v3_emit(
'update-app',
{
Expand All @@ -134,6 +138,8 @@ def update
}
)

add_warning_headers(app_updater.warnings) if app_updater.warnings&.any?

render status: :ok, json: Presenters::V3::AppPresenter.new(app)
rescue AppUpdate::DropletNotFound
droplet_not_found!
Expand Down
5 changes: 4 additions & 1 deletion app/controllers/v3/builds_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ def create
FeatureFlag.raise_unless_enabled!(:diego_cnb) if lifecycle.type == VCAP::CloudController::Lifecycles::CNB
unprocessable!(lifecycle.errors.full_messages) unless lifecycle.valid?

build = BuildCreate.new.create_and_stage(package: package, lifecycle: lifecycle, metadata: message.metadata)
build_creator = BuildCreate.new
build = build_creator.create_and_stage(package: package, lifecycle: lifecycle, metadata: message.metadata)

TelemetryLogger.v3_emit(
'create-build',
Expand All @@ -65,6 +66,8 @@ def create
}
)

add_warning_headers(build_creator.warnings) if build_creator.warnings&.any?

render status: :created, json: Presenters::V3::BuildPresenter.new(build)
rescue BuildCreate::InvalidPackage => e
bad_request!(e.message)
Expand Down
14 changes: 13 additions & 1 deletion app/messages/stack_create_message.rb
Original file line number Diff line number Diff line change
@@ -1,10 +1,22 @@
require 'messages/metadata_base_message'
require 'models/helpers/stack_states'

module VCAP::CloudController
class StackCreateMessage < MetadataBaseMessage
register_allowed_keys %i[name description]
register_allowed_keys %i[name description state]

validates :name, presence: true, length: { maximum: 250 }
validates :description, length: { maximum: 250 }
validates :state, inclusion: { in: StackStates::VALID_STATES, message: "must be one of #{StackStates::VALID_STATES.join(', ')}" }, allow_nil: false, if: :state_requested?

def state_requested?
requested?(:state)
end

def state
return @state if defined?(@state)

@state = requested?(:state) ? super : StackStates::DEFAULT_STATE
end
end
end
8 changes: 7 additions & 1 deletion app/messages/stack_update_message.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
require 'messages/metadata_base_message'
require 'models/helpers/stack_states'

module VCAP::CloudController
class StackUpdateMessage < MetadataBaseMessage
register_allowed_keys []
register_allowed_keys [:state]

validates_with NoAdditionalKeysValidator
validates :state, inclusion: { in: StackStates::VALID_STATES, message: "must be one of #{StackStates::VALID_STATES.join(', ')}" }, allow_nil: false, if: :state_requested?

def state_requested?
requested?(:state)
end
end
end
Loading