minestrone 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/main.yml +32 -0
  3. data/.gitignore +5 -0
  4. data/Gemfile +10 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +35 -0
  7. data/Rakefile +10 -0
  8. data/bin/capify +89 -0
  9. data/bin/min +5 -0
  10. data/docs/lib-codebase-map.md +162 -0
  11. data/docs/lib-dependency-graph.svg +129 -0
  12. data/lib/minestrone/callback.rb +45 -0
  13. data/lib/minestrone/cli/help.rb +131 -0
  14. data/lib/minestrone/cli/help.txt +72 -0
  15. data/lib/minestrone/cli/options.rb +232 -0
  16. data/lib/minestrone/cli.rb +159 -0
  17. data/lib/minestrone/command.rb +177 -0
  18. data/lib/minestrone/configuration/actions/file_transfer.rb +53 -0
  19. data/lib/minestrone/configuration/actions/inspect.rb +46 -0
  20. data/lib/minestrone/configuration/actions/invocation.rb +202 -0
  21. data/lib/minestrone/configuration/alias_task.rb +29 -0
  22. data/lib/minestrone/configuration/callbacks.rb +129 -0
  23. data/lib/minestrone/configuration/connections.rb +66 -0
  24. data/lib/minestrone/configuration/execution.rb +139 -0
  25. data/lib/minestrone/configuration/loading.rb +207 -0
  26. data/lib/minestrone/configuration/log_formatters.rb +75 -0
  27. data/lib/minestrone/configuration/namespaces.rb +225 -0
  28. data/lib/minestrone/configuration/servers.rb +70 -0
  29. data/lib/minestrone/configuration/variables.rb +115 -0
  30. data/lib/minestrone/configuration.rb +69 -0
  31. data/lib/minestrone/errors.rb +17 -0
  32. data/lib/minestrone/ext/string.rb +7 -0
  33. data/lib/minestrone/extensions.rb +56 -0
  34. data/lib/minestrone/logger.rb +171 -0
  35. data/lib/minestrone/processable.rb +50 -0
  36. data/lib/minestrone/recipes/deploy/assets.rb +194 -0
  37. data/lib/minestrone/recipes/deploy/bundler.rb +81 -0
  38. data/lib/minestrone/recipes/deploy/dependencies.rb +44 -0
  39. data/lib/minestrone/recipes/deploy/local_dependency.rb +45 -0
  40. data/lib/minestrone/recipes/deploy/remote_dependency.rb +119 -0
  41. data/lib/minestrone/recipes/deploy/scm/base.rb +204 -0
  42. data/lib/minestrone/recipes/deploy/scm/git.rb +284 -0
  43. data/lib/minestrone/recipes/deploy/scm/none.rb +54 -0
  44. data/lib/minestrone/recipes/deploy/scm.rb +22 -0
  45. data/lib/minestrone/recipes/deploy/strategy/base.rb +87 -0
  46. data/lib/minestrone/recipes/deploy/strategy/copy.rb +353 -0
  47. data/lib/minestrone/recipes/deploy/strategy/remote_cache.rb +80 -0
  48. data/lib/minestrone/recipes/deploy/strategy.rb +22 -0
  49. data/lib/minestrone/recipes/deploy.rb +639 -0
  50. data/lib/minestrone/recipes/standard.rb +23 -0
  51. data/lib/minestrone/recipes/templates/maintenance.rhtml +53 -0
  52. data/lib/minestrone/server_definition.rb +56 -0
  53. data/lib/minestrone/ssh.rb +81 -0
  54. data/lib/minestrone/task_definition.rb +82 -0
  55. data/lib/minestrone/transfer.rb +205 -0
  56. data/lib/minestrone/version.rb +11 -0
  57. data/lib/minestrone.rb +3 -0
  58. data/minestrone.gemspec +32 -0
  59. data/test/cli/execute_test.rb +130 -0
  60. data/test/cli/help_test.rb +178 -0
  61. data/test/cli/options_test.rb +315 -0
  62. data/test/cli/ui_test.rb +26 -0
  63. data/test/cli_test.rb +17 -0
  64. data/test/command_test.rb +305 -0
  65. data/test/configuration/actions/file_transfer_test.rb +61 -0
  66. data/test/configuration/actions/inspect_test.rb +76 -0
  67. data/test/configuration/actions/invocation_test.rb +258 -0
  68. data/test/configuration/alias_task_test.rb +110 -0
  69. data/test/configuration/callbacks_test.rb +201 -0
  70. data/test/configuration/connections_test.rb +192 -0
  71. data/test/configuration/execution_test.rb +176 -0
  72. data/test/configuration/loading_test.rb +149 -0
  73. data/test/configuration/namespace_dsl_test.rb +325 -0
  74. data/test/configuration/servers_test.rb +100 -0
  75. data/test/configuration/variables_test.rb +191 -0
  76. data/test/configuration_test.rb +77 -0
  77. data/test/deploy/local_dependency_test.rb +61 -0
  78. data/test/deploy/remote_dependency_test.rb +146 -0
  79. data/test/deploy/scm/base_test.rb +55 -0
  80. data/test/deploy/scm/git_test.rb +260 -0
  81. data/test/deploy/scm/none_test.rb +26 -0
  82. data/test/deploy/strategy/copy_test.rb +360 -0
  83. data/test/extensions_test.rb +69 -0
  84. data/test/fixtures/cli_integration.rb +5 -0
  85. data/test/fixtures/config.rb +4 -0
  86. data/test/fixtures/custom.rb +3 -0
  87. data/test/logger_formatting_test.rb +149 -0
  88. data/test/logger_test.rb +134 -0
  89. data/test/recipes_test.rb +26 -0
  90. data/test/server_definition_test.rb +121 -0
  91. data/test/ssh_test.rb +99 -0
  92. data/test/task_definition_test.rb +117 -0
  93. data/test/transfer_test.rb +172 -0
  94. data/test/utils.rb +28 -0
  95. data/test/version_test.rb +11 -0
  96. metadata +258 -0
