Skip to content

Commit 59b51b6

Browse files
committed
[PoC] Implement RFC Improve Stack management
https://github.com/cloudfoundry/community/pull/1220/files
1 parent bb92b9e commit 59b51b6

File tree

18 files changed

+659
-179
lines changed

18 files changed

+659
-179
lines changed

app/actions/build_create.rb

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ def create_and_stage(package:, lifecycle:, metadata: nil, start_after_staging: f
4545
raise InvalidPackage.new('Cannot stage package whose state is not ready.') if package.state != PackageModel::READY_STATE
4646

4747
requested_buildpacks_disabled!(lifecycle)
48+
validate_stack!(lifecycle, package)
4849

4950
staging_details = get_staging_details(package, lifecycle)
5051
staging_details.start_after_staging = start_after_staging
@@ -74,11 +75,13 @@ def create_and_stage(package:, lifecycle:, metadata: nil, start_after_staging: f
7475

7576
Repositories::AppUsageEventRepository.new.create_from_build(build, 'STAGING_STARTED')
7677
app = package.app
77-
Repositories::BuildEventRepository.record_build_create(build,
78-
@user_audit_info,
79-
app.name,
80-
app.space_guid,
81-
app.organization_guid)
78+
Repositories::BuildEventRepository.record_build_create(
79+
build,
80+
@user_audit_info,
81+
app.name,
82+
app.space_guid,
83+
app.organization_guid
84+
)
8285
end
8386

8487
logger.info("build created: #{build.guid}")
@@ -93,6 +96,30 @@ def create_and_stage(package:, lifecycle:, metadata: nil, start_after_staging: f
9396

9497
private
9598

99+
def validate_stack!(lifecycle, package)
100+
return unless lifecycle.type == Lifecycles::BUILDPACK
101+
102+
stack = Stack.find(name: lifecycle.staging_stack)
103+
return unless stack
104+
105+
if stack.disabled?
106+
raise CloudController::Errors::ApiError.new_from_details(
107+
'StackDisabled',
108+
"Cannot stage app, stack '#{stack.name}' is disabled."
109+
)
110+
end
111+
112+
if stack.locked? && package.app.processes.empty?
113+
raise CloudController::Errors::ApiError.new_from_details(
114+
'StackLocked',
115+
"Cannot stage new app, stack '#{stack.name}' is locked."
116+
)
117+
end
118+
119+
# This is a warning, so we just log it.
120+
logger.warn("Stack '#{stack.name}' is deprecated.") if stack.deprecated?
121+
end
122+
96123
def requested_buildpacks_disabled!(lifecycle)
97124
return if lifecycle.type == Lifecycles::DOCKER
98125

app/actions/stack_create.rb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ class Error < ::StandardError
66
def create(message)
77
stack = VCAP::CloudController::Stack.create(
88
name: message.name,
9-
description: message.description
9+
description: message.description,
10+
deprecated_at: message.deprecated_at,
11+
locked_at: message.locked_at,
12+
disabled_at: message.disabled_at
1013
)
1114

1215
MetadataUpdate.update(stack, message)

app/actions/stack_update.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,13 @@ def initialize
99

1010
def update(stack, message)
1111
stack.db.transaction do
12+
# Update stack attributes (excluding metadata which is handled separately)
13+
stack_attributes = {}
14+
%i[deprecated_at locked_at disabled_at].each do |attr|
15+
stack_attributes[attr] = message.public_send(attr) if message.requested?(attr)
16+
end
17+
stack.set(stack_attributes) if stack_attributes.any?
18+
stack.save
1219
MetadataUpdate.update(stack, message)
1320
end
1421
@logger.info("Finished updating metadata on stack #{stack.guid}")

app/controllers/v3/stacks_controller.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ def update
5555
stack = StackUpdate.new.update(stack, message)
5656

5757
render status: :ok, json: Presenters::V3::StackPresenter.new(stack)
58+
rescue StackUpdate::InvalidStack => e
59+
unprocessable! e
5860
end
5961

6062
def show_apps

app/messages/stack_create_message.rb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@
22

33
module VCAP::CloudController
44
class StackCreateMessage < MetadataBaseMessage
5-
register_allowed_keys %i[name description]
5+
register_allowed_keys %i[name description deprecated_at locked_at disabled_at]
66

77
validates :name, presence: true, length: { maximum: 250 }
88
validates :description, length: { maximum: 250 }
9+
validates :deprecated_at, simple_timestamp: true, allow_nil: true
10+
validates :locked_at, simple_timestamp: true, allow_nil: true
11+
validates :disabled_at, simple_timestamp: true, allow_nil: true
912
end
1013
end

app/messages/stack_update_message.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,12 @@
22

33
module VCAP::CloudController
44
class StackUpdateMessage < MetadataBaseMessage
5-
register_allowed_keys []
5+
register_allowed_keys %i[deprecated_at locked_at disabled_at]
66

77
validates_with NoAdditionalKeysValidator
8+
9+
validates :deprecated_at, simple_timestamp: true, allow_nil: true
10+
validates :locked_at, simple_timestamp: true, allow_nil: true
11+
validates :disabled_at, simple_timestamp: true, allow_nil: true
812
end
913
end

app/messages/validators.rb

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,28 @@ def is_semver?(value)
341341
end
342342
end
343343

344+
module TimestampValidationHelper
345+
private
346+
347+
def opinionated_iso_8601(timestamp, record, attribute)
348+
return if timestamp.nil?
349+
return unless /\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z\Z/ !~ timestamp.to_s
350+
351+
record.errors.add(attribute, message: "has an invalid timestamp format. Timestamps should be formatted as 'YYYY-MM-DDThh:mm:ssZ'")
352+
end
353+
end
354+
355+
class SimpleTimestampValidator < ActiveModel::EachValidator
356+
include TimestampValidationHelper
357+
358+
def validate_each(record, attribute, value)
359+
opinionated_iso_8601(value, record, attribute)
360+
end
361+
end
362+
344363
class TimestampValidator < ActiveModel::EachValidator
364+
include TimestampValidationHelper
365+
345366
def validate_each(record, attribute, values)
346367
if values.is_a?(Array)
347368
values.each do |timestamp|
@@ -372,14 +393,6 @@ def validate_each(record, attribute, values)
372393
end
373394
end
374395
end
375-
376-
private
377-
378-
def opinionated_iso_8601(timestamp, record, attribute)
379-
return unless /\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z\Z/ !~ timestamp.to_s
380-
381-
record.errors.add(attribute, message: "has an invalid timestamp format. Timestamps should be formatted as 'YYYY-MM-DDThh:mm:ssZ'")
382-
end
383396
end
384397

385398
class TargetGuidsValidator < ActiveModel::Validator

app/models/runtime/stack.rb

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ class AppsStillPresentError < StandardError
2626

2727
plugin :serialization
2828

29-
export_attributes :name, :description, :build_rootfs_image, :run_rootfs_image
30-
import_attributes :name, :description, :build_rootfs_image, :run_rootfs_image
29+
export_attributes :name, :description, :build_rootfs_image, :run_rootfs_image, :deprecated_at, :locked_at, :disabled_at
30+
import_attributes :name, :description, :build_rootfs_image, :run_rootfs_image, :deprecated_at, :locked_at, :disabled_at
3131

3232
strip_attributes :name
3333

@@ -43,6 +43,19 @@ def around_save
4343
def validate
4444
validates_presence :name
4545
validates_unique :name
46+
validate_timestamp_ordering
47+
end
48+
49+
def deprecated?
50+
deprecated_at && deprecated_at <= Time.now
51+
end
52+
53+
def locked?
54+
locked_at && locked_at <= Time.now
55+
end
56+
57+
def disabled?
58+
disabled_at && disabled_at <= Time.now
4659
end
4760

4861
def before_destroy
@@ -95,8 +108,20 @@ def self.populate_from_hash(hash)
95108
stack.set(hash)
96109
Steno.logger('cc.stack').warn('stack.populate.collision', hash) if stack.modified?
97110
else
98-
create(hash.slice('name', 'description', 'build_rootfs_image', 'run_rootfs_image'))
111+
create(hash.slice('name', 'description', 'build_rootfs_image', 'run_rootfs_image', 'deprecated_at', 'locked_at', 'disabled_at'))
99112
end
100113
end
114+
115+
private
116+
117+
def validate_timestamp_ordering
118+
return unless [deprecated_at, locked_at, disabled_at].any?
119+
120+
errors.add(:deprecated_at, 'must be before locked_at') if deprecated_at && locked_at && deprecated_at > locked_at
121+
122+
return unless locked_at && disabled_at && locked_at > disabled_at
123+
124+
errors.add(:locked_at, 'must be before disabled_at')
125+
end
101126
end
102127
end

app/presenters/v3/stack_presenter.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ def to_hash
1515
run_rootfs_image: stack.run_rootfs_image,
1616
build_rootfs_image: stack.build_rootfs_image,
1717
default: stack.default?,
18+
deprecated_at: stack.deprecated_at,
19+
locked_at: stack.locked_at,
20+
disabled_at: stack.disabled_at,
1821
metadata: {
1922
labels: hashified_labels(stack.labels),
2023
annotations: hashified_annotations(stack.annotations)
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
Sequel.migration do
2+
change do
3+
alter_table(:stacks) do
4+
add_column :deprecated_at, DateTime, null: true
5+
add_column :locked_at, DateTime, null: true
6+
add_column :disabled_at, DateTime, null: true
7+
end
8+
end
9+
end

0 commit comments

Comments
 (0)