Skip to content

Commit 98c5b21

Browse files
authored
Return frozen empty stand-in from #lifecycle_data on missing row (#5263)
The buildpack_lifecycle_data / cnb_lifecycle_data row can be destroyed independently of the owning app, build, or droplet. When that happens, calling #lifecycle_data returns nil and callers not guarding further method invocations hit NoMethodError (e.g. build_presenter.rb on lifecycle_data.to_hash), resulting in an HTTP 500 response. Fall back to a frozen empty BuildpackLifecycleDataModel / CNBLifecycleDataModel instance so callers see a well-shaped object whose to_hash returns '{ buildpacks: [], stack: nil }'. The freeze prevents any accidental attempt to persist the stand-in. Also freeze the docker-branch stand-in for the same reason and consistency. Document the possibility of an empty data payload in the buildpack and cnb sections of the v3 lifecycle doc.
1 parent 50d042d commit 98c5b21

7 files changed

Lines changed: 162 additions & 9 deletions

File tree

app/models/runtime/app_model.rb

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -109,10 +109,13 @@ def lifecycle_type
109109
end
110110

111111
def lifecycle_data
112-
return buildpack_lifecycle_data if lifecycle_type == BuildpackLifecycleDataModel::LIFECYCLE_TYPE
113-
return cnb_lifecycle_data if lifecycle_type == CNBLifecycleDataModel::LIFECYCLE_TYPE
112+
# The lifecycle_data row can be destroyed independently of this
113+
# app; fall back to a frozen empty instance so callers never see
114+
# nil (and can't accidentally persist the stand-in).
115+
return buildpack_lifecycle_data || BuildpackLifecycleDataModel.new.freeze if lifecycle_type == BuildpackLifecycleDataModel::LIFECYCLE_TYPE
116+
return cnb_lifecycle_data || CNBLifecycleDataModel.new.freeze if lifecycle_type == CNBLifecycleDataModel::LIFECYCLE_TYPE
114117

115-
DockerLifecycleDataModel.new
118+
DockerLifecycleDataModel.new.freeze
116119
end
117120

118121
def current_package

app/models/runtime/build_model.rb

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,10 +82,13 @@ def cnb_lifecycle?
8282
end
8383

8484
def lifecycle_data
85-
return buildpack_lifecycle_data if lifecycle_type == BuildpackLifecycleDataModel::LIFECYCLE_TYPE
86-
return cnb_lifecycle_data if lifecycle_type == CNBLifecycleDataModel::LIFECYCLE_TYPE
85+
# The lifecycle_data row can be destroyed independently of this
86+
# build; fall back to a frozen empty instance so callers never
87+
# see nil (and can't accidentally persist the stand-in).
88+
return buildpack_lifecycle_data || BuildpackLifecycleDataModel.new.freeze if lifecycle_type == BuildpackLifecycleDataModel::LIFECYCLE_TYPE
89+
return cnb_lifecycle_data || CNBLifecycleDataModel.new.freeze if lifecycle_type == CNBLifecycleDataModel::LIFECYCLE_TYPE
8790

88-
DockerLifecycleDataModel.new
91+
DockerLifecycleDataModel.new.freeze
8992
end
9093

9194
def staged?

app/models/runtime/droplet_model.rb

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -187,10 +187,13 @@ def lifecycle_type
187187
end
188188

189189
def lifecycle_data
190-
return buildpack_lifecycle_data if lifecycle_type == BuildpackLifecycleDataModel::LIFECYCLE_TYPE
191-
return cnb_lifecycle_data if lifecycle_type == CNBLifecycleDataModel::LIFECYCLE_TYPE
190+
# The lifecycle_data row can be destroyed independently of this
191+
# droplet; fall back to a frozen empty instance so callers never
192+
# see nil (and can't accidentally persist the stand-in).
193+
return buildpack_lifecycle_data || BuildpackLifecycleDataModel.new.freeze if lifecycle_type == BuildpackLifecycleDataModel::LIFECYCLE_TYPE
194+
return cnb_lifecycle_data || CNBLifecycleDataModel.new.freeze if lifecycle_type == CNBLifecycleDataModel::LIFECYCLE_TYPE
192195

