Skip to content
Draft
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
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
7 changes: 6 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 @@ -116,7 +119,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
1 change: 1 addition & 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
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
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/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
26 changes: 26 additions & 0 deletions app/models/runtime/stack.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
require 'models/helpers/process_types'
require 'models/helpers/stack_config_file'
require 'models/helpers/stack_states'

module VCAP::CloudController
class Stack < Sequel::Model
Expand Down Expand Up @@ -43,6 +44,7 @@ def around_save
def validate
validates_presence :name
validates_unique :name
validates_includes StackStates::VALID_STATES, :state, allow_nil: true
end

def before_destroy
Expand Down Expand Up @@ -98,5 +100,29 @@ def self.populate_from_hash(hash)
create(hash.slice('name', 'description', 'build_rootfs_image', 'run_rootfs_image'))
end
end

def active?
state == StackStates::STACK_ACTIVE
end

def deprecated?
state == StackStates::STACK_DEPRECATED
end

def restricted?
state == StackStates::STACK_RESTRICTED
end

def disabled?
state == StackStates::STACK_DISABLED
end

def can_stage_new_app?
!restricted? && !disabled?
end

def can_restage_apps?
!disabled?
end
end
end
7 changes: 7 additions & 0 deletions app/presenters/v3/build_presenter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ def to_hash
},
package: { guid: build.package_guid },
droplet: droplet,
warnings: build_warnings,
created_by: {
guid: build.created_by_user_guid,
name: build.created_by_user_name,
Expand Down Expand Up @@ -61,6 +62,12 @@ def error
e.presence
end

def build_warnings
return nil unless build.stack_warnings&.any?

build.stack_warnings.map { |warning| { detail: warning } }
end

def build_links
{
self: { href: url_builder.build_url(path: "/v3/builds/#{build.guid}") },
Expand Down
1 change: 1 addition & 0 deletions app/presenters/v3/stack_presenter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ def to_hash
updated_at: stack.updated_at,
name: stack.name,
description: stack.description,
state: stack.state,
run_rootfs_image: stack.run_rootfs_image,
build_rootfs_image: stack.build_rootfs_image,
default: stack.default?,
Expand Down
13 changes: 13 additions & 0 deletions db/migrations/20251117123719_add_state_to_stacks.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
Sequel.migration do
up do
alter_table :stacks do
add_column :state, String, null: false, default: 'ACTIVE', size: 255 unless @db.schema(:stacks).map(&:first).include?(:state)
end
end

down do
alter_table :stacks do
drop_column :state if @db.schema(:stacks).map(&:first).include?(:state)
end
end
end
26 changes: 26 additions & 0 deletions docs/v3/source/includes/api_resources/_stacks.erb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"updated_at": "2018-11-09T22:43:28Z",
"name": "my-stack",
"description": "Here is my stack!",
"state": "ACTIVE",
"build_rootfs_image": "my-stack",
"run_rootfs_image": "my-stack",
"default": true,
Expand Down Expand Up @@ -45,6 +46,7 @@
"build_rootfs_image": "my-stack-1-build",
"run_rootfs_image": "my-stack-1-run",
"description": "This is my first stack!",
"state": "ACTIVE",
"default": true,
"metadata": {
"labels": {},
Expand All @@ -64,6 +66,7 @@
"description": "This is my second stack!",
"build_rootfs_image": "my-stack-2-build",
"run_rootfs_image": "my-stack-2-run",
"state": "DEPRECATED",
"default": false,
"metadata": {
"labels": {},
Expand All @@ -79,3 +82,26 @@
}

<% end %>

<% content_for :single_stack_disabled do | metadata={} | %>
{
"guid": "11c916c9-c2f9-440e-8e73-102e79c4704d",
"created_at": "2018-11-09T22:43:28Z",
"updated_at": "2018-11-09T22:43:28Z",
"name": "my-stack",
"description": "Here is my stack!",
"state": "ACTIVE",
"build_rootfs_image": "my-stack",
"run_rootfs_image": "my-stack",
"default": true,
"metadata": {
"labels": <%= metadata.fetch(:labels, {}).to_json(space: ' ', object_nl: ' ')%>,
"annotations": <%= metadata.fetch(:annotations, {}).to_json(space: ' ', object_nl: ' ')%>
},
"links": {
"self": {
"href": "https://api.example.com/v3/stacks/11c916c9-c2f9-440e-8e73-102e79c4704d"
}
}
}
<% end %>
1 change: 1 addition & 0 deletions docs/v3/source/includes/resources/stacks/_object.md.erb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Name | Type | Description
**updated_at** | _[timestamp](#timestamps)_ | The time with zone when the object was last updated
**name** | _string_ | The name of the stack
**description** | _string_ | The description of the stack
**state** | string | The state of the stack; valid states are: `ACTIVE`, `RESTRICTED`, `DEPRECATED`, `DISABLED`
**build_rootfs_image** | _string | The name of the stack image associated with staging/building Apps. If a stack does not have unique images, this will be the same as the stack name.
**run_rootfs_image** | _string | The name of the stack image associated with running Apps + Tasks. If a stack does not have unique images, this will be the same as the stack name.
**default** | _boolean_ | Whether the stack is configured to be the default stack for new applications.
Expand Down
5 changes: 3 additions & 2 deletions docs/v3/source/includes/resources/stacks/_update.md.erb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ curl "https://api.example.org/v3/stacks/[guid]" \
-X PATCH \
-H "Authorization: bearer [token]" \
-H "Content-Type: application/json" \
-d '{ "metadata": { "labels": { "key": "value" }, "annotations": {"note": "detailed information"}}}'
-d '{ "metadata": { "labels": { "key": "value" }, "annotations": {"note": "detailed information"}, "state": "DISABLED" }}'

```

Expand All @@ -21,7 +21,7 @@ Example Response
HTTP/1.1 200 OK
Content-Type: application/json

<%= yield_content :single_stack, labels: { "key" => "value" }, "annotations": {"note" => "detailed information"} %>
<%= yield_content :single_stack_disabled, labels: { "key" => "value" }, "annotations": {"note" => "detailed information"} %>
```

#### Definition
Expand All @@ -33,6 +33,7 @@ Name | Type | Description
---- | ---- | -----------
**metadata.labels** | [_label object_](#labels) | Labels applied to the stack
**metadata.annotations** | [_annotation object_](#annotations) | Annotations applied to the stack
**state** | string | The state of the stack; valid states are: `ACTIVE`, `RESTRICTED`, `DEPRECATED`, `DISABLED`

#### Permitted roles
|
Expand Down
5 changes: 5 additions & 0 deletions errors/v2.yml
Original file line number Diff line number Diff line change
Expand Up @@ -863,6 +863,11 @@
http_code: 404
message: "The stack could not be found: %s"

250004:
name: StackValidationFailed
http_code: 422
message: "%s"

260001:
name: ServicePlanVisibilityInvalid
http_code: 400
Expand Down
2 changes: 2 additions & 0 deletions lib/cloud_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,5 @@ module VCAP::CloudController; end
require 'cloud_controller/errands/rotate_database_key'

require 'services'

require 'cloud_controller/stack_state_validator'
Loading