@@ -0,0 +1,353 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'minestrone/recipes/deploy/strategy/base'
4
+ require 'fileutils'
5
+ require 'tempfile' # Dir.tmpdir
6
+
7
+ module Minestrone
8
+ module Deploy
9
+ module Strategy
10
+
11
+ # This class implements the strategy for deployments which work
12
+ # by preparing the source code locally, compressing it, copying the
13
+ # file to the target host, and uncompressing it to the deployment
14
+ # directory.
15
+ #
16
+ # By default, the SCM checkout command is used to obtain the local copy
17
+ # of the source code. If you would rather use the export operation,
18
+ # you can set the :copy_strategy variable to :export.
19
+ #
20
+ # set :copy_strategy, :export
21
+ #
22
+ # For even faster deployments, you can set the :copy_cache variable to
23
+ # true. This will cause deployments to do a new checkout of your
24
+ # repository to a new directory, and then copy that checkout. Subsequent
25
+ # deploys will just resync that copy, rather than doing an entirely new
26
+ # checkout. Additionally, you can specify file patterns to exclude from
27
+ # the copy when using :copy_cache; just set the :copy_exclude variable
28
+ # to a file glob (or an array of globs).
29
+ #
30
+ # set :copy_cache, true
31
+ # set :copy_exclude, ".git/*"
32
+ #
33
+ # Note that :copy_strategy is ignored when :copy_cache is set. Also, if
34
+ # you want the copy cache put somewhere specific, you can set the variable
35
+ # to the path you want, instead of merely 'true':
36
+ #
37
+ # set :copy_cache, "/tmp/caches/myapp"
38
+ #
39
+ # This deployment strategy also supports a special variable,
40
+ # :copy_compression, which must be one of :gzip, :bz2, or
41
+ # :zip, and which specifies how the source should be compressed for
42
+ # transmission to each host.
43
+ #
44
+ # By default, files will be transferred across to the remote machines via 'sftp'. If you prefer
45
+ # to use 'scp' you can set the :copy_via variable to :scp.
46
+ #
47
+ # set :copy_via, :scp
48
+ #
49
+ # There is a possibility to pass a build command that will get
50
+ # executed if your code needs to be compiled or something needs to be
51
+ # done before the code is ready to run.
52
+ #
53
+ # set :build_script, "make all"
54
+ #
55
+ # Note that if you use :copy_cache, the :build_script is used on the
56
+ # cache and thus you get faster compilation if your script does not
57
+ # recompile everything.
58
+
59
+ class Copy < Base
60
+
61
+ # Obtains a copy of the source code locally (via the #command method),
62
+ # compresses it to a single file, copies that file to the target
63
+ # server, and uncompresses it into the deployment directory.
64
+
65
+ def deploy!
66
+ copy_cache ? run_copy_cache_strategy : run_copy_strategy
67
+
68
+ create_revision_file
69
+ compress_repository
70
+ distribute!
71
+ ensure
72
+ rollback_changes
73
+ end
74
+
75
+ def build(directory)
76
+ return unless build_script
77
+
78
+ execute "running build script on #{directory}" do
79
+ Dir.chdir(directory) { system(build_script) }
80
+ end
81
+ end
82
+
83
+ def check!
84
+ super.check do |d|
85
+ d.local.command(source.local.command) if source.local.command
86
+ d.local.command(compress(nil, nil).first)
87
+ d.remote.command(decompress(nil).first)
88
+ end
89
+ end
90
+
91
+ # Returns the location of the local copy cache, if the strategy should
92
+ # use a local cache + copy instead of a new checkout/export every
93
+ # time. Returns +nil+ unless :copy_cache has been set. If :copy_cache
94
+ # is +true+, a default cache location will be returned.
95
+
96
+ def copy_cache
97
+ @copy_cache ||= if configuration[:copy_cache] == true
98
+ File.expand_path(configuration[:application], Dir.tmpdir)
99
+ else
100
+ File.expand_path(configuration[:copy_cache], Dir.pwd)
101
+ end
102
+ rescue StandardError
103
+ nil
104
+ end
105
+
106
+
107
+ private
108
+
109
+ def run_copy_cache_strategy
110
+ copy_repository_to_local_cache
111
+ build(copy_cache)
112
+ copy_cache_to_staging_area
113
+ end
114
+
115
+ def run_copy_strategy
116
+ copy_repository_to_server
117
+ build(destination)
118
+ remove_excluded_files if copy_exclude.any?
119
+ end
120
+
121
+ def execute(description, &block)
122
+ logger.debug description
123
+ handle_system_errors(&block)
124
+ end
125
+
126
+ def handle_system_errors(&block)
127
+ block.call
128
+ raise_command_failed if last_command_failed?
129
+ end
130
+
131
+ def refresh_local_cache
132
+ execute "refreshing local cache to revision #{revision} at #{copy_cache}" do
133
+ system(source.sync(revision, copy_cache))
134
+ end
135
+ end
136
+
137
+ def create_local_cache
138
+ execute "preparing local cache at #{copy_cache}" do
139
+ system(source.checkout(revision, copy_cache))
140
+ end
141
+ end
142
+
143
+ def raise_command_failed
144
+ raise Minestrone::Error, "shell command failed with return code #{$?}"
145
+ end
146
+
147
+ def last_command_failed?
148
+ $? != 0
149
+ end
150
+
151
+ def copy_cache_to_staging_area
152
+ execute "copying cache to deployment staging area #{destination}" do
153
+ create_destination
154
+ Dir.chdir(copy_cache) { copy_files(queue_files) }
155
+ end
156
+ end
157
+
158
+ def create_destination
159
+ FileUtils.mkdir_p(destination)
160
+ end
161
+
162
+ def copy_files(files)
163
+ files.each { |name| process_file(name) }
164
+ end
165
+
166
+ def process_file(name)
167
+ send "copy_#{filetype(name)}", name
168
+ end
169
+
170
+ def filetype(name)
171
+ filetype = File.ftype(name)
172
+
173
+ case filetype
174
+ when 'link', 'directory'
175
+ filetype
176
+ else
177
+ 'file'
178
+ end
179
+ end
180
+
181
+ def copy_link(name)
182
+ FileUtils.ln_s(File.readlink(name), File.join(destination, name))
183
+ end
184
+
185
+ def copy_directory(name)
186
+ FileUtils.mkdir(File.join(destination, name))
187
+ copy_files(queue_files(name))
188
+ end
189
+
190
+ def copy_file(name)
191
+ FileUtils.ln(name, File.join(destination, name))
192
+ end
193
+
194
+ def queue_files(directory = nil)
195
+ Dir.glob(pattern_for(directory), File::FNM_DOTMATCH).reject! { |file| excluded_files_contain?(file) }
196
+ end
197
+
198
+ def pattern_for(directory)
199
+ (!directory.nil?) ? "#{escape_globs(directory)}/*" : "*"
200
+ end
201
+
202
+ def escape_globs(path)
203
+ path.gsub(/[*?{}\[\]]/, '\\\\\\&')
204
+ end
205
+
206
+ def excluded_files_contain?(file)
207
+ copy_exclude.any? { |p| File.fnmatch(p, file) } || ['.', '..'].include?(File.basename(file))
208
+ end
209
+
210
+ def copy_repository_to_server
211
+ execute "getting (via #{copy_strategy}) revision #{revision} to #{destination}" do
212
+ copy_repository_via_strategy
213
+ end
214
+ end
215
+
216
+ def copy_repository_via_strategy
217
+ system(command)
218
+ end
219
+
220
+ def remove_excluded_files
221
+ logger.debug "processing exclusions..."
222
+
223
+ copy_exclude.each do |pattern|
224
+ delete_list = Dir.glob(File.join(destination, pattern), File::FNM_DOTMATCH)
225
+ # avoid the /.. trap that deletes the parent directories
226
+ delete_list.delete_if { |dir| dir =~ /\/\.\.$/ }
227
+ FileUtils.rm_rf(delete_list.compact)
228
+ end
229
+ end
230
+
231
+ def create_revision_file
232
+ File.open(File.join(destination, "REVISION"), "w") { |f| f.puts(revision) }
233
+ end
234
+
235
+ def compress_repository
236
+ execute "Compressing #{destination} to #{filename}" do
237
+ Dir.chdir(copy_dir) { system(compress(File.basename(destination), File.basename(filename)).join(" ")) }
238
+ end
239
+ end
240
+
241
+ def rollback_changes
242
+ FileUtils.rm(filename) rescue nil
243
+ FileUtils.rm_rf(destination) rescue nil
244
+ end
245
+
246
+ def copy_repository_to_local_cache
247
+ File.exist?(copy_cache) ? refresh_local_cache : create_local_cache
248
+ end
249
+
250
+ def build_script
251
+ configuration[:build_script]
252
+ end
253
+
254
+ # Specify patterns to exclude from the copy. This is only valid
255
+ # when using a local cache.
256
+ def copy_exclude
257
+ @copy_exclude ||= Array(configuration.fetch(:copy_exclude, []))
258
+ end
259
+
260
+ # Returns the basename of the release_path, which will be used to
261
+ # name the local copy and archive file.
262
+ def destination
263
+ @destination ||= File.join(copy_dir, File.basename(configuration[:release_path]))
264
+ end
265
+
266
+ # Returns the value of the :copy_strategy variable, defaulting to
267
+ # :checkout if it has not been set.
268
+ def copy_strategy
269
+ @copy_strategy ||= configuration.fetch(:copy_strategy, :checkout)
270
+ end
271
+
272
+ # Should return the command(s) necessary to obtain the source code
273
+ # locally.
274
+ def command
275
+ @command ||= case copy_strategy
276
+ when :checkout
277
+ source.checkout(revision, destination)
278
+ when :export
279
+ source.export(revision, destination)
280
+ end
281
+ end
282
+
283
+ # Returns the name of the file that the source code will be
284
+ # compressed to.
285
+ def filename
286
+ @filename ||= File.join(copy_dir, "#{File.basename(destination)}.#{compression.extension}")
287
+ end
288
+
289
+ # The directory to which the copy should be checked out
290
+ def copy_dir
291
+ @copy_dir ||= File.expand_path(configuration[:copy_dir] || Dir.tmpdir, Dir.pwd)
292
+ end
293
+
294
+ # The directory on the remote server to which the archive should be
295
+ # copied
296
+ def remote_dir
297
+ @remote_dir ||= configuration[:copy_remote_dir] || "/tmp"
298
+ end
299
+
300
+ # The location on the remote server where the file should be
301
+ # temporarily stored.
302
+ def remote_filename
303
+ @remote_filename ||= File.join(remote_dir, File.basename(filename))
304
+ end
305
+
306
+ # A struct for representing the specifics of a compression type.
307
+ # Commands are arrays, where the first element is the utility to be
308
+ # used to perform the compression or decompression.
309
+ Compression = Struct.new(:extension, :compress_command, :decompress_command)
310
+
311
+ # The compression method to use, defaults to :gzip.
312
+ def compression
313
+ remote_tar = configuration[:copy_remote_tar] || 'tar'
314
+ local_tar = configuration[:copy_local_tar] || 'tar'
315
+ type = configuration[:copy_compression] || :gzip
316
+
317
+ case type
318
+ when :gzip, :gz then Compression.new("tar.gz", [local_tar, 'czf'], [remote_tar, 'xzf'])
319
+ when :bzip2, :bz2 then Compression.new("tar.bz2", [local_tar, 'cjf'], [remote_tar, 'xjf'])
320
+ when :zip then Compression.new("zip", %w(zip -qyr), %w(unzip -q))
321
+ else raise ArgumentError, "invalid compression type #{type.inspect}"
322
+ end
323
+ end
324
+
325
+ # Returns the command necessary to compress the given directory
326
+ # into the given file.
327
+ def compress(directory, file)
328
+ compression.compress_command + [file, directory]
329
+ end
330
+
331
+ # Returns the command necessary to decompress the given file,
332
+ # relative to the current working directory. It must also
333
+ # preserve the directory structure in the file.
334
+ def decompress(file)
335
+ compression.decompress_command + [file]
336
+ end
337
+
338
+ def decompress_remote_file
339
+ run "cd #{configuration[:releases_path]} && #{decompress(remote_filename).join(" ")} && rm #{remote_filename}"
340
+ end
341
+
342
+ # Uploads the file to the remote server
343
+ def distribute!
344
+ args = [filename, remote_filename]
345
+ args << { :via => configuration[:copy_via] } if configuration[:copy_via]
346
+
347
+ upload(*args)
348
+ decompress_remote_file
349
+ end
350
+ end
351
+ end
352
+ end
353
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'minestrone/recipes/deploy/strategy/base'
4
+
5
+ module Minestrone
6
+ module Deploy
7
+ module Strategy
8
+
9
+ # Implements the deployment strategy that keeps a cached checkout of
10
+ # the source code on the remote server. Each deploy simply updates the
11
+ # cached checkout, and then does a copy from the cached copy to the
12
+ # final deployment location.
13
+
14
+ class RemoteCache < Base
15
+
16
+ # Executes the SCM command for this strategy and writes the REVISION mark file on the server.
17
+ def deploy!
18
+ update_repository_cache
19
+ copy_repository_cache
20
+ end
21
+
22
+ def check!
23
+ super.check do |d|
24
+ d.remote.command(source.command)
25
+ d.remote.command("rsync") unless copy_exclude.empty?
26
+ d.remote.writable(shared_path)
27
+ end
28
+ end
29
+
30
+
31
+ private
32
+
33
+ def repository_cache
34
+ File.join(shared_path, configuration[:repository_cache] || "cached-copy")
35
+ end
36
+
37
+ def update_repository_cache
38
+ logger.trace "updating the cached checkout on the server"
39
+
40
+ command = "if [ -d #{repository_cache} ]; then " +
41
+ "#{source.sync(revision, repository_cache)}; " +
42
+ "else #{source.checkout(revision, repository_cache)}; fi"
43
+
44
+ scm_run(command)
45
+ end
46
+
47
+ def copy_repository_cache
48
+ logger.trace "copying the cached version to #{configuration[:release_path]}"
49
+
50
+ if copy_exclude.empty?
51
+ run "cp -RPp #{repository_cache} #{configuration[:release_path]} && #{mark}"
52
+ else
53
+ exclusions = copy_exclude.map { |e| %(--exclude="#{e}") }.join(' ')
54
+ run "rsync -lrpt #{exclusions} #{repository_cache}/ #{configuration[:release_path]} && #{mark}"
55
+ end
56
+ end
57
+
58
+ def copy_exclude
59
+ @copy_exclude ||= Array(configuration.fetch(:copy_exclude, []))
60
+ end
61
+
62
+ # Runs the given command, filtering output back through the
63
+ # #handle_data filter of the SCM implementation.
64
+ def scm_run(command)
65
+ run(command) do |ch, stream, text|
66
+ ch[:state] ||= { :channel => ch }
67
+ output = source.handle_data(ch[:state], stream, text)
68
+ ch.send_data(output) if output
69
+ end
70
+ end
71
+
72
+ # Returns the command which will write the identifier of the
73
+ # revision being deployed to the REVISION file on the server.
74
+ def mark
75
+ "(echo #{revision} > #{configuration[:release_path]}/REVISION)"
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Minestrone
4
+ module Deploy
5
+ module Strategy
6
+ def self.new(strategy, config = {})
7
+ strategy_file = "minestrone/recipes/deploy/strategy/#{strategy}"
8
+ strategy_const = strategy.to_s.capitalize.gsub(/_(.)/) { $1.upcase }
9
+
10
+ require(strategy_file) unless const_defined?(strategy_const)
11
+
12
+ if const_defined?(strategy_const)
13
+ const_get(strategy_const).new(config)
14
+ else
15
+ raise Minestrone::Error, "could not find `#{name}::#{strategy_const}' in `#{strategy_file}'"
16
+ end
17
+ rescue LoadError
18
+ raise Minestrone::Error, "could not find any strategy named `#{strategy}'"
19
+ end
20
+ end
21
+ end
22
+ end