bosh_cli 0.16

Sign up to get free protection for your applications and to get access to all the features.
Files changed (113) hide show
  1. data/README +4 -0
  2. data/Rakefile +55 -0
  3. data/bin/bosh +17 -0
  4. data/lib/cli.rb +76 -0
  5. data/lib/cli/cache.rb +44 -0
  6. data/lib/cli/changeset_helper.rb +142 -0
  7. data/lib/cli/command_definition.rb +52 -0
  8. data/lib/cli/commands/base.rb +245 -0
  9. data/lib/cli/commands/biff.rb +300 -0
  10. data/lib/cli/commands/blob.rb +125 -0
  11. data/lib/cli/commands/cloudcheck.rb +169 -0
  12. data/lib/cli/commands/deployment.rb +147 -0
  13. data/lib/cli/commands/job.rb +42 -0
  14. data/lib/cli/commands/job_management.rb +117 -0
  15. data/lib/cli/commands/log_management.rb +81 -0
  16. data/lib/cli/commands/maintenance.rb +131 -0
  17. data/lib/cli/commands/misc.rb +240 -0
  18. data/lib/cli/commands/package.rb +112 -0
  19. data/lib/cli/commands/property_management.rb +125 -0
  20. data/lib/cli/commands/release.rb +469 -0
  21. data/lib/cli/commands/ssh.rb +271 -0
  22. data/lib/cli/commands/stemcell.rb +184 -0
  23. data/lib/cli/commands/task.rb +213 -0
  24. data/lib/cli/commands/user.rb +28 -0
  25. data/lib/cli/commands/vms.rb +53 -0
  26. data/lib/cli/config.rb +154 -0
  27. data/lib/cli/core_ext.rb +145 -0
  28. data/lib/cli/dependency_helper.rb +62 -0
  29. data/lib/cli/deployment_helper.rb +263 -0
  30. data/lib/cli/deployment_manifest_compiler.rb +28 -0
  31. data/lib/cli/director.rb +633 -0
  32. data/lib/cli/director_task.rb +64 -0
  33. data/lib/cli/errors.rb +48 -0
  34. data/lib/cli/event_log_renderer.rb +351 -0
  35. data/lib/cli/job_builder.rb +226 -0
  36. data/lib/cli/package_builder.rb +254 -0
  37. data/lib/cli/packaging_helper.rb +248 -0
  38. data/lib/cli/release.rb +176 -0
  39. data/lib/cli/release_builder.rb +215 -0
  40. data/lib/cli/release_compiler.rb +178 -0
  41. data/lib/cli/release_tarball.rb +272 -0
  42. data/lib/cli/runner.rb +771 -0
  43. data/lib/cli/stemcell.rb +83 -0
  44. data/lib/cli/task_log_renderer.rb +40 -0
  45. data/lib/cli/templates/help_message.erb +75 -0
  46. data/lib/cli/validation.rb +42 -0
  47. data/lib/cli/version.rb +7 -0
  48. data/lib/cli/version_calc.rb +48 -0
  49. data/lib/cli/versions_index.rb +126 -0
  50. data/lib/cli/yaml_helper.rb +62 -0
  51. data/spec/assets/biff/bad_gateway_config.yml +28 -0
  52. data/spec/assets/biff/good_simple_config.yml +63 -0
  53. data/spec/assets/biff/good_simple_golden_config.yml +63 -0
  54. data/spec/assets/biff/good_simple_template.erb +69 -0
  55. data/spec/assets/biff/multiple_subnets_config.yml +40 -0
  56. data/spec/assets/biff/network_only_template.erb +34 -0
  57. data/spec/assets/biff/no_cc_config.yml +27 -0
  58. data/spec/assets/biff/no_range_config.yml +27 -0
  59. data/spec/assets/biff/no_subnet_config.yml +16 -0
  60. data/spec/assets/biff/ok_network_config.yml +30 -0
  61. data/spec/assets/biff/properties_template.erb +6 -0
  62. data/spec/assets/deployment.MF +0 -0
  63. data/spec/assets/plugins/bosh/cli/commands/echo.rb +43 -0
  64. data/spec/assets/plugins/bosh/cli/commands/ruby.rb +24 -0
  65. data/spec/assets/release/jobs/cacher.tgz +0 -0
  66. data/spec/assets/release/jobs/cacher/config/file1.conf +0 -0
  67. data/spec/assets/release/jobs/cacher/config/file2.conf +0 -0
  68. data/spec/assets/release/jobs/cacher/job.MF +6 -0
  69. data/spec/assets/release/jobs/cacher/monit +1 -0
  70. data/spec/assets/release/jobs/cleaner.tgz +0 -0
  71. data/spec/assets/release/jobs/cleaner/job.MF +4 -0
  72. data/spec/assets/release/jobs/cleaner/monit +1 -0
  73. data/spec/assets/release/jobs/sweeper.tgz +0 -0
  74. data/spec/assets/release/jobs/sweeper/config/test.conf +1 -0
  75. data/spec/assets/release/jobs/sweeper/job.MF +5 -0
  76. data/spec/assets/release/jobs/sweeper/monit +1 -0
  77. data/spec/assets/release/packages/mutator.tar.gz +0 -0
  78. data/spec/assets/release/packages/stuff.tgz +0 -0
  79. data/spec/assets/release/release.MF +17 -0
  80. data/spec/assets/release_invalid_checksum.tgz +0 -0
  81. data/spec/assets/release_invalid_jobs.tgz +0 -0
  82. data/spec/assets/release_no_name.tgz +0 -0
  83. data/spec/assets/release_no_version.tgz +0 -0
  84. data/spec/assets/stemcell/image +1 -0
  85. data/spec/assets/stemcell/stemcell.MF +6 -0
  86. data/spec/assets/stemcell_invalid_mf.tgz +0 -0
  87. data/spec/assets/stemcell_no_image.tgz +0 -0
  88. data/spec/assets/valid_release.tgz +0 -0
  89. data/spec/assets/valid_stemcell.tgz +0 -0
  90. data/spec/spec_helper.rb +25 -0
  91. data/spec/unit/base_command_spec.rb +66 -0
  92. data/spec/unit/biff_spec.rb +135 -0
  93. data/spec/unit/cache_spec.rb +36 -0
  94. data/spec/unit/cli_commands_spec.rb +481 -0
  95. data/spec/unit/config_spec.rb +139 -0
  96. data/spec/unit/core_ext_spec.rb +77 -0
  97. data/spec/unit/dependency_helper_spec.rb +52 -0
  98. data/spec/unit/deployment_manifest_compiler_spec.rb +63 -0
  99. data/spec/unit/director_spec.rb +511 -0
  100. data/spec/unit/director_task_spec.rb +48 -0
  101. data/spec/unit/event_log_renderer_spec.rb +171 -0
  102. data/spec/unit/hash_changeset_spec.rb +73 -0
  103. data/spec/unit/job_builder_spec.rb +454 -0
  104. data/spec/unit/package_builder_spec.rb +567 -0
  105. data/spec/unit/release_builder_spec.rb +65 -0
  106. data/spec/unit/release_spec.rb +66 -0
  107. data/spec/unit/release_tarball_spec.rb +33 -0
  108. data/spec/unit/runner_spec.rb +140 -0
  109. data/spec/unit/ssh_spec.rb +78 -0
  110. data/spec/unit/stemcell_spec.rb +17 -0
  111. data/spec/unit/version_calc_spec.rb +27 -0
  112. data/spec/unit/versions_index_spec.rb +132 -0
  113. metadata +338 -0
