Skip to content
Open
18 changes: 18 additions & 0 deletions app/actions/app_create.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ def initialize(user_audit_info)

def create(message, lifecycle)
app = nil
warnings = []
AppModel.db.transaction do
app = AppModel.create(
name: message.name,
Expand All @@ -24,6 +25,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 @@ -43,10 +45,14 @@ def create(message, lifecycle)
)
end

app.instance_variable_set(:@stack_warnings, warnings)

app
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 +80,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
21 changes: 20 additions & 1 deletion app/actions/app_update.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,13 @@ def update(app, message, lifecycle)
end
end

warnings = validate_stack_state(app, message, lifecycle)
app.instance_variable_set(:@stack_warnings, warnings)

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

Expand All @@ -81,5 +86,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)
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
26 changes: 26 additions & 0 deletions app/actions/build_create.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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 All @@ -60,6 +61,7 @@ def create_and_stage(package:, lifecycle:, metadata: nil, start_after_staging: f
created_by_user_name: @user_audit_info.user_name,
created_by_user_email: @user_audit_info.user_email
)
build.instance_variable_set(:@stack_warnings, warnings)

BuildModel.db.transaction do
build.save
Expand Down Expand Up @@ -179,5 +181,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)
MetadataUpdate.update(stack, message)
Repositories::StackEventRepository.new.record_stack_update(stack, @user_audit_info, message.audit_hash)
end
Expand Down
15 changes: 15 additions & 0 deletions app/actions/v2/app_create.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ def create(request_attrs)
@access_validator.validate_access(:create, process, request_attrs)
end

warnings = validate_stack_state(request_attrs)
process.instance_variable_set(:@stack_warnings, warnings)

process
end

Expand Down Expand Up @@ -106,6 +109,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.instance_variable_get(:@stack_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)

process.stack_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)
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
4 changes: 4 additions & 0 deletions app/controllers/v3/apps_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ def create
}
)

add_warning_headers(app.stack_warnings) if app.stack_warnings&.any?

render status: :created, json: Presenters::V3::AppPresenter.new(app)
rescue AppCreate::InvalidApp => e
unprocessable!(e.message)
Expand Down Expand Up @@ -134,6 +136,8 @@ def update
}
)

add_warning_headers(app.stack_warnings) if app.stack_warnings&.any?

render status: :ok, json: Presenters::V3::AppPresenter.new(app)
rescue AppUpdate::DropletNotFound
droplet_not_found!
Expand Down
2 changes: 2 additions & 0 deletions app/controllers/v3/builds_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ def create
}
)

add_warning_headers(build.stack_warnings) if build.stack_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
17 changes: 17 additions & 0 deletions app/models/helpers/stack_states.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
module VCAP::CloudController
class StackStates
STACK_ACTIVE = 'ACTIVE'.freeze
STACK_RESTRICTED = 'RESTRICTED'.freeze
STACK_DEPRECATED = 'DEPRECATED'.freeze
STACK_DISABLED = 'DISABLED'.freeze

DEFAULT_STATE = STACK_ACTIVE

VALID_STATES = [
STACK_ACTIVE,
STACK_RESTRICTED,
STACK_DEPRECATED,
STACK_DISABLED
].freeze
end
end
2 changes: 2 additions & 0 deletions app/models/runtime/app_model.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ class AppModel < Sequel::Model(:apps)
DEFAULT_CONTAINER_USER = 'vcap'.freeze
DEFAULT_DOCKER_CONTAINER_USER = 'root'.freeze

attr_reader :stack_warnings

many_to_many :routes, join_table: :route_mappings, left_key: :app_guid, left_primary_key: :guid, right_primary_key: :guid, right_key: :route_guid
one_to_many :route_mappings, class: 'VCAP::CloudController::RouteMappingModel', key: :app_guid, primary_key: :guid
one_to_many :service_bindings, key: :app_guid, primary_key: :guid
Expand Down
2 changes: 2 additions & 0 deletions app/models/runtime/build_model.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ class BuildModel < Sequel::Model(:builds)
CNBGenericBuildFailed CNBDownloadBuildpackFailed CNBDetectFailed
CNBBuildFailed CNBExportFailed CNBLaunchFailed CNBRestoreFailed].map(&:freeze).freeze

attr_reader :stack_warnings

many_to_one :app,
class: 'VCAP::CloudController::AppModel',
key: :app_guid,
Expand Down
2 changes: 2 additions & 0 deletions app/models/runtime/process_model.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ class ProcessModel < Sequel::Model(:processes) # rubocop:disable Metrics/ClassLe

extend IntegerArraySerializer

attr_reader :stack_warnings

def after_initialize
self.instances ||= db_schema[:instances][:default].to_i
self.memory ||= Config.config.get(:default_app_memory)
Expand Down
Loading