193-
DockerLifecycleDataModel.new
196+
DockerLifecycleDataModel.new.freeze
194197
end
195198

196199
def in_final_state?

docs/v3/source/includes/concepts/_lifecycles.md.erb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ Name | Type | Description
4242
**data.buildpacks** | _list of strings_ | A list of the names of buildpacks, URLs from which they may be downloaded, or `null` to auto-detect a suitable buildpack during staging
4343
**data.stack** | _string_ | The root filesystem to use with the buildpack, for example `cflinuxfs4`
4444

45+
For records whose historical lifecycle data is no longer available,
46+
**data.buildpacks** may be an empty list and **data.stack** may be `null`.
47+
4548
### Cloud Native Buildpacks Lifecycle *(experimental)*
4649

4750
```
@@ -83,6 +86,9 @@ Name | Type | Description
8386
**data.credentials** | _object_ | Credentials used to download the configured buildpacks. This can either contain username/password or a token.
8487
**data.stack** | _string_ | The root filesystem to use with the buildpack, for example `cflinuxfs4`
8588

89+
For records whose historical lifecycle data is no longer available,
90+
**data.buildpacks** may be an empty list and **data.stack** may be `null`.
91+
8692
### Docker lifecycle
8793

8894
```

spec/unit/models/runtime/app_model_spec.rb

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,52 @@ module VCAP::CloudController
362362
expect(app_model.buildpack_lifecycle_data).to be_nil
363363
expect(app_model.cnb_lifecycle_data).to be_nil
364364
end
365+
366+
it 'returns a frozen instance so callers cannot persist the stand-in' do
367+
expect(app_model.lifecycle_data).to be_frozen
368+
end
369+
end
370+
371+
context 'when lifecycle_type is buildpack but the associated row is missing' do
372+
let(:app_model) { create(:app_model) }
373+
374+
before do
375+
app_model.buildpack_lifecycle_data.destroy
376+
app_model.reload
377+
end
378+
379+
it 'returns an empty buildpack lifecycle data stand-in' do
380+
expect(app_model.lifecycle_data).to be_a(BuildpackLifecycleDataModel)
381+
end
382+
383+
it 'returns a frozen instance so callers cannot persist the stand-in' do
384+
expect(app_model.lifecycle_data).to be_frozen
385+
end
386+
387+
it 'produces an empty to_hash' do
388+
expect(app_model.lifecycle_data.to_hash).to eq(buildpacks: [], stack: nil)
389+
end
390+
end
391+
392+
context 'when lifecycle_type is cnb but the associated row is missing' do
393+
let(:app_model) { create(:app_model, :cnb) }
394+
395+
before do
396+
app_model.cnb_lifecycle_data.destroy
397+
app_model.reload
398+
end
399+
400+
it 'returns an empty cnb lifecycle data stand-in' do
401+
expect(app_model.lifecycle_data).to be_a(CNBLifecycleDataModel)
402+
end
403+
404+
it 'returns a frozen instance so callers cannot persist the stand-in' do
405+
expect(app_model.lifecycle_data).to be_frozen
406+
end
407+
408+
it 'produces an empty to_hash' do
409+
expect(app_model.lifecycle_data.to_hash).to eq(buildpacks: [], stack: nil)
410+
end
365411
end
366412
end
367413

