Skip to content

Commit b309377

Browse files
committed
Install buildpacks based on order on config
- Account for existing system buildpacks by offsetting the install position - Skip index for buildpacks with explicit position in config resolves #3798
1 parent 13d9fc2 commit b309377

File tree

6 files changed

+181
-50
lines changed

6 files changed

+181
-50
lines changed

app/jobs/runtime/buildpack_installer_factory.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,8 @@ def plan(buildpack_name, manifest_fields)
5656
name: buildpack_name,
5757
stack: buildpack_fields[:stack],
5858
file: buildpack_fields[:file],
59-
options: buildpack_fields[:options]
59+
options: buildpack_fields[:options],
60+
config_index: buildpack_fields[:config_index]
6061
})
6162
end
6263
end

app/jobs/runtime/create_buildpack_installer.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@ module VCAP::CloudController
22
module Jobs
33
module Runtime
44
class CreateBuildpackInstaller < BuildpackInstaller
5+
attr_accessor :config_index
6+
7+
def initialize(job_options)
8+
super
9+
@config_index = job_options[:config_index]
10+
end
11+
512
def perform
613
logger.info "Installing buildpack name `#{name}' with stack `#{stack_name}'"
714

@@ -11,6 +18,8 @@ def perform
1118
buildpacks_lock.db.transaction do
1219
buildpacks_lock.lock!
1320
buildpack = Buildpack.create(name: name, stack: stack_name, lifecycle: options[:lifecycle])
21+
# config_index is pre-calculated to account for existing buildpacks, so we can use it directly
22+
buildpack.move_to(config_index + 1) if !config_index.nil? && !options.key?(:position)
1423
end
1524
begin
1625
buildpack_uploader.upload_buildpack(buildpack, file, File.basename(file))

lib/cloud_controller/install_buildpacks.rb

Lines changed: 46 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -12,34 +12,15 @@ def install(buildpacks)
1212
CloudController::DependencyLocator.instance.buildpack_blobstore.ensure_bucket_exists
1313
job_factory = VCAP::CloudController::Jobs::Runtime::BuildpackInstallerFactory.new
1414

15+
existing_count_by_lifecycle = {
16+
Lifecycles::BUILDPACK => Buildpack.where(lifecycle: Lifecycles::BUILDPACK).count,
17+
Lifecycles::CNB => Buildpack.where(lifecycle: Lifecycles::CNB).count
18+
}
19+
1520
factory_options = []
1621
buildpacks.each do |bpack|
17-
buildpack_opts = bpack.deep_symbolize_keys
18-
19-
buildpack_opts[:lifecycle] = Lifecycles::BUILDPACK if buildpack_opts[:lifecycle].nil?
20-
21-
buildpack_name = buildpack_opts.delete(:name)
22-
if buildpack_name.nil?
23-
logger.error "A name must be specified for the buildpack_opts: #{buildpack_opts}"
24-
next
25-
end
26-
27-
package = buildpack_opts.delete(:package)
28-
buildpack_file = buildpack_opts.delete(:file)
29-
if package.nil? && buildpack_file.nil?
30-
logger.error "A package or file must be specified for the buildpack_opts: #{bpack}"
31-
next
32-
end
33-
34-
buildpack_file = buildpack_zip(package, buildpack_file)
35-
if buildpack_file.nil?
36-
logger.error "No file found for the buildpack_opts: #{bpack}"
37-
next
38-
elsif !File.file?(buildpack_file)
39-
logger.error "File not found: #{buildpack_file}, for the buildpack_opts: #{bpack}"
40-
next
41-
end
42-
factory_options << { name: buildpack_name, file: buildpack_file, options: buildpack_opts, stack: detected_stack(buildpack_file, buildpack_opts) }
22+
opts = buildpack_factory_options(bpack, existing_count_by_lifecycle, factory_options)
23+
factory_options << opts if opts
4324
end
4425

4526
buildpack_install_jobs = generate_install_jobs(factory_options, job_factory)
@@ -55,6 +36,45 @@ def logger
5536

5637
private
5738

