dpl 1.10.17.travis.6637.6 → 2.0.0.alpha.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (140) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +74 -0
  3. data/CONTRIBUTING.md +392 -0
  4. data/Gemfile +17 -3
  5. data/Gemfile.lock +373 -0
  6. data/LICENSE +16 -19
  7. data/NOTES.md +275 -0
  8. data/README.md +1977 -707
  9. data/Rakefile +2 -2
  10. data/bin/dpl +7 -3
  11. data/lib/dpl.rb +20 -0
  12. data/lib/dpl/assets/atlas/install +19 -0
  13. data/lib/dpl/assets/dpl/README.erb.md +133 -0
  14. data/lib/dpl/assets/dpl/git_ssh +2 -0
  15. data/lib/dpl/assets/git/detect_private_key +8 -0
  16. data/lib/dpl/assets/hephy/filter_log +3 -0
  17. data/lib/dpl/assets/pypi/install +4 -0
  18. data/lib/dpl/assets/scalingo/install +6 -0
  19. data/lib/dpl/cli.rb +36 -48
  20. data/lib/dpl/ctx.rb +2 -0
  21. data/lib/dpl/ctx/bash.rb +543 -0
  22. data/lib/dpl/ctx/test.rb +242 -0
  23. data/lib/dpl/helper/assets.rb +36 -0
  24. data/lib/dpl/helper/cmd.rb +167 -0
  25. data/lib/dpl/helper/config_file.rb +47 -0
  26. data/lib/dpl/helper/env.rb +39 -0
  27. data/lib/dpl/helper/interpolate.rb +126 -0
  28. data/lib/dpl/helper/memoize.rb +20 -0
  29. data/lib/dpl/helper/squiggle.rb +22 -0
  30. data/lib/dpl/helper/zip.rb +69 -0
  31. data/lib/dpl/provider.rb +562 -234
  32. data/lib/dpl/provider/dsl.rb +369 -0
  33. data/lib/dpl/provider/examples.rb +128 -0
  34. data/lib/dpl/provider/status.rb +59 -0
  35. data/lib/dpl/providers.rb +40 -0
  36. data/lib/dpl/providers/anynines.rb +65 -0
  37. data/lib/dpl/providers/atlas.rb +49 -0
  38. data/lib/dpl/providers/azure_web_apps.rb +59 -0
  39. data/lib/dpl/providers/bintray.rb +313 -0
  40. data/lib/dpl/providers/bluemixcloudfoundry.rb +92 -0
  41. data/lib/dpl/providers/boxfuse.rb +48 -0
  42. data/lib/dpl/providers/cargo.rb +19 -0
  43. data/lib/dpl/providers/chef_supermarket.rb +128 -0
  44. data/lib/dpl/providers/cloud66.rb +40 -0
  45. data/lib/dpl/providers/cloudfiles.rb +56 -0
  46. data/lib/dpl/providers/cloudfoundry.rb +81 -0
  47. data/lib/dpl/providers/codedeploy.rb +179 -0
  48. data/lib/dpl/providers/datica.rb +60 -0
  49. data/lib/dpl/providers/elasticbeanstalk.rb +195 -0
  50. data/lib/dpl/providers/engineyard.rb +107 -0
  51. data/lib/dpl/providers/firebase.rb +41 -0
  52. data/lib/dpl/providers/gae.rb +74 -0
  53. data/lib/dpl/providers/gcs.rb +105 -0
  54. data/lib/dpl/providers/hackage.rb +47 -0
  55. data/lib/dpl/providers/hephy.rb +101 -0
  56. data/lib/dpl/providers/heroku.rb +111 -0
  57. data/lib/dpl/providers/heroku/api.rb +119 -0
  58. data/lib/dpl/providers/heroku/git.rb +50 -0
  59. data/lib/dpl/providers/lambda.rb +202 -0
  60. data/lib/dpl/providers/launchpad.rb +74 -0
  61. data/lib/dpl/providers/netlify.rb +30 -0
  62. data/lib/dpl/providers/npm.rb +88 -0
  63. data/lib/dpl/providers/openshift.rb +46 -0
  64. data/lib/dpl/providers/opsworks.rb +142 -0
  65. data/lib/dpl/providers/packagecloud.rb +190 -0
  66. data/lib/dpl/providers/pages.rb +17 -0
  67. data/lib/dpl/providers/pages/api.rb +102 -0
  68. data/lib/dpl/providers/pages/git.rb +251 -0
  69. data/lib/dpl/providers/puppetforge.rb +44 -0
  70. data/lib/dpl/providers/pypi.rb +120 -0
  71. data/lib/dpl/providers/releases.rb +214 -0
  72. data/lib/dpl/providers/rubygems.rb +89 -0
  73. data/lib/dpl/providers/s3.rb +243 -0
  74. data/lib/dpl/providers/scalingo.rb +63 -0
  75. data/lib/dpl/providers/script.rb +28 -0
  76. data/lib/dpl/providers/snap.rb +59 -0
  77. data/lib/dpl/providers/surge.rb +55 -0
  78. data/lib/dpl/providers/testfairy.rb +93 -0
  79. data/lib/dpl/providers/transifex.rb +66 -0
  80. data/lib/dpl/support/aws_sdk_patch.rb +23 -0
  81. data/lib/dpl/support/gems.rb +69 -0
  82. data/lib/dpl/support/gstore_patch.rb +6 -0
  83. data/lib/dpl/support/version.rb +83 -0
  84. data/lib/dpl/version.rb +2 -2
  85. metadata +98 -169
  86. data/.coveralls.yml +0 -1
  87. data/.github/CONTRIBUTING.md +0 -173
  88. data/.github/stale.yml +0 -53
  89. data/.gitignore +0 -13
  90. data/.rspec +0 -2
  91. data/.travis.yml +0 -56
  92. data/dpl-anynines.gemspec +0 -3
  93. data/dpl-atlas.gemspec +0 -3
  94. data/dpl-azure_webapps.gemspec +0 -3
  95. data/dpl-bintray.gemspec +0 -3
  96. data/dpl-bitballoon.gemspec +0 -3
  97. data/dpl-bluemix_cloud_foundry.gemspec +0 -3
  98. data/dpl-boxfuse.gemspec +0 -3
  99. data/dpl-cargo.gemspec +0 -3
  100. data/dpl-catalyze.gemspec +0 -3
  101. data/dpl-chef_supermarket.gemspec +0 -20
  102. data/dpl-cloud66.gemspec +0 -3
  103. data/dpl-cloud_files.gemspec +0 -3
  104. data/dpl-cloud_foundry.gemspec +0 -3
  105. data/dpl-code_deploy.gemspec +0 -3
  106. data/dpl-deis.gemspec +0 -3
  107. data/dpl-elastic_beanstalk.gemspec +0 -3
  108. data/dpl-engine_yard.gemspec +0 -3
  109. data/dpl-firebase.gemspec +0 -3
  110. data/dpl-gae.gemspec +0 -3
  111. data/dpl-gcs.gemspec +0 -3
  112. data/dpl-hackage.gemspec +0 -3
  113. data/dpl-hephy.gemspec +0 -3
  114. data/dpl-heroku.gemspec +0 -3
  115. data/dpl-lambda.gemspec +0 -3
  116. data/dpl-launchpad.gemspec +0 -3
  117. data/dpl-npm.gemspec +0 -3
  118. data/dpl-openshift.gemspec +0 -3
  119. data/dpl-ops_works.gemspec +0 -3
  120. data/dpl-packagecloud.gemspec +0 -3
  121. data/dpl-pages.gemspec +0 -3
  122. data/dpl-puppet_forge.gemspec +0 -3
  123. data/dpl-pypi.gemspec +0 -3
  124. data/dpl-releases.gemspec +0 -8
  125. data/dpl-rubygems.gemspec +0 -3
  126. data/dpl-s3.gemspec +0 -3
  127. data/dpl-scalingo.gemspec +0 -3
  128. data/dpl-script.gemspec +0 -3
  129. data/dpl-snap.gemspec +0 -3
  130. data/dpl-surge.gemspec +0 -3
  131. data/dpl-testfairy.gemspec +0 -3
  132. data/dpl-transifex.gemspec +0 -3
  133. data/dpl.gemspec +0 -3
  134. data/gemspec_helper.rb +0 -51
  135. data/lib/dpl/error.rb +0 -3
  136. data/notes/engine_yard.md +0 -1
  137. data/notes/heroku.md +0 -3
  138. data/spec/cli_spec.rb +0 -36
  139. data/spec/provider_spec.rb +0 -191
  140. data/spec/spec_helper.rb +0 -20