spec/unit/models/runtime/build_model_spec.rb

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,52 @@ module VCAP::CloudController
146146
expect(build_model.buildpack_lifecycle_data).to be_nil
147147
expect(build_model.cnb_lifecycle_data).to be_nil
148148
end
149+
150+
it 'returns a frozen instance so callers cannot persist the stand-in' do
151+
expect(build_model.lifecycle_data).to be_frozen
152+
end
153+
end
154+
155+
context 'when lifecycle_type is buildpack but the associated row is missing' do
156+
let(:build_model) { create(:build_model, app: nil) }
157+
158+
before do
159+
build_model.buildpack_lifecycle_data.destroy
160+
build_model.reload
161+
end
162+
163+
it 'returns an empty buildpack lifecycle data stand-in' do
164+
expect(build_model.lifecycle_data).to be_a(BuildpackLifecycleDataModel)
165+
end
166+
167+
it 'returns a frozen instance so callers cannot persist the stand-in' do
168+
expect(build_model.lifecycle_data).to be_frozen
169+
end
170+
171+
it 'produces an empty to_hash' do
172+
expect(build_model.lifecycle_data.to_hash).to eq(buildpacks: [], stack: nil)
173+
end
174+
end
175+
176+
context 'when lifecycle_type is cnb but the associated row is missing' do
177+
let(:build_model) { create(:build_model, :cnb, app: nil) }
178+
179+
before do
180+
build_model.cnb_lifecycle_data.destroy
181+
build_model.reload
182+
end
183+
184+
it 'returns an empty cnb lifecycle data stand-in' do
185+
expect(build_model.lifecycle_data).to be_a(CNBLifecycleDataModel)
186+
end
187+
188+
it 'returns a frozen instance so callers cannot persist the stand-in' do
189+
expect(build_model.lifecycle_data).to be_frozen
190+
end
191+
192+
it 'produces an empty to_hash' do
193+
expect(build_model.lifecycle_data.to_hash).to eq(buildpacks: [], stack: nil)
194+
end
149195
end
150196
end
151197

spec/unit/models/runtime/droplet_model_spec.rb

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,52 @@ module VCAP::CloudController
165165
expect(droplet_model.buildpack_lifecycle_data).to be_nil
166166
expect(droplet_model.cnb_lifecycle_data).to be_nil
167167
end
168+
169+
it 'returns a frozen instance so callers cannot persist the stand-in' do
170+
expect(droplet_model.lifecycle_data).to be_frozen
171+
end
172+
end
173+
174+
context 'when lifecycle_type is buildpack but the associated row is missing' do
175+
let(:droplet_model) { create(:droplet_model, app: nil) }
176+
177+
before do
178+
droplet_model.buildpack_lifecycle_data.destroy
179+
droplet_model.reload
180+
end
181+
182+
it 'returns an empty buildpack lifecycle data stand-in' do
183+
expect(droplet_model.lifecycle_data).to be_a(BuildpackLifecycleDataModel)
184+
end
185+
186+
it 'returns a frozen instance so callers cannot persist the stand-in' do
187+
expect(droplet_model.lifecycle_data).to be_frozen
188+
end
189+
190+
it 'produces an empty to_hash' do
191+
expect(droplet_model.lifecycle_data.to_hash).to eq(buildpacks: [], stack: nil)
192+
end
193+
end
194+
195+
context 'when lifecycle_type is cnb but the associated row is missing' do
196+
let(:droplet_model) { create(:droplet_model, :cnb, app: nil) }
197+
198+
before do
199+
droplet_model.cnb_lifecycle_data.destroy
200+
droplet_model.reload
201+
end
202+
203+
it 'returns an empty cnb lifecycle data stand-in' do
204+
expect(droplet_model.lifecycle_data).to be_a(CNBLifecycleDataModel)
205+
end
206+
207+
it 'returns a frozen instance so callers cannot persist the stand-in' do
208+
expect(droplet_model.lifecycle_data).to be_frozen
209+
end
210+
211+
it 'produces an empty to_hash' do
212+
expect(droplet_model.lifecycle_data.to_hash).to eq(buildpacks: [], stack: nil)
213+
end
168214
end
169215
end
170216

0 commit comments

Comments
 (0)