@@ -0,0 +1,248 @@
1
+ # Copyright (c) 2009-2012 VMware, Inc.
2
+
3
+ # This relies on having the following instance variables in a host class:
4
+ # @dev_builds_dir, @final_builds_dir, @blobstore,
5
+ # @name, @version, @tarball_path, @final, @artefact_type
6
+
7
+ module Bosh::Cli
8
+ module PackagingHelper
9
+ include Bosh::Cli::VersionCalc
10
+
11
+ attr_accessor :dry_run
12
+
13
+ def init_indices
14
+ @dev_index = VersionsIndex.new(@dev_builds_dir)
15
+ @final_index = VersionsIndex.new(@final_builds_dir)
16
+ end
17
+
18
+ def final?
19
+ @final
20
+ end
21
+
22
+ def dry_run?
23
+ @dry_run
24
+ end
25
+
26
+ def new_version?
27
+ @tarball_generated || @promoted || @will_be_promoted
28
+ end
29
+
30
+ def older_version?
31
+ if @tarball_generated
32
+ false
33
+ elsif @used_final_version
34
+ version_cmp(@version, @final_index.latest_version) < 0
35
+ else
36
+ version_cmp(@version, @dev_index.latest_version) < 0
37
+ end
38
+ end
39
+
40
+ def notes
41
+ notes = []
42
+
43
+ if @will_be_promoted
44
+ new_final_version = @final_index.latest_version.to_i + 1
45
+ notes << "new final version #{new_final_version}"
46
+ elsif new_version?
47
+ notes << "new version"
48
+ end
49
+
50
+ notes << "older than latest" if older_version?
51
+ notes
52
+ end
53
+
54
+ def build
55
+ with_indent(" ") do
56
+ use_final_version || use_dev_version || generate_tarball
57
+ end
58
+ upload_tarball(@tarball_path) if final? && !dry_run?
59
+ @will_be_promoted = true if final? && dry_run? && @used_dev_version
60
+ end
61
+
62
+ def use_final_version
63
+ say("Final version:", " ")
64
+
65
+ item = @final_index[fingerprint]
66
+
67
+ if item.nil?
68
+ say("NOT FOUND".red)
69
+ return nil
70
+ end
71
+
72
+ blobstore_id = item["blobstore_id"]
73
+ version = item["version"]
74
+
75
+ if blobstore_id.nil?
76
+ say("No blobstore id".red)
77
+ return nil
78
+ end
79
+
80
+ filename = @final_index.filename(version)
81
+ need_fetch = true
82
+
83
+ if File.exists?(filename)
84
+ say("FOUND LOCAL".green)
85
+ if file_checksum(filename) == item["sha1"]
86
+ @tarball_path = filename
87
+ need_fetch = false
88
+ else
89
+ say("LOCAL CHECKSUM MISMATCH".red)
90
+ need_fetch = true
91
+ end
92
+ end
93
+
94
+ if need_fetch
95
+ say("Downloading `#{name} (#{version})' (#{blobstore_id})".green)
96
+ payload = @blobstore.get(blobstore_id)
97
+ if Digest::SHA1.hexdigest(payload) == item["sha1"]
98
+ @tarball_path = @final_index.add_version(fingerprint, item, payload)
99
+ else
100
+ err("`#{name}' (#{version}) is corrupted in blobstore " +
101
+ "(id=#{blobstore_id}), " +
102
+ "please remove it manually and re-generate the final release")
103
+ end
104
+ end
105
+
106
+ @version = version
107
+ @used_final_version = true
108
+ true
109
+ rescue Bosh::Blobstore::NotFound => e
110
+ raise BlobstoreError, "Final version of `#{name}' not found in blobstore"
111
+ rescue Bosh::Blobstore::BlobstoreError => e
112
+ raise BlobstoreError, "Blobstore error: #{e}"
113
+ end
114
+
115
+ def use_dev_version
116
+ say("Dev version:", " ")
117
+ item = @dev_index[fingerprint]
118
+
119
+ if item.nil?
120
+ say("NOT FOUND".red)
121
+ return nil
122
+ end
123
+
124
+ version = item["version"]
125
+ filename = @dev_index.filename(version)
126
+
127
+ if File.exists?(filename)
128
+ say("FOUND LOCAL".green)
129
+ else
130
+ say("TARBALL MISSING".red)
131
+ return nil
132
+ end
133
+
134
+ if file_checksum(filename) == item["sha1"]
135
+ @tarball_path = filename
136
+ @version = version
137
+ @used_dev_version = true
138
+ else
139
+ say("`#{name} (#{version})' tarball corrupted".red)
140
+ return nil
141
+ end
142
+ end
143
+
144
+ def generate_tarball
145
+ if final?
146
+ err_message = "No matching build found for " +
147
+ "`#{@name}' #{@artefact_type}.\n" +
148
+ "Please consider creating a dev release first.\n" +
149
+ "The fingerprint is `#{fingerprint}'."
150
+ err(err_message)
151
+ end
152
+
153
+ current_final = @final_index.latest_version.to_i
154
+ new_minor = minor_version(@dev_index.latest_version(current_final)) + 1
155
+
156
+ version = "#{current_final}.#{new_minor}-dev"
157
+ tmp_file = Tempfile.new(name)
158
+
159
+ say("Generating...")
160
+
161
+ copy_files
162
+
163
+ in_build_dir do
164
+ tar_out = `tar -chzf #{tmp_file.path} . 2>&1`
165
+ unless $?.exitstatus == 0
166
+ raise PackagingError, "Cannot create tarball: #{tar_out}"
167
+ end
168
+ end
169
+
170
+ payload = tmp_file.read
171
+
172
+ item = {
173
+ "version" => version
174
+ }
175
+
176
+ unless dry_run?
177
+ @dev_index.add_version(fingerprint, item, payload)
178
+ @tarball_path = @dev_index.filename(version)
179
+ end
180
+
181
+ @version = version
182
+ @tarball_generated = true
183
+ say("Generated version #{version}".green)
184
+ true
185
+ end
186
+
187
+ def upload_tarball(path)
188
+ item = @final_index[fingerprint]
189
+
190
+ say("Uploading final version #{version}...")
191
+
192
+ if !item.nil?
193
+ version = item["version"]
194
+ say("This package has already been uploaded")
195
+ return
196
+ end
197
+
198
+ version = @final_index.latest_version.to_i + 1
199
+ payload = File.read(path)
200
+
201
+ blobstore_id = @blobstore.create(payload)
202
+
203
+ item = {
204
+ "blobstore_id" => blobstore_id,
205
+ "version" => version
206
+ }
207
+
208
+ say("Uploaded, blobstore id #{blobstore_id}")
209
+ @final_index.add_version(fingerprint, item, payload)
210
+ @tarball_path = @final_index.filename(version)
211
+ @version = version
212
+ @promoted = true
213
+ true
214
+ rescue Bosh::Blobstore::BlobstoreError => e
215
+ raise BlobstoreError, "Blobstore error: #{e}"
216
+ end
217
+
218
+ def file_checksum(path)
219
+ Digest::SHA1.file(path).hexdigest
220
+ end
221
+
222
+ def checksum
223
+ if @tarball_path && File.exists?(@tarball_path)
224
+ file_checksum(@tarball_path)
225
+ else
226
+ raise RuntimeError,
227
+ "cannot read checksum for not yet generated package/job"
228
+ end
229
+ end
230
+
231
+ # Git doesn't really track file permissions, it just looks at executable
232
+ # bit and uses 0755 if it's set or 0644 if not. We have to mimic that
233
+ # behavior in the fingerprint calculation to avoid the situation where
234
+ # seemingly clean working copy would trigger new fingerprints for
235
+ # artifacts with changed permissions. Also we don't want current
236
+ # fingerprints to change, hence the exact values below.
237
+ def tracked_permissions(path)
238
+ if File.directory?(path)
239
+ "40755"
240
+ elsif File.executable?(path)
241
+ "100755"
242
+ else
243
+ "100644"
244
+ end
245
+ end
246
+ end
247
+ end
248
+
@@ -0,0 +1,176 @@
1
+ # Copyright (c) 2009-2012 VMware, Inc.
2
+
3
+ module Bosh::Cli
4
+ # This class encapsulates the details of handling dev and final releases:
5
+ # also it partitions release metadata between final config (which is
6
+ # under version control) and user dev config.
7
+ class Release
8
+ attr_reader :dir
9
+
10
+ def initialize(dir)
11
+ @dir = dir
12
+ config_dir = File.join(dir, "config")
13
+ @final_config_file = File.join(config_dir, "final.yml")
14
+ @dev_config_file = File.join(config_dir, "dev.yml")
15
+
16
+ @private_config_file = File.join(config_dir, "private.yml")
17
+
18
+ unless File.directory?(dir)
19
+ err("Cannot find release directory `#{dir}'")
20
+ end
21
+
22
+ unless File.directory?(config_dir)
23
+ err("Cannot find release config directory `#{config_dir}'")
24
+ end
25
+
26
+ @final_config = load_config(@final_config_file)
27
+ @dev_config = load_config(@dev_config_file)
28
+ @private_config = load_config(@private_config_file)
29
+
30
+ migrate_legacy_configs
31
+ end
32
+
33
+ # Devbox-specific attributes, gitignored
34
+ [:dev_name, :latest_release_filename].each do |attr|
35
+ define_method(attr) do
36
+ @dev_config[attr.to_s]
37
+ end
38
+
39
+ define_method("#{attr}=".to_sym) do |value|
40
+ @dev_config[attr.to_s] = value
41
+ end
42
+ end
43
+
44
+ # Shared attributes, present in repo
45
+ [:final_name, :min_cli_version].each do |attr|
46
+ define_method(attr) do
47
+ @final_config[attr.to_s]
48
+ end
49
+
50
+ define_method("#{attr}=".to_sym) do |value|
51
+ @final_config[attr.to_s] = value
52
+ end
53
+ end
54
+
55
+ # Check if the blobstore secret is provided in the private config file
56
+ #
57
+ # @return [Boolean]
58
+ def has_blobstore_secret?
59
+ @private_config.has_key?("blobstore_secret")
60
+ end
61
+
62
+ # Picks blobstore client to use with current release.
63
+ #
64
+ # @return [Bosh::Blobstore::Client] blobstore client
65
+ def blobstore
66
+ return @blobstore if @blobstore
67
+ blobstore_config = Marshal.load(Marshal.dump(@final_config["blobstore"]))
68
+
69
+ if blobstore_config.nil?
70
+ err("Missing blobstore configuration, please update your release")
71
+ end
72
+
73
+ provider = blobstore_config["provider"]
74
+ options = blobstore_config["options"] || {}
75
+
76
+ if has_blobstore_secret?
77
+ options["secret"] = @private_config["blobstore_secret"]
78
+ end
79
+
80
+ @blobstore = Bosh::Blobstore::Client.create(provider,
81
+ symbolize_keys(options))
82
+
83
+ rescue Bosh::Blobstore::BlobstoreError => e
84
+ err("Cannot initialize blobstore: #{e}")
85
+ end
86
+
87
+ def save_config
88
+ # TODO: introduce write_yaml helper
89
+ File.open(@dev_config_file, "w") do |f|
90
+ YAML.dump(@dev_config, f)
91
+ end
92
+
93
+ File.open(@final_config_file, "w") do |f|
94
+ YAML.dump(@final_config, f)
95
+ end
96
+ end
97
+
98
+ private
99
+
100
+ # Upgrade path for legacy clients that kept release metadata
101
+ # in config/dev.yml and config/final.yml
102
+ #
103
+ def migrate_legacy_configs
104
+ # We're using blobstore_options as old config marker.
105
+ # Unfortunately old CLI won't tell you to upgrade because it checks
106
+ # for valid blobstore options first, so instead of removing
107
+ # blobstore_options we mark it as deprecated, so new CLI proceeds
108
+ # to migrate while the old one tells you to upgrade.
109
+ if @dev_config.has_key?("blobstore_options") &&
110
+ @dev_config["blobstore_options"] != "deprecated"
111
+ say("Found legacy dev config file `#{@dev_config_file}'".yellow)
112
+
113
+ new_dev_config = {
114
+ "dev_name" => @dev_config["name"],
115
+ "latest_release_filename" =>
116
+ @dev_config["latest_release_filename"],
117
+
118
+ # Following two options are only needed for older clients
119
+ # to fail gracefully and never actually read by a new client
120
+ "blobstore_options" => "deprecated",
121
+ "min_cli_version" => "0.12"
122
+ }
123
+
124
+ @dev_config = new_dev_config
125
+
126
+ File.open(@dev_config_file, "w") do |f|
127
+ YAML.dump(@dev_config, f)
128
+ end
129
+ say("Migrated dev config file format".green)
130
+ end
131
+
132
+ if @final_config.has_key?("blobstore_options") &&
133
+ @final_config["blobstore_options"] != "deprecated"
134
+ say("Found legacy config file `#{@final_config_file}'".yellow)
135
+
136
+ unless @final_config["blobstore_options"]["provider"] == "atmos" &&
137
+ @final_config["blobstore_options"].has_key?("atmos_options")
138
+ err("Please update your release to the version " +
139
+ "that uses Atmos blobstore")
140
+ end
141
+
142
+ new_final_config = {
143
+ "final_name" => @final_config["name"],
144
+ "min_cli_version" => @final_config["min_cli_version"],
145
+ "blobstore" => {
146
+ "provider" => "atmos",
147
+ "options" => @final_config["blobstore_options"]["atmos_options"]
148
+ },
149
+ "blobstore_options" => "deprecated"
150
+ }
151
+
152
+ @final_config = new_final_config
153
+
154
+ File.open(@final_config_file, "w") { |f| YAML.dump(@final_config, f) }
155
+ say("Migrated final config file format".green)
156
+ end
157
+ end
158
+
159
+ def symbolize_keys(hash)
160
+ hash.inject({}) do |h, (key, value)|
161
+ h[key.to_sym] = value
162
+ h
163
+ end
164
+ end
165
+
166
+ def load_config(file)
167
+ if File.exists?(file)
168
+ load_yaml_file(file)
169
+ else
170
+ {}
171
+ end
172
+ end
173
+
174
+ end
175
+
176
+ end
@@ -0,0 +1,215 @@
1
+ # Copyright (c) 2009-2012 VMware, Inc.
2
+
3
+ module Bosh::Cli
4
+ class ReleaseBuilder
5
+ include Bosh::Cli::DependencyHelper
6
+
7
+ DEFAULT_RELEASE_NAME = "bosh_release"
8
+
9
+ attr_reader :release, :packages, :jobs, :changed_jobs
10
+
11
+ def initialize(release, packages, jobs, options = { })
12
+ @release = release
13
+ @final = options.has_key?(:final) ? !!options[:final] : false
14
+ @packages = packages
15
+ @jobs = jobs
16
+
17
+ @index = VersionsIndex.new(releases_dir, release_name)
18
+ create_release_build_dir
19
+ end
20
+
21
+ def release_name
22
+ name = @final ? @release.final_name : @release.dev_name
23
+ name.blank? ? DEFAULT_RELEASE_NAME : name
24
+ end
25
+
26
+ def version
27
+ @version ||= assign_version
28
+ end
29
+
30
+ def final?
31
+ @final
32
+ end
33
+
34
+ def affected_jobs
35
+ result = Set.new(@jobs.select { |job| job.new_version? })
36
+ return result if @packages.empty?
37
+
38
+ new_package_names = @packages.inject([]) do |list, package|
39
+ list << package.name if package.new_version?
40
+ list
41
+ end
42
+
43
+ @jobs.each do |job|
44
+ result << job if (new_package_names & job.packages).size > 0
45
+ end
46
+
47
+ result.to_a
48
+ end
49
+
50
+ def build(options = {})
51
+ options = { :generate_tarball => true }.merge(options)
52
+
53
+ header("Generating manifest...")
54
+ generate_manifest
55
+ if options[:generate_tarball]
56
+ header("Generating tarball...")
57
+ generate_tarball
58
+ end
59
+ @build_complete = true
60
+ end
61
+
62
+ def copy_packages
63
+ packages.each do |package|
64
+ say("%-40s %s" % [package.name.green,
65
+ pretty_size(package.tarball_path)])
66
+ FileUtils.cp(package.tarball_path,
67
+ File.join(build_dir, "packages", "#{package.name}.tgz"),
68
+ :preserve => true)
69
+ end
70
+ @packages_copied = true
71
+ end
72
+
73
+ def copy_jobs
74
+ jobs.each do |job|
75
+ say("%-40s %s" % [job.name.green, pretty_size(job.tarball_path)])
76
+ FileUtils.cp(job.tarball_path,
77
+ File.join(build_dir, "jobs", "#{job.name}.tgz"),
78
+ :preserve => true)
79
+ end
80
+ @jobs_copied = true
81
+ end
82
+
83
+ def generate_manifest
84
+ manifest = {}
85
+ manifest["packages"] = []
86
+
87
+ manifest["packages"] = packages.map do |package|
88
+ {
89
+ "name" => package.name,
90
+ "version" => package.version,
91
+ "sha1" => package.checksum,
92
+ "dependencies" => package.dependencies
93
+ }
94
+ end
95
+
96
+ manifest["jobs"] = jobs.map do |job|
97
+ {
98
+ "name" => job.name,
99
+ "version" => job.version,
100
+ "sha1" => job.checksum,
101
+ }
102
+ end
103
+
104
+ manifest["name"] = release_name
105
+
106
+ unless manifest["name"].bosh_valid_id?
107
+ raise InvalidRelease, "Release name `#{manifest["name"]}' " +
108
+ "is not a valid BOSH identifier"
109
+ end
110
+
111
+ fingerprint = make_fingerprint(manifest)
112
+
113
+ if @index[fingerprint]
114
+ old_version = @index[fingerprint]["version"]
115
+ say("This version is no different from version #{old_version}")
116
+ @version = old_version
117
+ else
118
+ @version = assign_version
119
+ @index.add_version(fingerprint, { "version" => @version })
120
+ end
121
+
122
+ manifest["version"] = @version
123
+ manifest_yaml = YAML.dump(manifest)
124
+
125
+ say("Writing manifest...")
126
+ File.open(File.join(build_dir, "release.MF"), "w") do |f|
127
+ f.write(manifest_yaml)
128
+ end
129
+
130
+ File.open(manifest_path, "w") do |f|
131
+ f.write(manifest_yaml)
132
+ end
133
+
134
+ @manifest_generated = true
135
+ end
136
+
137
+ def generate_tarball
138
+ generate_manifest unless @manifest_generated
139
+ return if @index.version_exists?(@version)
140
+
141
+ unless @jobs_copied
142
+ header("Copying jobs...")
143
+ copy_jobs
144
+ nl
145
+ end
146
+ unless @packages_copied
147
+ header("Copying packages...")
148
+ copy_packages
149
+ nl
150
+ end
151
+
152
+ FileUtils.mkdir_p(File.dirname(tarball_path))
153
+
154
+ in_build_dir do
155
+ `tar -czf #{tarball_path} . 2>&1`
156
+ unless $?.exitstatus == 0
157
+ raise InvalidRelease, "Cannot create release tarball"
158
+ end
159
+ say("Generated #{tarball_path}")
160
+ end
161
+ end
162
+
163
+ def releases_dir
164
+ File.join(@release.dir, final? ? "releases" : "dev_releases")
165
+ end
166
+
167
+ def tarball_path
168
+ File.join(releases_dir, "#{release_name}-#{version}.tgz")
169
+ end
170
+
171
+ def manifest_path
172
+ File.join(releases_dir, "#{release_name}-#{version}.yml")
173
+ end
174
+
175
+ def make_fingerprint(item)
176
+ case item
177
+ when Array
178
+ source = item.map { |e| make_fingerprint(e) }.sort.join("")
179
+ when Hash
180
+ source = item.keys.sort.map{ |k| make_fingerprint(item[k]) }.join("")
181
+ else
182
+ source = item.to_s
183
+ end
184
+ Digest::SHA1.hexdigest(source)
185
+ end
186
+
187
+ private
188
+
189
+ def version=(version)
190
+ @version = version
191
+ end
192
+
193
+ def assign_version
194
+ current_version = @index.latest_version.to_i
195
+ current_version + 1
196
+ end
197
+
198
+ def build_dir
199
+ @build_dir ||= Dir.mktmpdir
200
+ end
201
+
202
+ def create_release_build_dir
203
+ in_build_dir do
204
+ FileUtils.mkdir("packages")
205
+ FileUtils.mkdir("jobs")
206
+ end
207
+ end
208
+
209
+ def in_build_dir(&block)
210
+ Dir.chdir(build_dir) { yield }
211
+ end
212
+
213
+ end
214
+
215
+ end