@@ -0,0 +1,39 @@
1
+ module Dpl
2
+ module Env
3
+ def self.included(base)
4
+ base.extend(ClassMethods)
5
+ end
6
+
7
+ # should this sit in Cl?
8
+ module ClassMethods
9
+ attr_reader :env_prefixes
10
+
11
+ def env(*strs)
12
+ opts = strs.last.is_a?(Hash) ? strs.pop : {}
13
+ if strs.any?
14
+ strs = strs.map(&:to_s).map(&:upcase)
15
+ @env_prefixes = strs.map { |str| "#{str.to_s.upcase}_" }
16
+ # allow unconventional ENV vars such as GOOGLECLOUDKEYFILE
17
+ @env_prefixes += strs if opts[:allow_skip_underscore]
18
+ elsif env_prefixes
19
+ opts = ENV.select { |key, _| prefixed?(key) }
20
+ opts.map { |key, value| [unprefix(key).downcase.to_sym, value] }.to_h
21
+ else
22
+ {}
23
+ end
24
+ end
25
+
26
+ def prefixed?(key)
27
+ env_prefixes.any? { |prefix| key.to_s.start_with?(prefix) }
28
+ end
29
+
30
+ def unprefix(key)
31
+ env_prefixes.inject(key) { |key, prefix| key.sub(prefix, '') }
32
+ end
33
+ end
34
+
35
+ def opts
36
+ @opts ||= self.class.env.merge(super)
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,126 @@
1
+ require 'uri'
2
+
3
+ module Dpl
4
+ module Interpolate
5
+ # Interpolates variables in the given string.
6
+ #
7
+ # Variables can be contained in scripts, shell commands, and messages.
8
+ # They have the syntax `%{name}` or `%s` (or any other identifier supported
9
+ # by [Kernel#sprintf](https://ruby-doc.org/core-2.6.3/Kernel.html#method-i-format)).
10
+ #
11
+ # This supports two styles of interpolation:
12
+ #
13
+ # * Named variables `%{name}` and
14
+ # * Positional variables.
15
+ #
16
+ # Named variable names need to match constants on the provider class, or
17
+ # methods on the provider instance, which will be called in order to
18
+ # evaluate the value to be interpolated.
19
+ #
20
+ # Positional variables can be used if no corresponding method exists, e.g.
21
+ # if the value that needs to be interpolated is an argument passed to a
22
+ # local method.
23
+ #
24
+ # For example, using named variables:
25
+ #
26
+ # ```ruby
27
+ # def upload_file
28
+ # interpolate('Uploading file %{file} to %{target}')
29
+ # end
30
+ #
31
+ # def file
32
+ # './file_name'
33
+ # end
34
+ #
35
+ # def target
36
+ # 'target host'
37
+ # end
38
+ # ```
39
+ #
40
+ # Using positional variables:
41
+ #
42
+ # ```ruby
43
+ # def upload_file(file, target)
44
+ # interpolate('Uploading file %s to %s', file, target)
45
+ # end
46
+ # ```
47
+ #
48
+ # Implementors are encouraged to use named variables when possible, but
49
+ # are free to choose according to their needs.
50
+ def interpolate(str, args = [], opts = {})
51
+ args = args.shift if args.is_a?(Array) && args.first.is_a?(Hash)
52
+ Interpolator.new(str, self, args || {}, opts).apply
53
+ end
54
+
55
+ # Obfuscates the given string.
56
+ #
57
+ # Replaces all but the first N characters with asterisks, and paddes
58
+ # the string to a standard length of 20 characters. N depends on the
59
+ # length of the original string.
60
+ def obfuscate(str, opts = {})
61
+ return str if opts[:secure] || !str.tainted?
62
+ keep = (str.length / (4.0 + str.length / 5).round).round
63
+ keep = 1 if keep == 0
64
+ str[0, keep] + '*' * (20 - keep)
65
+ end
66
+
67
+ class Interpolator < Struct.new(:str, :obj, :args, :opts)
68
+ include Interpolate
69
+
70
+ MODIFIER = %i(obfuscate escape quote)
71
+ PATTERN = /%\{(\$?[\w]+)\}/
72
+ ENV_VAR = /^\$[A-Z_]+$/
73
+ UPCASE = /^[A-Z_]+$/
74
+
75
+ def apply
76
+ str = interpolate(self.str.to_s)
77
+ str = obfuscate(str) unless opts[:secure]
78
+ str = str.gsub(' ', ' ') if str.lines.size == 1
79
+ str
80
+ end
81
+
82
+ def interpolate(str)
83
+ str = str % args if args.is_a?(Array) && args.any?
84
+ str.to_s.gsub(PATTERN) { normalize(lookup($1.to_sym)) }
85
+ end
86
+
87
+ def obfuscate(str)
88
+ secrets(str).inject(str) do |str, secret|
89
+ str.gsub(secret, super(secret))
90
+ end
91
+ end
92
+
93
+ def secrets(str)
94
+ return [] unless str.is_a?(String) && str.tainted?
95
+ opts = obj.class.opts.select(&:secret?)
96
+ secrets = opts.map { |opt| obj.opts[opt.name] }.compact
97
+ secrets.select { |secret| str.include?(secret) }
98
+ end
99
+
100
+ def normalize(obj)
101
+ obj.is_a?(Array) ? obj.join(' ') : obj.to_s
102
+ end
103
+
104
+ def lookup(key)
105
+ if mod = modifier(key)
106
+ key = key.to_s.sub("#{mod}d_", '')
107
+ obj.send(mod, lookup(key))
108
+ elsif key.to_s =~ ENV_VAR
109
+ ENV[key.to_s.sub('$', '')]
110
+ elsif key.to_s =~ UPCASE && obj.class.const_defined?(key)
111
+ obj.class.const_get(key)
112
+ elsif args.is_a?(Hash) && args.key?(key)
113
+ args[key]
114
+ elsif obj.respond_to?(key, true)
115
+ obj.send(key)
116
+ else
117
+ raise KeyError, key
118
+ end
119
+ end
120
+
121
+ def modifier(key)
122
+ MODIFIER.detect { |mod| key.to_s.start_with?("#{mod}d_") }
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,20 @@
1
+ module Memoize
2
+ class ArgsError < StandardError; end
3
+
4
+ module ClassMethods
5
+ def memoize(name)
6
+ ivar = :"@#{name.to_s.sub('?', '_predicate')}"
7
+ prepend Module.new {
8
+ define_method(name) do |*args|
9
+ raise ArgsError.new('cannot pass arguments to memoized method %p' % name) unless args.empty?
10
+ return instance_variable_get(ivar) if instance_variable_defined?(ivar)
11
+ instance_variable_set(ivar, super())
12
+ end
13
+ }
14
+ end
15
+ end
16
+
17
+ def self.included(base)
18
+ base.extend(ClassMethods)
19
+ end
20
+ end
@@ -0,0 +1,22 @@
1
+ # Beloved squiggly heredocs did not exist in Ruby 2.1, which we still want to
2
+ # support, so let's give kudos with a method `sq`.
3
+ module Squiggle
4
+ # Outdents each line on a multiline string by the number of leading
5
+ # whitespace characters on the first line.
6
+ #
7
+ # This method exists so we can unindet heredoc strings the same way that
8
+ # Ruby 2.2's squiggly heredocs work, but still support Ruby 2.1 for the
9
+ # time being.
10
+ #
11
+ # For example:
12
+ #
13
+ # str = sq(<<-str)
14
+ # This multiline string will be outdented by two characters,
15
+ # so the extra indentation on this line will be kept,
16
+ # while this line sits on the same level as the first line.
17
+ # str
18
+ def sq(str)
19
+ width = str =~ /( *)\S/ && $1.size
20
+ str.lines.map { |line| line.gsub(/^ {#{width}}/, '') }.join
21
+ end
22
+ end
@@ -0,0 +1,69 @@
1
+ require 'tempfile'
2
+
3
+ module Dpl
4
+ class Zip < Struct.new(:src, :dest, :opts)
5
+ ZIP_EXT = %w(.zip .jar)
6
+
7
+ def initialize(*)
8
+ require 'zip'
9
+ super
10
+ end
11
+
12
+ def zip
13
+ if zip_file?
14
+ File.new(src)
15
+ elsif dir?
16
+ zip_dir
17
+ else
18
+ zip_file
19
+ end
20
+ end
21
+
22
+ def zip_dir
23
+ create(Dir.glob(*glob).reject { |path| dir?(path) })
24
+ end
25
+
26
+ def zip_file
27
+ create([src])
28
+ end
29
+
30
+ def create(files)
31
+ ::Zip::File.open(dest, ::Zip::File::CREATE) do |zip|
32
+ files.each do |file|
33
+ zip.add(file.sub("#{src}/", ''), file)
34
+ end
35
+ end
36
+ File.new(dest)
37
+ end
38
+
39
+ def zip_file?
40
+ exts.include?(File.extname(src))
41
+ end
42
+
43
+ def dir?(path = src)
44
+ File.directory?(path)
45
+ end
46
+
47
+ def copy
48
+ FileUtils.cp(src, dest)
49
+ end
50
+
51
+ def glob
52
+ glob = ["#{src}/**/*"]
53
+ glob << File::FNM_DOTMATCH if dot_match?
54
+ glob
55
+ end
56
+
57
+ def dot_match?
58
+ opts[:dot_match]
59
+ end
60
+
61
+ def exts
62
+ opts[:exts] ||= ZIP_EXT
63
+ end
64
+
65
+ def opts
66
+ super || {}
67
+ end
68
+ end
69
+ end
data/lib/dpl/provider.rb CHANGED
@@ -1,312 +1,640 @@
1
- require 'dpl/error'
2
- require 'dpl/version'
1
+ require 'cl'
3
2
  require 'fileutils'
3
+ require 'forwardable'
4
+ require 'shellwords'
5
+ require 'dpl/helper/assets'
6
+ require 'dpl/helper/cmd'
7
+ require 'dpl/helper/config_file'
8
+ require 'dpl/helper/env'
9
+ require 'dpl/helper/interpolate'
10
+ require 'dpl/helper/memoize'
11
+ require 'dpl/helper/squiggle'
12
+ require 'dpl/provider/dsl'
13
+ require 'dpl/provider/examples'
14
+ require 'dpl/version'
4
15
 
5
- module DPL
6
- class Provider
7
- include FileUtils
8
-
9
- # map of DPL provider class name constants to their corresponding
10
- # file names. There is no simple rule to map them automatically
11
- # (camel-cases, snake-cases, call-caps, etc.), so we need an explicit
12
- # map.
13
- GEM_NAME_OF = {
14
- 'Anynines' => 'anynines',
15
- 'Appfog' => 'appfog',
16
- 'Atlas' => 'atlas',
17
- 'AzureWebApps' => 'azure_webapps',
18
- 'Bintray' => 'bintray',
19
- 'BitBalloon' => 'bitballoon',
20
- 'BluemixCloudFoundry' => 'bluemix_cloud_foundry',
21
- 'Boxfuse' => 'boxfuse',
22
- 'Catalyze' => 'catalyze',
23
- 'ChefSupermarket' => 'chef_supermarket',
24
- 'Cloud66' => 'cloud66',
25
- 'CloudFiles' => 'cloud_files',
26
- 'CloudFoundry' => 'cloud_foundry',
27
- 'CodeDeploy' => 'code_deploy',
28
- 'Cargo' => 'cargo',
29
- 'Deis' => 'deis',
30
- 'ElasticBeanstalk' => 'elastic_beanstalk',
31
- 'EngineYard' => 'engine_yard',
32
- 'Firebase' => 'firebase',
33
- 'GAE' => 'gae',
34
- 'GCS' => 'gcs',
35
- 'Hackage' => 'hackage',
36
- 'Hephy' => 'hephy',
37
- 'Heroku' => 'heroku',
38
- 'Lambda' => 'lambda',
39
- 'Launchpad' => 'launchpad',
40
- 'Nodejitsu' => 'nodejitsu',
41
- 'NPM' => 'npm',
42
- 'Openshift' => 'openshift',
43
- 'OpsWorks' => 'ops_works',
44
- 'Packagecloud' => 'packagecloud',
45
- 'Pages' => 'pages',
46
- 'PuppetForge' => 'puppet_forge',
47
- 'PyPI' => 'pypi',
48
- 'Releases' => 'releases',
49
- 'RubyGems' => 'rubygems',
50
- 'S3' => 's3',
51
- 'Scalingo' => 'scalingo',
52
- 'Script' => 'script',
53
- 'Snap' => 'snap',
54
- 'Surge' => 'surge',
55
- 'TestFairy' => 'testfairy',
56
- 'Transifex' => 'transifex',
57
- }
16
+ module Dpl
17
+ # Base class for all concrete providers that `dpl` supports.
18
+ #
19
+ # These are subclasses of `Cl::Cmd` which means they are going to be detected
20
+ # by the first argument passed to `dpl [provider]`, instantiated, and run.
21
+ #
22
+ # Implementors are encouraged to use the provider DSL to declare various
23
+ # features, requirements, and attributes that apply to their provider, to
24
+ # implement any of the following stages (methods) according to their needs
25
+ # and semantics:
26
+ #
27
+ # * init
28
+ # * install
29
+ # * login
30
+ # * setup
31
+ # * validate
32
+ # * prepare
33
+ # * deploy
34
+ # * finish
35
+ #
36
+ # The main logic should sit in the `deploy` stage.
37
+ #
38
+ # If at any time the method `error` is called, or any exception raised the
39
+ # deploy process will be halted, and subsequent stages skipped. However, the
40
+ # stage `finish` will run even if previous stages have raised an error,
41
+ # giving the provider the opportunity to potentially clean up stage.
42
+ #
43
+ # In addition to this the following methods will be called if implemented
44
+ # by the provider:
45
+ #
46
+ # * run_cmd
47
+ # * add_key
48
+ # * remove_key
49
+ #
50
+ # Like the `finish` stage, the method `remove_key` will be called even if
51
+ # previous stages have raised an error.
52
+ #
53
+ # See the respective method's documentation for details on these.
54
+ #
55
+ # The following stages are not meant to be overwritten, but considered
56
+ # internal:
57
+ #
58
+ # * before_install
59
+ # * before_setup
60
+ # * before_prepare
61
+ # * before_finish
62
+ #
63
+ # Dependencies declared as required, such as APT, NPM, or Python are going to
64
+ # be installed as part of the `before_install` stage .
65
+ #
66
+ # Cleanup is run as part of the `before_prepare` stage if the option
67
+ # `--cleanup` was given. This will use `git stash --all` in order to reset
68
+ # the working directory to the committed state, and cleanup any left over
69
+ # artifacts from the build process. Providers can use the DSL method `keep`
70
+ # in order to declare known artifacts (such as CLI tooling installed to the
71
+ # working directory) that needs to be moved out of the way and restored after
72
+ # the cleanup process. (It is recommended to place such artifacts outside of
73
+ # the build working directory though, for example in `~/.dpl`).
74
+ #
75
+ # The method `run_cmd` is called for each command specified using the `--run`
76
+ # option. By default, these command are going to be run as local shell
77
+ # commands, but providers can choose to overwrite this method in order to run
78
+ # the command on a remote machine.
79
+ #
80
+ # @see https://github.com/svenfuchs/cl Cl's documentation for details on how
81
+ # providers (commands) are declared and run.
82
+
83
+ class Provider < Cl::Cmd
84
+ extend Dsl, Forwardable
85
+ include Assets, Env, ConfigFile, FileUtils, Interpolate, Memoize, Squiggle
86
+
87
+ class << self
88
+ def examples
89
+ @examples ||= super || Examples.new(self).cmds
90
+ end
58
91
 
59
- def self.new(context, options)
60
- return super if self < Provider
61
-
62
- # when requiring the file corresponding to the provider name
63
- # given in the options, the general strategy is to normalize
64
- # the option to lower-case alphanumeric, then
65
- # use that key to find the file name using the GEM_NAME_OF map.
66
-
67
- context.fold("Installing deploy dependencies") do
68
- begin
69
- opt_lower = super.option(:provider).to_s.downcase
70
- opt = opt_lower.gsub(/[^a-z0-9]/, '')
71
- class_name = class_of(opt)
72
- raise Error, "could not find provider %p" % opt unless class_name
73
- require "dpl/provider/#{GEM_NAME_OF[class_name]}"
74
- provider = const_get(class_name).new(context, options)
75
- rescue NameError, LoadError => e
76
- if /uninitialized constant DPL::Provider::(?<provider_wanted>\S+)/ =~ e.message
77
- provider_gem_name = GEM_NAME_OF[provider_wanted]
78
- elsif %r(cannot load such file -- dpl/provider/(?<provider_file_name>\S+)) =~ e.message
79
- provider_gem_name = GEM_NAME_OF[class_name]
80
- else
81
- # don't know what to do with this error
82
- raise e
83
- end
84
- install_cmd = "gem install dpl-#{provider_gem_name || opt} -v #{ENV['DPL_VERSION'] || DPL::VERSION}"
85
-
86
- if File.exist?(local_gem = File.join(Dir.pwd, "dpl-#{GEM_NAME_OF[provider_gem_name] || opt_lower}-#{ENV['DPL_VERSION'] || DPL::VERSION}.gem"))
87
- install_cmd = "gem install #{local_gem}"
88
- end
89
-
90
- context.shell(install_cmd)
91
- Gem.clear_paths
92
-
93
- require "dpl/provider/#{GEM_NAME_OF[class_name]}"
94
- provider = const_get(class_name).new(context, options)
95
- rescue DPL::Error
96
- if opt_lower
97
- provider = const_get(opt.capitalize).new(context, options)
98
- else
99
- raise Error, 'missing provider'
100
- end
101
- end
102
-
103
- if options[:no_deploy]
104
- def provider.deploy; end
105
- else
106
- provider.install_deploy_dependencies if provider.respond_to? :install_deploy_dependencies
107
- end
108
-
109
- provider
92
+ def move_files(ctx)
93
+ ctx.move_files(move) if move.any?
110
94
  end
111
- end
112
95
 
113
- def self.experimental(name)
114
- puts "", "!!! #{name} support is experimental !!!", ""
115
- end
96
+ def unmove_files(ctx)
97
+ ctx.unmove_files(move) if move.any?
98
+ end
116
99
 
117
- def self.deprecated(*lines)
118
- puts ''
119
- lines.each do |line|
120
- puts "\e[31;1m#{line}\e[0m"
100
+ def install_deps?
101
+ apt? || gem? || npm? || pip?
102
+ end
103
+
104
+ def install_deps(ctx)
105
+ ctx.apts_get(apt) if apt?
106
+ ctx.gems_require(gem) if gem?
107
+ npm.each { |npm| ctx.npm_install *npm } if npm?
108
+ pip.each { |pip| ctx.pip_install *pip } if pip?
109
+ end
110
+
111
+ def validate_runtimes(ctx)
112
+ ctx.validate_runtimes(runtimes) if runtimes.any?
121
113
  end
122
- puts ''
123
114
  end
124
115
 
125
- def self.context
126
- self
116
+ # Fold names to display in the build log.
117
+ FOLDS = {
118
+ init: 'Initialize deployment',
119
+ setup: 'Setup deployment',
120
+ validate: 'Validate deployment',
121
+ install: 'Install deployment dependencies',
122
+ login: 'Authenticate deployment',
123
+ prepare: 'Prepare deployment',
124
+ deploy: 'Run deployment',
125
+ finish: 'Finish deployment',
126
+ }
127
+
128
+ # Deployment process stages.
129
+ #
130
+ # In addition to the stages listed here the stage `finish` will be run at
131
+ # the end of the process.
132
+ #
133
+ # Also, the methods `add_key` (called before `setup`), `remove_key` (called
134
+ # before `finish`), and `run_cmd` (called after `deploy`) may be of
135
+ # interest to implementors.
136
+ STAGES = %i(
137
+ init
138
+ install
139
+ login
140
+ setup
141
+ validate
142
+ prepare
143
+ deploy
144
+ )
145
+
146
+ abstract
147
+
148
+ arg :provider, 'The provider name', required: true
149
+
150
+ opt '--run CMD', 'Command to execute after the deployment finished successfully', type: :array
151
+ opt '--cleanup', 'Skip cleaning up build artifacts before the deployment', negate: %w(skip)
152
+ opt '--stage NAME', 'Execute the given stage(s) only', type: :array, internal: true, default: STAGES
153
+ opt '--backtrace', 'Print the backtrace for exceptions', internal: true
154
+ opt '--fold', 'Wrap log output in folds', internal: true
155
+
156
+ msgs before_install: 'Installing deployment dependencies',
157
+ before_setup: 'Setting the build environment up for the deployment',
158
+ setup_git_ssh: 'Setting up git-ssh',
159
+ cleanup: 'Cleaning up git repository with `git stash --all`',
160
+ ssh_keygen: 'Generating SSH key',
161
+ setup_git_ua: 'Setting up git HTTP user agent',
162
+ ssh_remote_host: 'SSH remote is %s at port %s',
163
+ ssh_try_connect: 'Waiting for SSH connection ...',
164
+ ssh_connected: 'SSH connection established.',
165
+ ssh_failed: 'Failed to establish SSH connection.'
166
+
167
+ def_delegators :'self.class', :status, :full_name, :install_deps,
168
+ :install_deps?, :keep, :move_files, :unmove_files, :needs?, :runtimes,
169
+ :validate_runtimes, :user_agent
170
+
171
+ def_delegators :ctx, :apt_get, :gem_require, :npm_install, :pip_install,
172
+ :build_dir, :build_number, :repo_slug, :encoding, :git_author_email,
173
+ :git_author_name, :git_branch, :git_commit_msg, :git_dirty?, :git_log,
174
+ :git_ls_files, :git_ls_remote?, :git_remote_urls, :git_rev_parse,
175
+ :git_sha, :git_tag, :machine_name, :node_version, :npm_version, :sleep,
176
+ :ssh_keygen, :success?, :tmp_dir, :which, :logger, :rendezvous,
177
+ :file_size, :write_file, :write_netrc, :last_out, :last_err, :test?,
178
+ :tty?
179
+
180
+ attr_reader :repo_name, :key_name
181
+
182
+ def initialize(ctx, *args)
183
+ @repo_name = ctx.repo_name
184
+ @key_name = ctx.machine_name
185
+ super
127
186
  end
128
187
 
129
- def self.shell(command, options = {})
130
- system(command)
188
+ # Runs all stages, all commands provided by the user, as well as the final
189
+ # stage `finish` (which will be run even if an error has been raised during
190
+ # previous stages).
191
+ def run
192
+ stages = stage.select { |stage| run_stage?(stage) }
193
+ stages.each { |stage| run_stage(stage) }
194
+ run_cmds
195
+ rescue Error
196
+ raise
197
+ rescue Exception => e
198
+ raise Error.new("#{e.message} (#{e.class})", backtrace: backtrace? ? e.backtrace : nil) unless test?
199
+ raise
200
+ ensure
201
+ run_stage(:finish, fold: false) if finish?
131
202
  end
132
203
 
133
- def self.apt_get(name, command = name)
134
- context.shell("sudo apt-get -qq install #{name}", retry: true) if `which #{command}`.chop.empty?
204
+ # Whether or not a stage needs to be run
205
+ def run_stage?(stage)
206
+ respond_to?(:"before_#{stage}") || respond_to?(stage)
135
207
  end
136
208
 
137
- def self.pip(name, command = name, version = nil)
138
- if version
139
- puts "pip install --user #{name}==#{version}"
140
- context.shell("pip uninstall --user -y #{name}") unless `which #{command}`.chop.empty?
141
- context.shell("pip install --user #{name}==#{version}", retry: true)
142
- else
143
- puts "pip install --user #{name}"
144
- context.shell("pip install --user #{name}", retry: true) if `which #{command}`.chop.empty?
209
+ def finish?
210
+ stage.size == STAGES.size
211
+ end
212
+
213
+ # Runs a single stage.
214
+ #
215
+ # For each stage the base class has the opportunity to implement a `before`
216
+ # stage method, in order to apply default behaviour. Provider implementors
217
+ # are asked to not overwrite these methods.
218
+ #
219
+ # Any log output from both the before stage and stage method is going to be
220
+ # folded in the resulting build log.
221
+ def run_stage(stage, opts = {})
222
+ fold(stage, opts) do
223
+ send(:"before_#{stage}") if respond_to?(:"before_#{stage}")
224
+ send(stage) if respond_to?(stage)
145
225
  end
146
- context.shell("export PATH=$PATH:$HOME/.local/bin")
147
226
  end
148
227
 
149
- def self.npm_g(name, command = name)
150
- context.shell("npm install -g #{name}", retry: true) if `which #{command}`.chop.empty?
228
+ # Initialize the deployment process.
229
+ #
230
+ # This will:
231
+ #
232
+ # * Displays warning messages about the provider's maturity status, and deprecated
233
+ # options used.
234
+ # * Setup a ~/.dpl working directory
235
+ # * Move files out of the way that have been declared as such
236
+ def before_init
237
+ warn status.msg if status && status.announce?
238
+ deprecations.each { |(key, msg)| ctx.deprecate_opt(key, msg) }
239
+ setup_dpl_dir
240
+ move_files(ctx)
151
241
  end
152
242
 
153
- def self.class_of(filename)
154
- GEM_NAME_OF.keys.detect { |p| p.to_s.downcase == filename }
243
+ # Install APT, NPM, and Python dependencies as declared by the provider.
244
+ def before_install
245
+ validate_runtimes(ctx)
246
+ return unless install_deps?
247
+ info :before_install
248
+ install_deps(ctx)
155
249
  end
156
250
 
157
- attr_reader :context, :options
251
+ # Sets the build environment up for the deployment.
252
+ #
253
+ # This will:
254
+ #
255
+ # * Setup a ~/.dpl working directory
256
+ # * Create a temporary, per build SSH key, and call `add_key` if the feature `ssh_key` has been declared as required.
257
+ # * Setup git config (email and user name) if the feature `git` has been declared as required.
258
+ # * Either set or unset the environment variable `GIT_HTTP_USER_AGENT` depending if the feature `git_http_user_agent` has been declared as required.
259
+ def before_setup
260
+ info :before_setup
261
+ setup_ssh_key if needs?(:ssh_key)
262
+ setup_git_config if needs?(:git)
263
+ setup_git_http_user_agent
264
+ end
158
265
 
159
- def initialize(context, options)
160
- @context, @options = context, options
161
- if options.key?(:needs_git_http_user_agent) && !options[:needs_git_http_user_agent]
162
- context.env.delete 'GIT_HTTP_USER_AGENT'
163
- else
164
- context.env['GIT_HTTP_USER_AGENT'] = user_agent(git: `git --version`[/[\d\.]+/])
266
+ # Prepares the deployment by cleaning up the working directory.
267
+ #
268
+ # @see Provider#cleanup
269
+ def before_prepare
270
+ cleanup if cleanup?
271
+ end
272
+
273
+ # Runs each command as given by the user using the `--run` option.
274
+ #
275
+ # For a command that matches `restart` the method `restart` will be called
276
+ # (which can be overwritten by providers, e.g. in order to restart service
277
+ # instances).
278
+ #
279
+ # All other commands will be passed to the method `run_cmd`. By default this
280
+ # will be run as a shell command locally, but providers can choose to
281
+ # overwrite this method in order to run the command on a remote machine.
282
+ def run_cmds
283
+ Array(opts[:run]).each do |cmd|
284
+ cmd.downcase == 'restart' ? restart : run_cmd(cmd)
165
285
  end
166
286
  end
167
287
 
168
- def user_agent(*strings)
169
- strings.unshift "dpl/#{DPL::VERSION}"
170
- strings.unshift "travis/0.1.0" if context.env['TRAVIS']
171
- strings = strings.flat_map { |e| Hash === e ? e.map { |k,v| "#{k}/#{v}" } : e }
172
- strings.join(" ").gsub(/\s+/, " ").strip
288
+ def run_cmd(cmd)
289
+ cmd.downcase == 'restart' ? restart : shell(cmd)
173
290
  end
174
291
 
175
- def option(name, *alternatives)
176
- options.fetch(name) do
177
- alternatives.any? ? option(*alternatives) : raise(Error, "missing #{name}")
178
- end
292
+ # Finalizes the deployment process.
293
+ #
294
+ # This will:
295
+ #
296
+ # * Call the method `remove_key` if implemented by the provider, and if the
297
+ # feature `ssh_key` has been declared as required.
298
+ # * Revert the cleanup process, i.e. restore files moved out of the way
299
+ # during `cleanup`.
300
+ # * Remove the temporary directory `~/.dpl`
301
+ def before_finish
302
+ remove_key if needs?(:ssh_key) && respond_to?(:remove_key)
303
+ uncleanup if cleanup?
304
+ unmove_files(ctx)
305
+ remove_dpl_dir
179
306
  end
180
307
 
181
- def deploy
182
- setup_git_credentials
183
- rm_rf ".dpl"
184
- mkdir_p ".dpl"
308
+ # Resets the current working directory to the commited state.
309
+ #
310
+ # Cleanup will use `git stash --all` in order to reset the working
311
+ # directory to the committed state, and cleanup any left over artifacts
312
+ # from the build process. Providers can use the DSL method `keep` in order
313
+ # to declare known artifacts (such as CLI tooling installed to the working
314
+ # directory) that needs to be moved out of the way and restored after the
315
+ # cleanup process.
316
+ def cleanup
317
+ info :cleanup
318
+ keep.each { |path| shell "mv ./#{path} ~/#{path}", echo: false, assert: false }
319
+ shell 'git stash --all'
320
+ keep.each { |path| shell "mv ~/#{path} ./#{path}", echo: false, assert: false }
321
+ end
185
322
 
186
- context.fold("Preparing deploy") do
187
- check_auth
188
- check_app
323
+ # Restore files that have been cleaned up.
324
+ def uncleanup
325
+ shell 'git stash pop', assert: false
326
+ end
189
327
 
190
- if needs_key?
191
- create_key(".dpl/id_rsa")
192
- setup_key(".dpl/id_rsa.pub")
193
- setup_git_ssh(".dpl/git-ssh", ".dpl/id_rsa")
194
- end
328
+ # Creates the directory `~/.dpl` as an internal working directory.
329
+ def setup_dpl_dir
330
+ rm_rf '~/.dpl'
331
+ mkdir_p '~/.dpl'
332
+ chmod 0700, '~/.dpl'
333
+ end
195
334
 
196
- cleanup
197
- end
335
+ # Remove the internal working directory `~/.dpl`.
336
+ def remove_dpl_dir
337
+ rm_rf '~/.dpl'
338
+ end
198
339
 
199
- context.fold("Deploying application") { push_app }
340
+ # Creates an SSH key, and sets up git-ssh if needed.
341
+ #
342
+ # This will:
343
+ #
344
+ # * Create a temporary, per build SSH key.
345
+ # * Setup a `git-ssh` executable to use that key.
346
+ # * Call the method `add_key` if implemented by the provider.
347
+ def setup_ssh_key
348
+ ssh_keygen(key_name, '~/.dpl/id_rsa')
349
+ setup_git_ssh('~/.dpl/id_rsa')
350
+ add_key('~/.dpl/id_rsa.pub') if respond_to?(:add_key)
351
+ end
200
352
 
201
- Array(options[:run]).each do |command|
202
- if command == 'restart'
203
- context.fold("Restarting application") { restart }
204
- else
205
- context.fold("Running %p" % command) { run(command) }
206
- end
207
- end
208
- ensure
209
- if needs_key?
210
- remove_key rescue nil
211
- end
212
- uncleanup
353
+ # Setup git config
354
+ #
355
+ # This adds the current user's name and email address (as user@localhost)
356
+ # to the git config.
357
+ def setup_git_config
358
+ shell "git config user.email >/dev/null 2>/dev/null || git config user.email `whoami`@localhost", echo: false, assert: false
359
+ shell "git config user.name >/dev/null 2>/dev/null || git config user.name `whoami`", echo: false, assert: false
213
360
  end
214
361
 
215
- def sha
216
- @sha ||= context.env['TRAVIS_COMMIT'] || `git rev-parse HEAD`.strip
362
+ # Sets up `git-ssh` and the GIT_SSH env var
363
+ def setup_git_ssh(key)
364
+ info :setup_git_ssh
365
+ path, conf = '~/.dpl/git-ssh', asset(:dpl, :git_ssh).read % expand(key)
366
+ open(path, 'w+') { |file| file.write(conf) }
367
+ chmod(0740, path)
368
+ ENV['GIT_SSH'] = expand(path)
217
369
  end
218
370
 
219
- def commit_msg
220
- @commit_msg ||= %x{git log #{sha} -n 1 --pretty=%B}.strip
371
+ # Generates an SSH key.
372
+ def ssh_keygen(key, path)
373
+ info :ssh_keygen
374
+ ctx.ssh_keygen(key, expand(path))
221
375
  end
222
376
 
223
- def cleanup
224
- return if options[:skip_cleanup]
225
- context.shell "mv .dpl ~/dpl"
226
- log "Cleaning up git repository with `git stash --all`. " \
227
- "If you need build artifacts for deployment, set `deploy.skip_cleanup: true`. " \
228
- "See https://docs.travis-ci.com/user/deployment#Uploading-Files-and-skip_cleanup."
229
- context.shell "git stash --all"
230
- context.shell "mv ~/dpl .dpl"
377
+ # Sets or unsets the environment variable `GIT_HTTP_USER_AGENT`.
378
+ def setup_git_http_user_agent
379
+ return ENV.delete('GIT_HTTP_USER_AGENT') unless needs?(:git_http_user_agent)
380
+ info :setup_git_ua
381
+ ENV['GIT_HTTP_USER_AGENT'] = user_agent(git: `git --version`[/[\d\.]+/])
231
382
  end
232
383
 
233
- def uncleanup
234
- return if options[:skip_cleanup]
235
- context.shell "git stash pop"
384
+ # Waits for SSH access on the given host and port.
385
+ #
386
+ # This will try to connect to the given SSH host and port, and keep
387
+ # retrying 30 times, waiting a second inbetween retries.
388
+ def wait_for_ssh_access(host, port)
389
+ info :ssh_remote_host, host, port
390
+ 1.upto(20) { try_ssh_access(host, port) && break || sleep(3) }
391
+ success? ? info(:ssh_connected) : error(:ssh_failed)
236
392
  end
237
393
 
238
- def needs_key?
239
- true
394
+ # Tries to connect to the given SSH host and port.
395
+ def try_ssh_access(host, port)
396
+ info :ssh_try_connect
397
+ shell "#{ENV['GIT_SSH']} #{host} -p #{port} 2>&1 | grep -c 'PTY allocation request failed' > /dev/null", echo: false, assert: false
240
398
  end
241
399
 
242
- def check_app
400
+ # Creates a log fold.
401
+ #
402
+ # Folds any log output from the given block into a fold with the given
403
+ # name.
404
+ def fold(name, opts = {}, &block)
405
+ return yield unless fold?(name, opts)
406
+ title = FOLDS[name] || "deploy.#{name}"
407
+ ctx.fold(title, &block)
243
408
  end
244
409
 
245
- def create_key(file)
246
- context.shell "ssh-keygen -t rsa -N \"\" -C #{option(:key_name)} -f #{file}"
410
+ # Checks if the given stage needs to be folded.
411
+ #
412
+ # Depends on the option `--fold`, also omits folds for the init and finish
413
+ # stages. Can be overwritten by passing `fold: false`.
414
+ def fold?(name, opts = {})
415
+ !opts[:fold].is_a?(FalseClass) && super() && !%i(init).include?(name)
247
416
  end
248
417
 
249
- def setup_git_credentials
250
- context.shell "git config user.email >/dev/null 2>/dev/null || git config user.email `whoami`@localhost"
251
- context.shell "git config user.name >/dev/null 2>/dev/null || git config user.name `whoami`@localhost"
418
+ # Runs a script as a shell command.
419
+ #
420
+ # Scripts can be stored as separate files (assets) in the directory
421
+ # `lib/dpl/assets/[provider]`.
422
+ #
423
+ # This is meant for large shell commands that would be hard to read if
424
+ # embedded in Ruby code. Storing them as separate files helps with proper
425
+ # syntax highlighting etc in editors, and allows to execute them for
426
+ # testing purposes.
427
+ #
428
+ # Scripts can have interpolation variables. See Dpl::Interpolate for
429
+ # details on interpolating variables.
430
+ #
431
+ # See Ctx::Bash#shell for details on the options accepted.
432
+ def script(name, opts = {})
433
+ opts[:assert] = name if opts[:assert].is_a?(TrueClass)
434
+ shell(asset(name).read, opts.merge(echo: false))
252
435
  end
253
436
 
254
- def setup_git_ssh(path, key_path)
255
- key_path = File.expand_path(key_path)
256
- path = File.expand_path(path)
437
+ # Runs a single shell command.
438
+ #
439
+ # Shell commands can have interpolation variables. See Dpl::Interpolate for
440
+ # details on interpolating variables.
441
+ #
442
+ # See Ctx::Bash#shell for details on the options accepted.
443
+ def shell(cmd, *args)
444
+ opts = args.last.is_a?(Hash) ? args.pop : {}
445
+ cmd = Cmd.new(self, cmd, opts)
446
+ ctx.shell(cmd)
447
+ end
257
448
 
258
- File.open(path, 'w') do |file|
259
- file.write "#!/bin/sh\n"
260
- file.write "exec ssh -o StrictHostKeychecking=no -o CheckHostIP=no -o UserKnownHostsFile=/dev/null -i #{key_path} -- \"$@\"\n"
449
+ # @!method print
450
+ # Prints a partial message to stdout
451
+ #
452
+ # This method does not append a newline character to the given message,
453
+ # which usually is not the desired behaviour. The method is intended to be
454
+ # used if an initial, partial message is supposed to be printed, which will
455
+ # be completed later (using the method `info`).
456
+ #
457
+ # For example:
458
+ #
459
+ # print 'Starting a long running task ...'
460
+ # run_long_running_task
461
+ # info 'done.'
462
+ #
463
+ # Messages support interpolation variables. See Dpl::Interpolate for
464
+ # details on interpolating variables.
465
+
466
+ # @!method info
467
+ # Outputs an info message to stdout
468
+ #
469
+ # This method is intended to be used for default, info level messages that
470
+ # are supposed to show up in the build log.
471
+ #
472
+ # @!method warn
473
+ # Outputs an warning message to stderr
474
+ #
475
+ # This method is intended to be used for warning messages that are supposed
476
+ # to show up in the build log, but do not qualify as errors that would
477
+ # abort the deployment process. The warning will be highlighted as red
478
+ # text. Use sparingly.
479
+ #
480
+ # Messages support interpolation variables. See Dpl::Interpolate for
481
+ # details on interpolating variables.
482
+
483
+ # @!method error
484
+ # Outputs an error message to stderr, and raises an error, halting the
485
+ # deployment process.
486
+ #
487
+ # This method is intended to be used for all error conditions that require
488
+ # the deployment process to be aborted.
489
+ #
490
+ # Messages support interpolation variables. See Dpl::Interpolate for
491
+ # details on interpolating variables.
492
+ %i(print info warn error).each do |level|
493
+ define_method(level) do |msg, *args|
494
+ msg = interpolate(self.msg(msg), args) if msg.is_a?(Symbol)
495
+ ctx.send(level, msg)
261
496
  end
497
+ end
262
498
 
263
- chmod(0740, path)
264
- context.env['GIT_SSH'] = path
499
+ # @!method cmd
500
+ # Looks up a shell command from the commands declared by the provider
501
+ # (using the class level DSL).
502
+ #
503
+ # Not usually useful to be used by provider implementors directly. Use the
504
+ # method `shell` in order to execute shell commands.
505
+
506
+ # @!method err
507
+ # Looks up an error message from the error messages declared by the
508
+ # provider (using the class level DSL), as needed by the option `assert`
509
+ # when passed to the method `shell`.
510
+
511
+ # @!method msg
512
+ # Looks up a message from the messages declared by the provider (using the
513
+ # class level DSL).
514
+ #
515
+ # For example, a message declared on the class body like so:
516
+ #
517
+ # ```ruby
518
+ # msgs commit_msg: 'Commit build artifacts on build %{build_number}'
519
+ # ```
520
+ #
521
+ # could be used by the implementation like so:
522
+ #
523
+ # ```ruby
524
+ # def commit_msg
525
+ # interpolate(msg(:commit_msg))
526
+ # end
527
+ # ```
528
+ #
529
+ # Note that the the method `interpolate` needs to be used in order to
530
+ # interpolate variables used in a message (if any).
531
+ %i(cmd err msg str).each do |name|
532
+ define_method(name) do |*keys|
533
+ key = keys.detect { |key| key.is_a?(Symbol) }
534
+ self.class.send(:"#{name}s")[key] if key
535
+ end
265
536
  end
266
537
 
267
- def detect_encoding?
268
- options[:detect_encoding]
538
+ # Escapes the given string so it can be safely used in Bash.
539
+ def escape(str)
540
+ Shellwords.escape(str)
269
541
  end
270
542
 
271
- def default_text_charset?
272
- options[:default_text_charset]
543
+ # Double quotes the given string.
544
+ def quote(str)
545
+ %("#{str.gsub('"', '\"')}")
273
546
  end
274
547
 
275
- def default_text_charset
276
- options[:default_text_charset].downcase
548
+ # Outdents the given string.
549
+ #
550
+ # @see Dpl::Squiggle
551
+ def sq(str)
552
+ self.class.sq(str)
277
553
  end
278
554
 
279
- def install_deploy_dependencies
555
+ # Generate shell option strings to be passed to a shell command.
556
+ #
557
+ # This generates strings like `--key="value"` for the option keys passed.
558
+ # These keys are supposed to correspond to methods on the provider
559
+ # instance, which will be called in order to determine the option value.
560
+ #
561
+ # If the returned value is an array then the option will be repeated
562
+ # multiple times. If it is a String then it will be double quoted.
563
+ # Otherwise it is assumed to be a flag that does not have a value.
564
+ #
565
+ # @option prefix [String] Use this to set a single dash as an option prefix (defaults to two dashes).
566
+ # @option dashed [Boolean] Use this to dasherize the option key (rather than underscore it, defaults to underscore).
567
+ def opts_for(keys, opts = {})
568
+ strs = Array(keys).map { |key| opt_for(key, opts) if send(:"#{key}?") }.compact
569
+ strs.join(' ') if strs.any?
280
570
  end
281
571
 
282
- def encoding_for(path)
283
- file_cmd_output = `file '#{path}'`
284
- case file_cmd_output
285
- when /gzip compressed/
286
- 'gzip'
287
- when /compress'd/
288
- 'compress'
289
- when /text/
290
- 'text'
291
- when /data/
292
- # Shrugs?
572
+ def opt_for(key, opts = {})
573
+ case value = send(key)
574
+ when String then "#{opt_key(key, opts)}=#{value.inspect}"
575
+ when Array then value.map { |value| "#{opt_key(key, opts)}=#{value.inspect}" }
576
+ else opt_key(key, opts)
293
577
  end
294
578
  end
295
579
 
296
- def log(message)
297
- $stderr.puts(message)
580
+ def opt_key(key, opts)
581
+ "#{opts[:prefix] || '--'}#{opts[:dashed] ? key.to_s.gsub('_', '-') : key}"
298
582
  end
299
583
 
300
- def warn(message)
301
- log "\e[31;1m#{message}\e[0m"
584
+ # Compacts the given hash by rejecting nil values.
585
+ def compact(hash)
586
+ hash.reject { |_, value| value.nil? }
302
587
  end
303
588
 
304
- def run(command)
305
- error "running commands not supported"
589
+ # Returns a new hash with the given keys selected from the given hash.
590
+ def only(hash, *keys)
591
+ hash.select { |key, _| keys.include?(key) }
306
592
  end
307
593
 
308
- def error(message)
309
- raise Error, message
594
+ # Deep symbolizes the given hash's keys
595
+ def symbolize(obj)
596
+ case obj
597
+ when Hash
598
+ obj.map { |key, obj| [key.to_sym, symbolize(obj)] }.to_h
599
+ when Array
600
+ obj.map { |obj| symbolize(obj) }
601
+ else
602
+ obj
603
+ end
604
+ end
605
+
606
+ def file?(path)
607
+ File.file?(expand(path))
608
+ end
609
+
610
+ def mkdir_p(path)
611
+ FileUtils.mkdir_p(expand(path))
612
+ end
613
+
614
+ def chmod(perm, path)
615
+ super(perm, expand(path))
616
+ end
617
+
618
+ def mv(src, dest)
619
+ super(expand(src), expand(dest))
620
+ end
621
+
622
+ def rm_rf(path)
623
+ super(expand(path))
624
+ end
625
+
626
+ def open(path, *args, &block)
627
+ File.open(expand(path), *args, &block)
628
+ end
629
+
630
+ def read(path)
631
+ File.read(expand(path))
632
+ end
633
+
634
+ def expand(*args)
635
+ File.expand_path(*args)
310
636
  end
311
637
  end
312
638
  end
639
+
640
+ require 'dpl/providers'