39+
def buildpack_factory_options(bpack, existing_count_by_lifecycle, factory_options)
40+
buildpack_opts = bpack.deep_symbolize_keys
41+
buildpack_opts[:lifecycle] = Lifecycles::BUILDPACK if buildpack_opts[:lifecycle].nil?
42+
43+
buildpack_name = buildpack_opts.delete(:name)
44+
if buildpack_name.nil?
45+
logger.error "A name must be specified for the buildpack_opts: #{buildpack_opts}"
46+
return
47+
end
48+
49+
package = buildpack_opts.delete(:package)
50+
buildpack_file = buildpack_opts.delete(:file)
51+
if package.nil? && buildpack_file.nil?
52+
logger.error "A package or file must be specified for the buildpack_opts: #{bpack}"
53+
return
54+
end
55+
56+
buildpack_file = buildpack_zip(package, buildpack_file)
57+
if buildpack_file.nil?
58+
logger.error "No file found for the buildpack_opts: #{bpack}"
59+
return
60+
elsif !File.file?(buildpack_file)
61+
logger.error "File not found: #{buildpack_file}, for the buildpack_opts: #{bpack}"
62+
return
63+
end
64+
65+
lifecycle = buildpack_opts[:lifecycle]
66+
existing_offset = existing_count_by_lifecycle[lifecycle] || 0
67+
# exclude counting buildpacks with position because they don't need a config index
68+
unless buildpack_opts.key?(:position)
69+
config_index = existing_offset + factory_options.count do |opts|
70+
opts[:options][:lifecycle] == lifecycle && !opts[:options].key?(:position)
71+
end
72+
end
73+
74+
{ name: buildpack_name, file: buildpack_file, options: buildpack_opts, stack: detected_stack(buildpack_file, buildpack_opts),
75+
config_index: config_index }
76+
end
77+
5878
def generate_install_jobs(factory_options, job_factory)
5979
buildpack_install_jobs = []
6080
buildpacks_by_lifecycle = factory_options.group_by { |options| options[:options][:lifecycle] }

spec/unit/jobs/runtime/buildpack_installer_factory_spec.rb

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ module Jobs::Runtime
1515
end
1616

1717
context 'when the manifest has one buildpack' do
18-
let(:buildpack_fields) { [{ file: file, options: opts }] }
18+
let(:buildpack_fields) { [{ file: file, options: opts, config_index: 0 }] }
1919
let(:single_buildpack_job) { jobs.first }
2020

2121
shared_examples_for 'passthrough parameters' do
@@ -34,7 +34,7 @@ module Jobs::Runtime
3434

3535
context 'when there is no matching buildpack record by name' do
3636
context 'and there is a detected stack in the zipfile' do
37-
let(:buildpack_fields) { [{ file: file, options: opts, stack: 'detected stack' }] }
37+
let(:buildpack_fields) { [{ file: file, options: opts, stack: 'detected stack', config_index: 0 }] }
3838

3939
include_examples 'passthrough parameters'
4040

@@ -45,6 +45,18 @@ module Jobs::Runtime
4545
it 'sets the stack to the detected stack' do
4646
expect(single_buildpack_job.stack_name).to eq('detected stack')
4747
end
48+
49+
it 'passes config_index to the create job' do
50+
expect(single_buildpack_job.config_index).to eq(0)
51+
end
52+
53+
context 'with a specific config_index' do
54+
let(:buildpack_fields) { [{ file: file, options: opts, stack: 'detected stack', config_index: 3 }] }
55+
56+
it 'passes the config_index through' do
57+
expect(single_buildpack_job.config_index).to eq(3)
58+
end
59+
end
4860
end
4961

5062
context 'and there is not a detected stack in the zipfile' do
@@ -66,7 +78,7 @@ module Jobs::Runtime
6678
let!(:existing_buildpack) { Buildpack.make(name: name, stack: existing_stack.name, key: 'new_key', guid: 'the-guid') }
6779

6880
context 'and the buildpack zip has the same stack' do
69-
let(:buildpack_fields) { [{ file: file, options: opts, stack: existing_stack.name }] }
81+
let(:buildpack_fields) { [{ file: file, options: opts, stack: existing_stack.name, config_index: 0 }] }
7082

7183
include_examples 'passthrough parameters'
7284

@@ -84,7 +96,7 @@ module Jobs::Runtime
8496
end
8597

8698
context 'and the buildpack zip has a different stack' do
87-
let(:buildpack_fields) { [{ file: file, options: opts, stack: 'manifest stack' }] }
99+
let(:buildpack_fields) { [{ file: file, options: opts, stack: 'manifest stack', config_index: 0 }] }
88100

89101
include_examples 'passthrough parameters'
90102

@@ -98,7 +110,7 @@ module Jobs::Runtime
98110
end
99111

