bosh_cli 1.2831.0 → 1.2839.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,316 +0,0 @@
1
- module Bosh::Cli
2
- class PackageBuilder
3
- include PackagingHelper
4
-
5
- class GlobMatch
6
- # Helper class encapsulating the data we know about the glob. We need
7
- # both directory and file path, as we match the same path in several
8
- # directories (src, src_alt, blobs)
9
- attr_reader :dir
10
- attr_reader :path
11
-
12
- def initialize(dir, path)
13
- @dir = dir
14
- @path = path
15
- end
16
-
17
- def full_path
18
- File.join(dir, path)
19
- end
20
-
21
- def <=>(other)
22
- @path <=> other.path
23
- end
24
-
25
- # GlobMatch will be used as Hash key (as implied by using Set),
26
- # hence we need to define both eql? and hash
27
- def eql?(other)
28
- @path == other.path
29
- end
30
-
31
- def hash
32
- @path.hash
33
- end
34
- end
35
-
36
- attr_reader :name, :globs, :version, :dependencies, :tarball_path
37
- # We have two ways of getting/storing a package:
38
- # development versions of packages, kept in release directory
39
- # final versions of packages, kept in blobstore
40
- # development packages and their metadata should always be gitignored
41
- # final build tarballs should be ignored as well
42
- # final builds metadata should be checked in
43
-
44
- # @param [String] directory Release directory
45
- # @param [Hash] options Package build options
46
- def self.discover(directory, options = {})
47
- builders = []
48
-
49
- Dir[File.join(directory, "packages", "*")].each do |package_dir|
50
- next unless File.directory?(package_dir)
51
- package_dirname = File.basename(package_dir)
52
- package_spec = load_yaml_file(File.join(package_dir, "spec"))
53
-
54
- if package_spec["name"] != package_dirname
55
- raise InvalidPackage,
56
- "Found `#{package_spec["name"]}' package in " +
57
- "`#{package_dirname}' directory, please fix it"
58
- end
59
-
60
- is_final = options[:final]
61
- blobstore = options[:blobstore]
62
- dry_run = options[:dry_run]
63
-
64
- builder = new(package_spec, directory, is_final, blobstore)
65
- builder.dry_run = true if dry_run
66
-
67
- builders << builder
68
- end
69
-
70
- builders
71
- end
72
-
73
- def initialize(spec, release_dir, final, blobstore,
74
- sources_dir = nil, blobs_dir = nil, alt_src_dir = nil)
75
- spec = load_yaml_file(spec) if spec.is_a?(String) && File.file?(spec)
76
-
77
- @name = spec["name"]
78
- @globs = spec["files"]
79
- @excluded_globs = spec["excluded_files"] || []
80
- @dependencies = Array(spec["dependencies"])
81
-
82
- @release_dir = release_dir
83
- @sources_dir = sources_dir || File.join(@release_dir, "src")
84
- @alt_sources_dir = alt_src_dir || File.join(@release_dir, "src_alt")
85
- @blobs_dir = blobs_dir || File.join(@release_dir, "blobs")
86
-
87
- @final = final
88
- @blobstore = blobstore
89
- @artefact_type = "package"
90
-
91
- @metadata_files = %w(packaging pre_packaging)
92
-
93
- if @final && File.exists?(@alt_sources_dir)
94
- err("Please remove `#{File.basename(@alt_sources_dir)}' first")
95
- end
96
-
97
- if @name.blank?
98
- raise InvalidPackage, "Package name is missing"
99
- end
100
-
101
- unless @name.bosh_valid_id?
102
- raise InvalidPackage, "Package name should be a valid BOSH identifier"
103
- end
104
-
105
- unless @globs.is_a?(Array) && @globs.size > 0
106
- raise InvalidPackage, "Package '#{@name}' doesn't include any files"
107
- end
108
-
109
- @dev_builds_dir = File.join(@release_dir, ".dev_builds",
110
- "packages", @name)
111
- @final_builds_dir = File.join(@release_dir, ".final_builds",
112
- "packages", @name)
113
-
114
- FileUtils.mkdir_p(package_dir)
115
- FileUtils.mkdir_p(@dev_builds_dir)
116
- FileUtils.mkdir_p(@final_builds_dir)
117
-
118
- init_indices
119
- end
120
-
121
- def reload # Mostly for tests
122
- @fingerprint = nil
123
- @resolved_globs = nil
124
- init_indices
125
- self
126
- end
127
-
128
- def fingerprint
129
- @fingerprint ||= make_fingerprint
130
- end
131
-
132
- def glob_matches
133
- @resolved_globs ||= resolve_globs
134
- end
135
-
136
- def build_dir
137
- @build_dir ||= Dir.mktmpdir
138
- end
139
-
140
- def package_dir
141
- File.join(@release_dir, "packages", name)
142
- end
143
-
144
- def copy_files
145
- copied = 0
146
-
147
- glob_matches.each do |match|
148
- destination = File.join(build_dir, match.path)
149
-
150
- if File.directory?(match.full_path)
151
- FileUtils.mkdir_p(destination)
152
- else
153
- FileUtils.mkdir_p(File.dirname(destination))
154
- FileUtils.cp(match.full_path, destination, :preserve => true)
155
- copied += 1
156
- end
157
- end
158
-
159
- in_package_dir do
160
- @metadata_files.each do |filename|
161
- destination = File.join(build_dir, filename)
162
- next unless File.exists?(filename)
163
- if File.exists?(destination)
164
- raise InvalidPackage, "Package '#{name}' has '#{filename}' file " +
165
- "which conflicts with BOSH packaging"
166
- end
167
- FileUtils.cp(filename, destination, :preserve => true)
168
- copied += 1
169
- end
170
- end
171
-
172
- pre_package
173
- copied
174
- end
175
-
176
- def pre_package
177
- pre_packaging_script = File.join(package_dir, "pre_packaging")
178
-
179
- if File.exists?(pre_packaging_script)
180
-
181
- say("Pre-packaging...")
182
- FileUtils.cp(pre_packaging_script, build_dir, :preserve => true)
183
-
184
- old_env = ENV
185
-
186
- begin
187
- ENV.delete_if { |key, _| key[0, 7] == "BUNDLE_" }
188
- if ENV["RUBYOPT"]
189
- ENV["RUBYOPT"] = ENV["RUBYOPT"].sub("-rbundler/setup", "")
190
- end
191
- ENV["BUILD_DIR"] = build_dir
192
- ENV["RELEASE_DIR"] = @release_dir
193
- in_build_dir do
194
- pre_packaging_out = `bash -x pre_packaging 2>&1`
195
- unless $?.exitstatus == 0
196
- pre_packaging_out.split("\n").each do |line|
197
- say("> #{line}")
198
- end
199
- raise InvalidPackage, "`#{name}' pre-packaging failed"
200
- end
201
- end
202
-
203
- ensure
204
- ENV.delete("BUILD_DIR")
205
- old_env.each { |k, v| ENV[k] = old_env[k] }
206
- end
207
-
208
- FileUtils.rm(File.join(build_dir, "pre_packaging"))
209
- end
210
- end
211
-
212
- private
213
-
214
- def make_fingerprint
215
- versioning_scheme = 2
216
- contents = "v#{versioning_scheme}"
217
-
218
- signatures = glob_matches.map do |match|
219
- file_digest = nil
220
-
221
- unless File.directory?(match.full_path)
222
- file_digest = Digest::SHA1.file(match.full_path).hexdigest
223
- end
224
-
225
- "%s%s%s" % [match.path, file_digest,
226
- tracked_permissions(match.full_path)]
227
- end
228
- contents << signatures.join("")
229
-
230
- in_package_dir do
231
- @metadata_files.each do |file|
232
- if File.file?(file)
233
- file_digest = Digest::SHA1.file(file).hexdigest
234
- contents << "%s%s" % [file, file_digest]
235
- end
236
- end
237
- end
238
-
239
- contents << @dependencies.sort.join(",")
240
-
241
- Digest::SHA1.hexdigest(contents)
242
- end
243
-
244
- # @return Array<GlobMatch>
245
- def resolve_globs
246
- all_matches = Set.new
247
-
248
- @globs.each do |glob|
249
- matches = Set.new
250
-
251
- src_matches = resolve_glob_in_dir(glob, @sources_dir)
252
- src_alt_matches = []
253
- if File.directory?(@alt_sources_dir)
254
- src_alt_matches = resolve_glob_in_dir(glob, @alt_sources_dir)
255
- end
256
-
257
- # Glob like core/dea/**/* might not yield anything in alt source even
258
- # when `src_alt/core' exists. That's error prone, so we don't lookup
259
- # in `src' if `src_alt' contains any part of the glob hierarchy.
260
- top_dir = glob.split(File::SEPARATOR)[0]
261
- top_dir_in_src_alt_exists = top_dir && File.exists?(File.join(@alt_sources_dir, top_dir))
262
-
263
- if top_dir_in_src_alt_exists && src_alt_matches.empty? && src_matches.any?
264
- raise InvalidPackage, "Package `#{name}' has a glob that " +
265
- "doesn't match in `#{File.basename(@alt_sources_dir)}' " +
266
- "but matches in `#{File.basename(@sources_dir)}'. " +
267
- "However `#{File.basename(@alt_sources_dir)}/#{top_dir}' " +
268
- "exists, so this might be an error."
269
- end
270
-
271
- # First add src_alt matches since src_alt takes priority over src matches
272
- matches += src_alt_matches.map { |path| GlobMatch.new(@alt_sources_dir, path) }
273
-
274
- # Only add if top-level-dir does not exist in src_alt. No partial matches.
275
- if !top_dir_in_src_alt_exists
276
- matches += src_matches.map { |path| GlobMatch.new(@sources_dir, path) }
277
- end
278
-
279
- # Blobs directory is a little bit different: whatever matches a blob
280
- # will complement already found matches, unless this particular path
281
- # has already been matched.
282
- if File.directory?(File.join(@blobs_dir))
283
- resolve_glob_in_dir(glob, @blobs_dir).each { |path| matches << GlobMatch.new(@blobs_dir, path) }
284
- end
285
-
286
- if matches.empty?
287
- raise InvalidPackage, "Package `#{name}' has a glob that resolves to an empty file list: #{glob}"
288
- end
289
-
290
- all_matches += matches
291
- end
292
-
293
- all_matches.reject! do |match|
294
- @excluded_globs.detect { |excluded_glob| File.fnmatch(excluded_glob, match.path) }
295
- end
296
- all_matches.sort
297
- end
298
-
299
- def resolve_glob_in_dir(glob, dir)
300
- Dir.chdir(dir) do
301
- Dir.glob(glob, File::FNM_DOTMATCH).reject do |fn|
302
- %w(. ..).include?(File.basename(fn))
303
- end
304
- end
305
- end
306
-
307
- def in_build_dir(&block)
308
- Dir.chdir(build_dir) { yield }
309
- end
310
-
311
- def in_package_dir(&block)
312
- Dir.chdir(package_dir) { yield }
313
- end
314
-
315
- end
316
- end
@@ -1,217 +0,0 @@
1
- # This relies on having the following instance variables in a host class:
2
- # @dev_builds_dir, @final_builds_dir, @blobstore,
3
- # @name, @version, @tarball_path, @final, @artefact_type
4
-
5
- module Bosh::Cli
6
- module PackagingHelper
7
- attr_accessor :dry_run
8
-
9
- def init_indices
10
- @dev_index = Versions::VersionsIndex.new(@dev_builds_dir)
11
- @dev_storage = Versions::LocalVersionStorage.new(@dev_builds_dir)
12
-
13
- @final_index = Versions::VersionsIndex.new(@final_builds_dir)
14
- @final_storage = Versions::LocalVersionStorage.new(@final_builds_dir)
15
-
16
- @final_resolver = Versions::VersionFileResolver.new(@final_storage, @blobstore)
17
- end
18
-
19
- def final?
20
- @final
21
- end
22
-
23
- def dry_run?
24
- @dry_run
25
- end
26
-
27
- def new_version?
28
- @tarball_generated || @promoted || @will_be_promoted
29
- end
30
-
31
- def notes
32
- notes = []
33
-
34
- if @will_be_promoted
35
- new_final_version = @version
36
- notes << "new final version #{new_final_version}"
37
- elsif new_version?
38
- notes << 'new version'
39
- end
40
-
41
- notes
42
- end
43
-
44
- def build
45
- with_indent(' ') do
46
- use_final_version || use_dev_version || generate_tarball
47
- end
48
- upload_tarball(@tarball_path) if final? && !dry_run?
49
- @will_be_promoted = true if final? && dry_run? && @used_dev_version
50
- end
51
-
52
- def use_final_version
53
- say('Final version:', ' ')
54
-
55
- item = @final_index[fingerprint]
56
-
57
- if item.nil?
58
- say('NOT FOUND'.make_red)
59
- return nil
60
- end
61
-
62
- blobstore_id = item['blobstore_id']
63
- version = item['version'] || fingerprint
64
- sha1 = item['sha1']
65
-
66
- if blobstore_id.nil?
67
- say('No blobstore id'.make_red)
68
- return nil
69
- end
70
-
71
- desc = "#{name} (#{version})"
72
-
73
- @tarball_path = @final_resolver.find_file(blobstore_id, sha1, version, "package #{desc}")
74
-
75
- @version = version
76
- @used_final_version = true
77
- true
78
- rescue Bosh::Blobstore::NotFound
79
- raise BlobstoreError, "Final version of `#{name}' not found in blobstore"
80
- rescue Bosh::Blobstore::BlobstoreError => e
81
- raise BlobstoreError, "Blobstore error: #{e}"
82
- end
83
-
84
- def use_dev_version
85
- say('Dev version:', ' ')
86
- item = @dev_index[fingerprint]
87
-
88
- if item.nil?
89
- say('NOT FOUND'.make_red)
90
- return nil
91
- end
92
-
93
- version = @dev_index['version'] || fingerprint
94
-
95
- if !@dev_storage.has_file?(version)
96
- say('TARBALL MISSING'.make_red)
97
- return nil
98
- end
99
-
100
- say('FOUND LOCAL'.make_green)
101
- @tarball_path = @dev_storage.get_file(version)
102
-
103
- if file_checksum(@tarball_path) != item['sha1']
104
- say("`#{name} (#{version})' tarball corrupted".make_red)
105
- return nil
106
- end
107
-
108
- if final? && !dry_run?
109
- # copy from dev index/storage to final index/storage
110
- @final_index.add_version(fingerprint, item)
111
- @tarball_path = @final_storage.put_file(version, @tarball_path)
112
- item['sha1'] = Digest::SHA1.file(@tarball_path).hexdigest
113
- @final_index.update_version(fingerprint, item)
114
- end
115
-
116
- @version = version
117
- @used_dev_version = true
118
- end
119
-
120
- def generate_tarball
121
- version = fingerprint
122
- tmp_file = Tempfile.new(name)
123
-
124
- say('Generating...')
125
-
126
- copy_files
127
-
128
- in_build_dir do
129
- tar_out = `tar -chzf #{tmp_file.path} . 2>&1`
130
- unless $?.exitstatus == 0
131
- raise PackagingError, "Cannot create tarball: #{tar_out}"
132
- end
133
- end
134
-
135
- item = {
136
- 'version' => version
137
- }
138
-
139
- if final?
140
- # add version (with its validation) before adding sha1
141
- @final_index.add_version(fingerprint, item)
142
- @tarball_path = @final_storage.put_file(fingerprint, tmp_file.path)
143
- item['sha1'] = file_checksum(@tarball_path)
144
- @final_index.update_version(fingerprint, item)
145
- elsif dry_run?
146
- else
147
- # add version (with its validation) before adding sha1
148
- @dev_index.add_version(fingerprint, item)
149
- @tarball_path = @dev_storage.put_file(fingerprint, tmp_file.path)
150
- item['sha1'] = file_checksum(@tarball_path)
151
- @dev_index.update_version(fingerprint, item)
152
- end
153
-
154
- @version = version
155
- @tarball_generated = true
156
- say("Generated version #{version}".make_green)
157
- true
158
- end
159
-
160
- def upload_tarball(path)
161
- item = @final_index[fingerprint]
162
-
163
- unless item
164
- say("Failed to find entry `#{fingerprint}' in index, check local storage")
165
- return
166
- end
167
-
168
- if item['blobstore_id']
169
- return
170
- end
171
-
172
- say("Uploading final version `#{version}'...")
173
-
174
- blobstore_id = nil
175
- File.open(path, 'r') do |f|
176
- blobstore_id = @blobstore.create(f)
177
- end
178
-
179
- say("Uploaded, blobstore id `#{blobstore_id}'")
180
- item['blobstore_id'] = blobstore_id
181
- @final_index.update_version(fingerprint, item)
182
- @promoted = true
183
- true
184
- rescue Bosh::Blobstore::BlobstoreError => e
185
- raise BlobstoreError, "Blobstore error: #{e}"
186
- end
187
-
188
- def file_checksum(path)
189
- Digest::SHA1.file(path).hexdigest
190
- end
191
-
192
- def checksum
193
- if @tarball_path && File.exists?(@tarball_path)
194
- file_checksum(@tarball_path)
195
- else
196
- raise RuntimeError,
197
- 'cannot read checksum for not yet generated package/job'
198
- end
199
- end
200
-
201
- # Git doesn't really track file permissions, it just looks at executable
202
- # bit and uses 0755 if it's set or 0644 if not. We have to mimic that
203
- # behavior in the fingerprint calculation to avoid the situation where
204
- # seemingly clean working copy would trigger new fingerprints for
205
- # artifacts with changed permissions. Also we don't want current
206
- # fingerprints to change, hence the exact values below.
207
- def tracked_permissions(path)
208
- if File.directory?(path)
209
- '40755'
210
- elsif File.executable?(path)
211
- '100755'
212
- else
213
- '100644'
214
- end
215
- end
216
- end
217
- end