Skip to content
Draft
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
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 @@ -6,7 +6,8 @@ class Error < ::StandardError
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 @@ -9,6 +9,7 @@ def initialize

def update(stack, message)
stack.db.transaction do
stack.update(state: message.state) if message.requested?(:state)
MetadataUpdate.update(stack, message)
end
@logger.info("Finished updating metadata on stack #{stack.guid}")
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
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
Loading