100112
context 'and the manifest stack is nil' do
101-
let(:buildpack_fields) { [{ file: file, options: opts, stack: nil }] }
113+
let(:buildpack_fields) { [{ file: file, options: opts, stack: nil, config_index: 0 }] }
102114

103115
it 'errors' do
104116
expect do
@@ -112,7 +124,7 @@ module Jobs::Runtime
112124
let!(:existing_buildpack) { Buildpack.make(name: name, stack: nil, key: 'new_key', guid: 'the-guid') }
113125

114126
context 'and the buildpack zip also has a nil stack' do
115-
let(:buildpack_fields) { [{ file: file, options: opts, stack: nil }] }
127+
let(:buildpack_fields) { [{ file: file, options: opts, stack: nil, config_index: 0 }] }
116128

117129
include_examples 'passthrough parameters'
118130

@@ -130,7 +142,7 @@ module Jobs::Runtime
130142
end
131143

132144
context 'but the buildpack zip /has/ a stack' do
133-
let(:buildpack_fields) { [{ file: file, options: opts, stack: 'manifest stack' }] }
145+
let(:buildpack_fields) { [{ file: file, options: opts, stack: 'manifest stack', config_index: 0 }] }
134146

135147
include_examples 'passthrough parameters'
136148

@@ -157,7 +169,7 @@ module Jobs::Runtime
157169
let!(:another_existing_buildpack) { Buildpack.make(name: name, stack: another_existing_stack.name, key: 'new_key', guid: 'another-guid') }
158170

159171
context 'and one matches the manifest stack' do
160-
let(:buildpack_fields) { [{ file: file, options: opts, stack: existing_stack.name }] }
172+
let(:buildpack_fields) { [{ file: file, options: opts, stack: existing_stack.name, config_index: 0 }] }
161173

162174
include_examples 'passthrough parameters'
163175

@@ -175,7 +187,7 @@ module Jobs::Runtime
175187
end
176188

177189
context 'and the manifest stack is nil' do
178-
let(:buildpack_fields) { [{ file: file, options: opts, stack: nil }] }
190+
let(:buildpack_fields) { [{ file: file, options: opts, stack: nil, config_index: 0 }] }
179191

180192
it 'errors' do
181193
expect do
@@ -185,7 +197,7 @@ module Jobs::Runtime
185197
end
186198

187199
context 'and none match the manifest stack' do
188-
let(:buildpack_fields) { [{ file: file, options: opts, stack: 'manifest stack' }] }
200+
let(:buildpack_fields) { [{ file: file, options: opts, stack: 'manifest stack', config_index: 0 }] }
189201

190202
include_examples 'passthrough parameters'
191203

@@ -202,7 +214,9 @@ module Jobs::Runtime
202214

203215
context 'when the manifest has multiple buildpack entries for one name, with different stacks' do
204216
let(:another_file) { 'the-other-file' }
205-
let(:buildpack_fields) { [{ file: file, options: opts, stack: 'existing stack' }, { file: another_file, options: opts, stack: 'manifest stack' }] }
217+
let(:buildpack_fields) do
218+
[{ file: file, options: opts, stack: 'existing stack', config_index: 0 }, { file: another_file, options: opts, stack: 'manifest stack', config_index: 1 }]
219+
end
206220

207221
context 'and there are no matching Buildpacks' do
208222
it 'plans to create all the Buildpacks' do
@@ -277,7 +291,10 @@ module Jobs::Runtime
277291

278292
let(:another_existing_stack) { Stack.make(name: 'another existing stack') }
279293
let!(:another_existing_buildpack) { Buildpack.make(name: name, stack: another_existing_stack.name, key: 'a_different_key', guid: 'a-different-guid') }
280-
let(:buildpack_fields) { [{ file: file, options: opts, stack: existing_stack.name }, { file: another_file, options: opts, stack: another_existing_stack.name }] }
294+
let(:buildpack_fields) do
295+
[{ file: file, options: opts, stack: existing_stack.name, config_index: 0 },
296+
{ file: another_file, options: opts, stack: another_existing_stack.name, config_index: 1 }]
297+
end
281298

282299
context 'and none of them has a nil stack' do
283300
it 'creates a job for each buildpack' do
@@ -297,7 +314,7 @@ module Jobs::Runtime
297314
end
298315

299316
context 'when one of them is stackless' do
300-
let(:buildpack_fields) { [{ file: file, options: opts }] }
317+
let(:buildpack_fields) { [{ file: file, options: opts, config_index: 0 }] }
301318

302319
before do
303320
Stack.make(name: 'existing stack')
@@ -316,7 +333,7 @@ module Jobs::Runtime
316333

317334
context 'when the manifest has multiple buildpack entries for one name, with the same stack' do
318335
let(:another_file) { 'the-other-file' }
319-
let(:buildpack_fields) { [{ file: file, options: opts, stack: 'stack' }, { file: another_file, options: opts, stack: 'stack' }] }
336+
let(:buildpack_fields) { [{ file: file, options: opts, stack: 'stack', config_index: 0 }, { file: another_file, options: opts, stack: 'stack', config_index: 1 }] }
320337

321338
it 'raises a DuplicateInstall error' do
322339
expect do
@@ -327,7 +344,7 @@ module Jobs::Runtime
327344

328345
context 'when the manifest has multiple buildpack entries for one name, neither specifying a stack' do
329346
let(:another_file) { 'the-other-file' }
330-
let(:buildpack_fields) { [{ file: file, options: opts, stack: nil }, { file: another_file, options: opts, stack: nil }] }
347+
let(:buildpack_fields) { [{ file: file, options: opts, stack: nil, config_index: 0 }, { file: another_file, options: opts, stack: nil, config_index: 1 }] }
331348

332349
it 'raises a DuplicateInstall error' do
333350
expect do
@@ -338,7 +355,7 @@ module Jobs::Runtime
338355

339356
context 'when the manifest has multiple buildpack entries for one name, one stackful and one stackless' do
340357
let(:another_file) { 'the-other-file' }
341-
let(:buildpack_fields) { [{ file: file, options: opts, stack: 'stack' }, { file: another_file, options: opts, stack: nil }] }
358+
let(:buildpack_fields) { [{ file: file, options: opts, stack: 'stack', config_index: 0 }, { file: another_file, options: opts, stack: nil, config_index: 1 }] }
342359

343360
it 'raises a StacklessBuildpackIncompatibilityError error' do
344361
expect do

spec/unit/jobs/runtime/create_buildpack_installer_spec.rb

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,44 @@ module Jobs::Runtime
7878
end
7979
end
8080

81+
context 'when config_index is provided' do
82+
let!(:existing_stack) { Stack.make(name: stack_name) }
83+
let!(:existing_buildpack1) { Buildpack.make(name: 'first_buildpack', stack: stack_name) }
84+
let!(:existing_buildpack2) { Buildpack.make(name: 'second_buildpack', stack: stack_name) }
85+
let!(:existing_buildpack3) { Buildpack.make(name: 'third_buildpack', stack: stack_name) }
86+
let!(:existing_buildpack4) { Buildpack.make(name: 'fourth_buildpack', stack: stack_name) }
87+
let!(:existing_buildpack5) { Buildpack.make(name: 'fifth_buildpack', stack: stack_name) }
88+
let(:job_options) { { name: 'mybuildpack', stack: stack_name, file: zipfile, options: { enabled: true, locked: false }, config_index: 5 } }
89+
90+
it 'moves the buildpack to position config_index + 1' do
91+
job.perform
92+
buildpack = Buildpack.find(name: 'mybuildpack')
93+
expect(buildpack.position).to eq(6)
94+
end
95+
end
96+
97+
context 'when config_index is provided but options includes an explicit position' do
98+
let!(:existing_stack) { Stack.make(name: stack_name) }
99+
let(:job_options) { { name: 'mybuildpack', stack: stack_name, file: zipfile, options: { enabled: true, locked: false, position: 7 }, config_index: 5 } }
100+
101+
it 'uses the explicit position from options instead of config_index' do
102+
job.perform
103+
buildpack = Buildpack.find(name: 'mybuildpack')
104+
expect(buildpack.position).to eq(7)
105+
end
106+
end
107+
108+
context 'when config_index is nil' do
109+
let!(:existing_stack) { Stack.make(name: stack_name) }
110+
let(:job_options) { { name: 'mybuildpack', stack: stack_name, file: zipfile, options: { enabled: true, locked: false } } }
111+
112+
it 'does not call move_to and uses default list position' do
113+
job.perform
114+
buildpack = Buildpack.find(name: 'mybuildpack')
115+
expect(buildpack.position).to eq(1)
116+
end
117+
end
118+
81119
context 'when uploading the buildpack fails' do
82120
let!(:existing_stack) { Stack.make(name: stack_name) }
83121

0 commit comments

Comments
 (0)