r10k 3.10.0 → 3.13.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (93) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +0 -10
  3. data/CHANGELOG.mkd +33 -0
  4. data/README.mkd +6 -0
  5. data/doc/dynamic-environments/configuration.mkd +25 -7
  6. data/doc/dynamic-environments/usage.mkd +26 -0
  7. data/doc/puppetfile.mkd +18 -5
  8. data/integration/Rakefile +2 -0
  9. data/integration/tests/basic_functionality/basic_deployment.rb +176 -0
  10. data/integration/tests/user_scenario/basic_workflow/single_env_purge_unmanaged_modules.rb +15 -13
  11. data/integration/tests/user_scenario/complex_workflow/multi_env_add_change_remove.rb +3 -3
  12. data/integration/tests/user_scenario/complex_workflow/multi_env_remove_re-add.rb +3 -3
  13. data/integration/tests/user_scenario/complex_workflow/multi_env_unamanaged.rb +3 -3
  14. data/lib/r10k/action/base.rb +1 -1
  15. data/lib/r10k/action/deploy/deploy_helpers.rb +4 -0
  16. data/lib/r10k/action/deploy/display.rb +1 -1
  17. data/lib/r10k/action/deploy/environment.rb +19 -9
  18. data/lib/r10k/action/deploy/module.rb +41 -11
  19. data/lib/r10k/action/puppetfile/check.rb +7 -5
  20. data/lib/r10k/action/puppetfile/install.rb +22 -16
  21. data/lib/r10k/action/puppetfile/purge.rb +12 -9
  22. data/lib/r10k/action/runner.rb +40 -4
  23. data/lib/r10k/action/visitor.rb +3 -0
  24. data/lib/r10k/cli/deploy.rb +5 -0
  25. data/lib/r10k/cli/puppetfile.rb +0 -1
  26. data/lib/r10k/content_synchronizer.rb +16 -4
  27. data/lib/r10k/environment/bare.rb +4 -7
  28. data/lib/r10k/environment/base.rb +64 -11
  29. data/lib/r10k/environment/plain.rb +16 -0
  30. data/lib/r10k/environment/with_modules.rb +6 -10
  31. data/lib/r10k/environment.rb +1 -0
  32. data/lib/r10k/errors.rb +5 -0
  33. data/lib/r10k/git/rugged/credentials.rb +77 -0
  34. data/lib/r10k/git/stateful_repository.rb +8 -0
  35. data/lib/r10k/git.rb +3 -0
  36. data/lib/r10k/initializers.rb +14 -7
  37. data/lib/r10k/logging.rb +78 -1
  38. data/lib/r10k/module/base.rb +42 -1
  39. data/lib/r10k/module/definition.rb +64 -0
  40. data/lib/r10k/module/forge.rb +11 -2
  41. data/lib/r10k/module/git.rb +23 -1
  42. data/lib/r10k/module/local.rb +6 -3
  43. data/lib/r10k/module/svn.rb +11 -0
  44. data/lib/r10k/module.rb +20 -2
  45. data/lib/r10k/module_loader/puppetfile/dsl.rb +8 -3
  46. data/lib/r10k/module_loader/puppetfile.rb +109 -28
  47. data/lib/r10k/puppetfile.rb +11 -13
  48. data/lib/r10k/settings/definition.rb +1 -1
  49. data/lib/r10k/settings.rb +89 -1
  50. data/lib/r10k/source/yaml.rb +1 -1
  51. data/lib/r10k/util/purgeable.rb +6 -2
  52. data/lib/r10k/util/setopts.rb +2 -0
  53. data/lib/r10k/util/subprocess.rb +1 -0
  54. data/lib/r10k/version.rb +1 -1
  55. data/locales/r10k.pot +168 -68
  56. data/r10k.gemspec +2 -0
  57. data/r10k.yaml.example +28 -0
  58. data/spec/fixtures/unit/action/r10k_logging.yaml +12 -0
  59. data/spec/fixtures/unit/puppetfile/forge-override/Puppetfile +8 -0
  60. data/spec/fixtures/unit/puppetfile/various-modules/Puppetfile +10 -0
  61. data/spec/fixtures/unit/puppetfile/various-modules/Puppetfile.new +10 -0
  62. data/spec/fixtures/unit/util/purgeable/managed_one/managed_subdir_1/managed_symlink_file +1 -0
  63. data/spec/fixtures/unit/util/purgeable/managed_one/managed_subdir_1/{managed_subdir_2 → subdir_allowlisted_2}/ignored_1 +0 -0
  64. data/spec/fixtures/unit/util/purgeable/managed_one/managed_subdir_1/unmanaged_symlink_dir +1 -0
  65. data/spec/fixtures/unit/util/purgeable/managed_one/managed_symlink_dir +1 -0
  66. data/spec/fixtures/unit/util/purgeable/managed_one/unmanaged_symlink_file +1 -0
  67. data/spec/integration/util/purageable_spec.rb +41 -0
  68. data/spec/r10k-mocks/mock_env.rb +3 -0
  69. data/spec/r10k-mocks/mock_source.rb +7 -3
  70. data/spec/unit/action/deploy/environment_spec.rb +96 -30
  71. data/spec/unit/action/deploy/module_spec.rb +217 -51
  72. data/spec/unit/action/puppetfile/check_spec.rb +17 -5
  73. data/spec/unit/action/puppetfile/install_spec.rb +42 -36
  74. data/spec/unit/action/puppetfile/purge_spec.rb +15 -17
  75. data/spec/unit/action/runner_spec.rb +132 -9
  76. data/spec/unit/environment/bare_spec.rb +13 -0
  77. data/spec/unit/environment/base_spec.rb +30 -17
  78. data/spec/unit/environment/git_spec.rb +2 -2
  79. data/spec/unit/environment/plain_spec.rb +8 -0
  80. data/spec/unit/environment/svn_spec.rb +4 -3
  81. data/spec/unit/environment/with_modules_spec.rb +3 -2
  82. data/spec/unit/git/rugged/credentials_spec.rb +29 -0
  83. data/spec/unit/git/stateful_repository_spec.rb +5 -0
  84. data/spec/unit/module/base_spec.rb +54 -8
  85. data/spec/unit/module/forge_spec.rb +51 -4
  86. data/spec/unit/module/git_spec.rb +67 -9
  87. data/spec/unit/module/svn_spec.rb +35 -5
  88. data/spec/unit/module_loader/puppetfile_spec.rb +108 -33
  89. data/spec/unit/module_spec.rb +12 -1
  90. data/spec/unit/puppetfile_spec.rb +33 -3
  91. data/spec/unit/settings_spec.rb +43 -2
  92. data/spec/unit/util/purgeable_spec.rb +22 -11
  93. metadata +31 -3
@@ -23,6 +23,7 @@ class R10K::Environment::WithModules < R10K::Environment::Base
23
23
  def initialize(name, basedir, dirname, options = {})
24
24
  super
25
25
 
26
+ @all_modules = nil
26
27
  @managed_content = {}
27
28
  @modules = []
28
29
  @moduledir = case options[:moduledir]
@@ -43,10 +44,12 @@ class R10K::Environment::WithModules < R10K::Environment::Base
43
44
  # - The r10k environment object
44
45
  # - A Puppetfile in the environment's content
45
46
  def modules
46
- return @modules if puppetfile.nil?
47
+ if @all_modules.nil?
48
+ puppetfile_modules = super()
49
+ @all_modules = @modules + puppetfile_modules
50
+ end
47
51
 
48
- puppetfile.load unless puppetfile.loaded?
49
- @modules + puppetfile.modules
52
+ @all_modules
50
53
  end
51
54
 
52
55
  def module_conflicts?(mod_b)
@@ -126,13 +129,6 @@ class R10K::Environment::WithModules < R10K::Environment::Base
126
129
 
127
130
  include R10K::Util::Purgeable
128
131
 
129
- # Returns an array of the full paths that can be purged.
130
- # @note This implements a required method for the Purgeable mixin
131
- # @return [Array<String>]
132
- def managed_directories
133
- [@full_path]
134
- end
135
-
136
132
  # Returns an array of the full paths of filenames that should exist. Files
137
133
  # inside managed_directories that are not listed in desired_contents will
138
134
  # be purged.
@@ -30,6 +30,7 @@ module R10K
30
30
 
31
31
  require 'r10k/environment/base'
32
32
  require 'r10k/environment/with_modules'
33
+ require 'r10k/environment/plain'
33
34
  require 'r10k/environment/bare'
34
35
  require 'r10k/environment/git'
35
36
  require 'r10k/environment/svn'
data/lib/r10k/errors.rb CHANGED
@@ -58,4 +58,9 @@ module R10K
58
58
  str.gsub(/^/, prefix)
59
59
  end
60
60
  end
61
+
62
+ # An error class for configuration errors
63
+ #
64
+ class ConfigError < Error
65
+ end
61
66
  end
@@ -1,6 +1,10 @@
1
1
  require 'r10k/git/rugged'
2
2
  require 'r10k/git/errors'
3
3
  require 'r10k/logging'
4
+ require 'json'
5
+ require 'jwt'
6
+ require 'net/http'
7
+ require 'openssl'
4
8
 
5
9
  # Generate credentials for secured remote connections.
6
10
  #
@@ -62,15 +66,29 @@ class R10K::Git::Rugged::Credentials
62
66
 
63
67
  def get_plaintext_credentials(url, username_from_url)
64
68
  per_repo_oauth_token = nil
69
+ per_repo_github_app_id = nil
70
+ per_repo_github_app_key = nil
71
+ per_repo_github_app_ttl = nil
72
+
65
73
  if per_repo_settings = R10K::Git.get_repo_settings(url)
66
74
  per_repo_oauth_token = per_repo_settings[:oauth_token]
75
+ per_repo_github_app_id = per_repo_settings[:github_app_id]
76
+ per_repo_github_app_key = per_repo_settings[:github_app_key]
77
+ per_repo_github_app_ttl = per_repo_settings[:github_app_ttl]
67
78
  end
68
79
 
80
+ app_id = per_repo_github_app_id || R10K::Git.settings[:github_app_id]
81
+ app_key = per_repo_github_app_key || R10K::Git.settings[:github_app_key]
82
+ app_ttl = per_repo_github_app_ttl || R10K::Git.settings[:github_app_ttl]
83
+
69
84
  if token_path = per_repo_oauth_token || R10K::Git.settings[:oauth_token]
70
85
  @oauth_token ||= extract_token(token_path, url)
71
86
 
72
87
  user = 'x-oauth-token'
73
88
  password = @oauth_token
89
+ elsif app_id && app_key && app_ttl
90
+ user = 'x-access-token'
91
+ password = github_app_token(app_id, app_key, app_ttl)
74
92
  else
75
93
  user = get_git_username(url, username_from_url)
76
94
  password = URI.parse(url).password || ''
@@ -125,4 +143,63 @@ class R10K::Git::Rugged::Credentials
125
143
 
126
144
  user
127
145
  end
146
+
147
+ def github_app_token(app_id, private_key, ttl)
148
+ raise R10K::Git::GitError, _('Github App id contains invalid characters.') unless app_id =~ /^\d+$/
149
+ raise R10K::Git::GitError, _('Github App token ttl contains invalid characters.') unless ttl =~ /^\d+$/
150
+ raise R10K::Git::GitError, _('Github App key is missing or unreadable') unless File.readable?(private_key)
151
+
152
+ begin
153
+ ssl_key = OpenSSL::PKey::RSA.new(File.read(private_key).strip)
154
+ unless ssl_key.private?
155
+ raise R10K::Git::GitError, _('Github App key is not a valid SSL private key')
156
+ end
157
+ rescue OpenSSL::PKey::RSAError
158
+ raise R10K::Git::GitError, _('Github App key is not a valid SSL key')
159
+ end
160
+
161
+ logger.debug2 _("Using Github App id %{app_id} with SSL key from %{key_path}") % { key_path: private_key, app_id: app_id }
162
+
163
+ jwt_issue_time = Time.now.to_i - 60
164
+ jwt_exp_time = (jwt_issue_time + 60) + ttl.to_i
165
+ payload = { iat: jwt_issue_time, exp: jwt_exp_time, iss: app_id }
166
+ jwt = JWT.encode(payload, ssl_key, "RS256")
167
+
168
+ get = URI.parse("https://api.github.com/app/installations")
169
+ get_request = Net::HTTP::Get.new(get)
170
+ get_request["Authorization"] = "Bearer #{jwt}"
171
+ get_request["Accept"] = "application/vnd.github.v3+json"
172
+ get_req_options = { use_ssl: get.scheme == "https", }
173
+ get_response = Net::HTTP.start(get.hostname, get.port, get_req_options) do |http|
174
+ http.request(get_request)
175
+ end
176
+
177
+ unless (get_response.class < Net::HTTPSuccess)
178
+ logger.debug2 _("Unexpected response code: #{get_response.code}\nResponse body: #{get_response.body}")
179
+ raise R10K::Git::GitError, _("Error using private key to get Github App access token from url")
180
+ end
181
+
182
+ access_tokens_url = JSON.parse(get_response.body)[0]['access_tokens_url']
183
+
184
+ post = URI.parse(access_tokens_url)
185
+ post_request = Net::HTTP::Post.new(post)
186
+ post_request["Authorization"] = "Bearer #{jwt}"
187
+ post_request["Accept"] = "application/vnd.github.v3+json"
188
+ post_req_options = { use_ssl: post.scheme == "https", }
189
+ post_response = Net::HTTP.start(post.hostname, post.port, post_req_options) do |http|
190
+ http.request(post_request)
191
+ end
192
+
193
+ unless (post_response.class < Net::HTTPSuccess)
194
+ logger.debug2 _("Unexpected response code: #{post_response.code}\nResponse body: #{post_response.body}")
195
+ raise R10K::Git::GitError, _("Error using private key to generate access token from #{access_token_url}")
196
+ end
197
+
198
+ token = JSON.parse(post_response.body)['token']
199
+
200
+ raise R10K::Git::GitError, _("Github App token contains invalid characters.") unless valid_token?(token)
201
+
202
+ logger.debug2 _("Github App token generated, expires at: %{expire}") % {expire: JSON.parse(post_response.body)['expires_at']}
203
+ token
204
+ end
128
205
  end
@@ -35,6 +35,7 @@ class R10K::Git::StatefulRepository
35
35
  @cache.resolve(ref)
36
36
  end
37
37
 
38
+ # Returns true if the sync actually updated the repo, false otherwise
38
39
  def sync(ref, force=true)
39
40
  @cache.sync if sync_cache?(ref)
40
41
 
@@ -46,6 +47,7 @@ class R10K::Git::StatefulRepository
46
47
 
47
48
  workdir_status = status(ref)
48
49
 
50
+ updated = true
49
51
  case workdir_status
50
52
  when :absent
51
53
  logger.debug(_("Cloning %{repo_path} and checking out %{ref}") % {repo_path: @repo.path, ref: ref })
@@ -64,15 +66,20 @@ class R10K::Git::StatefulRepository
64
66
  @repo.checkout(sha, {:force => force})
65
67
  else
66
68
  logger.warn(_("Skipping %{repo_path} due to local modifications") % {repo_path: @repo.path})
69
+ updated = false
67
70
  end
68
71
  else
69
72
  logger.debug(_("%{repo_path} is already at Git ref %{ref}") % {repo_path: @repo.path, ref: ref })
73
+ updated = false
70
74
  end
75
+ updated
71
76
  end
72
77
 
73
78
  def status(ref)
74
79
  if !@repo.exist?
75
80
  :absent
81
+ elsif !@cache.exist?
82
+ :mismatched
76
83
  elsif !@repo.git_dir.exist?
77
84
  :mismatched
78
85
  elsif !@repo.git_dir.directory?
@@ -93,6 +100,7 @@ class R10K::Git::StatefulRepository
93
100
  # @api private
94
101
  def sync_cache?(ref)
95
102
  return true if !@cache.exist?
103
+ return true if ref == 'HEAD'
96
104
  return true if !([:commit, :tag].include? @cache.ref_type(ref))
97
105
  return false
98
106
  end
data/lib/r10k/git.rb CHANGED
@@ -135,6 +135,9 @@ module R10K
135
135
 
136
136
  def_setting_attr :private_key
137
137
  def_setting_attr :oauth_token
138
+ def_setting_attr :github_app_id
139
+ def_setting_attr :github_app_key
140
+ def_setting_attr :github_app_ttl
138
141
  def_setting_attr :proxy
139
142
  def_setting_attr :username
140
143
  def_setting_attr :repositories, {}
@@ -30,6 +30,8 @@ module R10K
30
30
  logger.warn(_("the purgedirs key in r10k.yaml is deprecated. it is currently ignored."))
31
31
  end
32
32
 
33
+ with_setting(:logging) { |value| LoggingInitializer.new(value).call }
34
+
33
35
  with_setting(:deploy) { |value| DeployInitializer.new(value).call }
34
36
 
35
37
  with_setting(:cachedir) { |value| R10K::Git::Cache.settings[:cache_root] = value }
@@ -41,6 +43,14 @@ module R10K
41
43
  end
42
44
  end
43
45
 
46
+ class LoggingInitializer < BaseInitializer
47
+ def call
48
+ with_setting(:level) { |value| R10K::Logging.level = value }
49
+ with_setting(:disable_default_stderr) { |value| R10K::Logging.disable_default_stderr = value }
50
+ with_setting(:outputs) { |value| R10K::Logging.add_outputters(value) }
51
+ end
52
+ end
53
+
44
54
  class DeployInitializer < BaseInitializer
45
55
  def call
46
56
  with_setting(:puppet_path) { |value| R10K::Settings.puppet_path = value }
@@ -56,6 +66,9 @@ module R10K
56
66
  with_setting(:proxy) { |value| R10K::Git.settings[:proxy] = value }
57
67
  with_setting(:repositories) { |value| R10K::Git.settings[:repositories] = value }
58
68
  with_setting(:oauth_token) { |value| R10K::Git.settings[:oauth_token] = value }
69
+ with_setting(:github_app_id) { |value| R10K::Git.settings[:github_app_id] = value }
70
+ with_setting(:github_app_key) { |value| R10K::Git.settings[:github_app_key] = value }
71
+ with_setting(:github_app_ttl) { |value| R10K::Git.settings[:github_app_ttl] = value }
59
72
  end
60
73
  end
61
74
 
@@ -63,13 +76,7 @@ module R10K
63
76
  def call
64
77
  with_setting(:baseurl) { |value| PuppetForge.host = value }
65
78
  with_setting(:proxy) { |value| PuppetForge::Connection.proxy = value }
66
- with_setting(:authorization_token) { |value|
67
- if @settings[:baseurl]
68
- PuppetForge::Connection.authorization = value
69
- else
70
- raise R10K::Error, "Cannot specify a Forge authorization token without configuring a custom baseurl."
71
- end
72
- }
79
+ with_setting(:authorization_token) { |value| PuppetForge::Connection.authorization = value }
73
80
  end
74
81
  end
75
82
  end
data/lib/r10k/logging.rb CHANGED
@@ -8,6 +8,16 @@ require 'r10k/logging/terminaloutputter'
8
8
  module R10K::Logging
9
9
 
10
10
  LOG_LEVELS = %w{DEBUG2 DEBUG1 DEBUG INFO NOTICE WARN ERROR FATAL}
11
+ SYSLOG_LEVELS_MAP = {
12
+ 'DEBUG2' => 'DEBUG',
13
+ 'DEBUG1' => 'DEBUG',
14
+ 'DEBUG' => 'DEBUG',
15
+ 'INFO' => 'INFO',
16
+ 'NOTICE' => 'INFO',
17
+ 'WARN' => 'WARN',
18
+ 'ERROR' => 'ERROR',
19
+ 'FATAL' => 'FATAL',
20
+ }.freeze
11
21
 
12
22
  def logger_name
13
23
  self.class.to_s
@@ -21,6 +31,9 @@ module R10K::Logging
21
31
  else
22
32
  @logger = Log4r::Logger.new(name)
23
33
  @logger.add(R10K::Logging.outputter)
34
+ R10K::Logging.outputters.each do |output|
35
+ @logger.add(output)
36
+ end
24
37
  end
25
38
  end
26
39
  @logger
@@ -59,7 +72,7 @@ module R10K::Logging
59
72
  if level.nil?
60
73
  raise ArgumentError, _("Invalid log level '%{val}'. Valid levels are %{log_levels}") % {val: val, log_levels: LOG_LEVELS.map(&:downcase).inspect}
61
74
  end
62
- outputter.level = level
75
+ outputter.level = level unless @disable_default_stderr
63
76
  @level = level
64
77
 
65
78
  if level < Log4r::INFO
@@ -69,6 +82,58 @@ module R10K::Logging
69
82
  end
70
83
  end
71
84
 
85
+ def disable_default_stderr=(val)
86
+ @disable_default_stderr = val
87
+ outputter.level = val ? Log4r::OFF : @level
88
+ end
89
+
90
+ def add_outputters(outputs)
91
+ outputs.each do |output|
92
+ type = output.fetch(:type)
93
+ # Support specifying both short as well as full names
94
+ type = type.to_s[0..-10] if type.to_s.downcase.end_with? 'outputter'
95
+
96
+ name = output.fetch(:name, 'r10k')
97
+ if output[:level]
98
+ level = parse_level(output[:level])
99
+ if level.nil?
100
+ raise ArgumentError, _("Invalid log level '%{val}'. Valid levels are %{log_levels}") % { val: output[:level], log_levels: LOG_LEVELS.map(&:downcase).inspect }
101
+ end
102
+ else
103
+ level = self.level
104
+ end
105
+ only_at = output[:only_at]
106
+ only_at&.map! do |val|
107
+ lv = parse_level(val)
108
+ if lv.nil?
109
+ raise ArgumentError, _("Invalid log level '%{val}'. Valid levels are %{log_levels}") % { val: val, log_levels: LOG_LEVELS.map(&:downcase).inspect }
110
+ end
111
+
112
+ lv
113
+ end
114
+ parameters = output.fetch(:parameters, {}).merge({ level: level })
115
+
116
+ begin
117
+ # Try to load the outputter file if possible
118
+ require "log4r/outputter/#{type.to_s.downcase}outputter"
119
+ rescue LoadError
120
+ false
121
+ end
122
+ outputtertype = Log4r.constants
123
+ .select { |klass| klass.to_s.end_with? 'Outputter' }
124
+ .find { |klass| klass.to_s.downcase == "#{type.to_s.downcase}outputter" }
125
+ raise ArgumentError, "Unable to find a #{output[:type]} outputter." unless outputtertype
126
+
127
+ outputter = Log4r.const_get(outputtertype).new(name, parameters)
128
+ outputter.only_at(*only_at) if only_at
129
+ # Handle log4r's syslog mapping correctly
130
+ outputter.map_levels_by_name_to_syslog(SYSLOG_LEVELS_MAP) if outputter.respond_to? :map_levels_by_name_to_syslog
131
+
132
+ @outputters << outputter
133
+ Log4r::Logger.global.add outputter
134
+ end
135
+ end
136
+
72
137
  extend Forwardable
73
138
  def_delegators :@outputter, :use_color, :use_color=
74
139
 
@@ -87,6 +152,16 @@ module R10K::Logging
87
152
  # @return [Log4r::Outputter]
88
153
  attr_reader :outputter
89
154
 
155
+ # @!attribute [r] outputters
156
+ # @api private
157
+ # @return [Array[Log4r::Outputter]]
158
+ attr_reader :outputters
159
+
160
+ # @!attribute [r] disable_default_stderr
161
+ # @api private
162
+ # @return [Boolean]
163
+ attr_reader :disable_default_stderr
164
+
90
165
  def default_formatter
91
166
  Log4r::PatternFormatter.new(:pattern => '%l\t -> %m')
92
167
  end
@@ -106,4 +181,6 @@ module R10K::Logging
106
181
  @level = Log4r::WARN
107
182
  @formatter = default_formatter
108
183
  @outputter = default_outputter
184
+ @outputters = []
185
+ @disable_default_stderr = false
109
186
  end
@@ -1,9 +1,12 @@
1
1
  require 'r10k/module'
2
+ require 'r10k/logging'
2
3
  require 'puppet_forge'
3
4
 
4
5
  # This class defines a common interface for module implementations.
5
6
  class R10K::Module::Base
6
7
 
8
+ include R10K::Logging
9
+
7
10
  # @!attribute [r] title
8
11
  # @return [String] The forward slash separated owner and name of the module
9
12
  attr_reader :title
@@ -35,6 +38,10 @@ class R10K::Module::Base
35
38
  # @return [String] Where the module was sourced from. E.g., "Puppetfile"
36
39
  attr_accessor :origin
37
40
 
41
+ # @!attribute [rw] spec_deletable
42
+ # @return [Boolean] set this to true if the spec dir can be safely removed, ie in the moduledir
43
+ attr_accessor :spec_deletable
44
+
38
45
  # There's been some churn over `author` vs `owner` and `full_name` over
39
46
  # `title`, so in the short run it's easier to support both and deprecate one
40
47
  # later.
@@ -43,7 +50,7 @@ class R10K::Module::Base
43
50
 
44
51
  # @param title [String]
45
52
  # @param dirname [String]
46
- # @param args [Array]
53
+ # @param args [Hash]
47
54
  def initialize(title, dirname, args, environment=nil)
48
55
  @title = PuppetForge::V3.normalize_name(title)
49
56
  @dirname = dirname
@@ -52,6 +59,9 @@ class R10K::Module::Base
52
59
  @path = Pathname.new(File.join(@dirname, @name))
53
60
  @environment = environment
54
61
  @overrides = args.delete(:overrides) || {}
62
+ @spec_deletable = true
63
+ @exclude_spec = args.delete(:exclude_spec)
64
+ @exclude_spec = @overrides[:modules].delete(:exclude_spec) if @overrides.dig(:modules, :exclude_spec)
55
65
  @origin = 'external' # Expect Puppetfile or R10k::Environment to set this to a specific value
56
66
 
57
67
  @requested_modules = @overrides.dig(:modules, :requested_modules) || []
@@ -64,8 +74,39 @@ class R10K::Module::Base
64
74
  path.to_s
65
75
  end
66
76
 
77
+ # Delete the spec dir if @exclude_spec has been set to true and @spec_deletable is also true
78
+ def maybe_delete_spec_dir
79
+ if @exclude_spec
80
+ if @spec_deletable
81
+ delete_spec_dir
82
+ else
83
+ logger.info _("Spec dir for #{@title} will not be deleted because it is not in the moduledir")
84
+ end
85
+ end
86
+ end
87
+
88
+ # Actually remove the spec dir
89
+ def delete_spec_dir
90
+ spec_path = @path + 'spec'
91
+ if spec_path.symlink?
92
+ spec_path = spec_path.realpath
93
+ end
94
+ if spec_path.directory?
95
+ logger.debug2 _("Deleting spec data at #{spec_path}")
96
+ # Use the secure flag for the #rm_rf method to avoid security issues
97
+ # involving TOCTTOU(time of check to time of use); more details here:
98
+ # https://ruby-doc.org/stdlib-2.7.0/libdoc/fileutils/rdoc/FileUtils.html#method-c-rm_rf
99
+ # Additionally, #rm_rf also has problems in windows with with symlink targets
100
+ # also being deleted; this should be revisted if Windows becomes higher priority.
101
+ FileUtils.rm_rf(spec_path, secure: true)
102
+ else
103
+ logger.debug2 _("No spec dir detected at #{spec_path}, skipping deletion")
104
+ end
105
+ end
106
+
67
107
  # Synchronize this module with the indicated state.
68
108
  # @param [Hash] opts Deprecated
109
+ # @return [Boolean] true if the module was updated, false otherwise
69
110
  def sync(opts={})
70
111
  raise NotImplementedError
71
112
  end
@@ -0,0 +1,64 @@
1
+ require 'r10k/module'
2
+
3
+ class R10K::Module::Definition < R10K::Module::Base
4
+
5
+ attr_reader :version
6
+
7
+ def initialize(name, dirname:, args:, implementation:, environment: nil)
8
+ @original_name = name
9
+ @original_args = args.dup
10
+ @implementation = implementation
11
+ @version = implementation.statically_defined_version(name, args)
12
+
13
+ super(name, dirname, args, environment)
14
+ end
15
+
16
+ def to_implementation
17
+ mod = @implementation.new(@title, @dirname, @original_args, @environment)
18
+
19
+ mod.origin = origin
20
+ mod.spec_deletable = spec_deletable
21
+
22
+ mod
23
+ end
24
+
25
+ # syncing is a noop for module definitions
26
+ # Returns false to inidicate the module was not updated
27
+ def sync(args = {})
28
+ logger.debug1(_("Not updating module %{name}, assuming content unchanged") % {name: name})
29
+ false
30
+ end
31
+
32
+ def status
33
+ :insync
34
+ end
35
+
36
+ def properties
37
+ type = nil
38
+
39
+ if @args[:type]
40
+ type = @args[:type]
41
+ elsif @args[:ref] || @args[:commit] || @args[:branch] || @args[:tag]
42
+ type = 'git'
43
+ elsif @args[:svn]
44
+ # This logic is clear and included for completeness sake, though at
45
+ # this time module definitions do not support SVN versions.
46
+ type = 'svn'
47
+ else
48
+ type = 'forge'
49
+ end
50
+
51
+ {
52
+ expected: version,
53
+ # We can't get the value for `actual` here because that requires the
54
+ # implementation (and potentially expensive operations by the
55
+ # implementation). Some consumers will check this value, if it exists
56
+ # and if not, fall back to the expected version. That is the correct
57
+ # behavior when assuming modules are unchanged, and why `actual` is set
58
+ # to `nil` here.
59
+ actual: nil,
60
+ type: type
61
+ }
62
+ end
63
+ end
64
+
@@ -25,6 +25,10 @@ class R10K::Module::Forge < R10K::Module::Base
25
25
  expected_version == :latest || expected_version.nil? || PuppetForge::Util.version_valid?(expected_version)
26
26
  end
27
27
 
28
+ def self.statically_defined_version(name, args)
29
+ args[:version] if args[:version].is_a?(String)
30
+ end
31
+
28
32
  # @!attribute [r] metadata
29
33
  # @api private
30
34
  # @return [PuppetForge::Metadata]
@@ -35,8 +39,6 @@ class R10K::Module::Forge < R10K::Module::Base
35
39
  # @return [PuppetForge::V3::Module] The Puppet Forge module metadata
36
40
  attr_reader :v3_module
37
41
 
38
- include R10K::Logging
39
-
40
42
  include R10K::Util::Setopts
41
43
 
42
44
  def initialize(title, dirname, opts, environment=nil)
@@ -58,17 +60,24 @@ class R10K::Module::Forge < R10K::Module::Base
58
60
  end
59
61
 
60
62
  # @param [Hash] opts Deprecated
63
+ # @return [Boolean] true if the module was updated, false otherwise
61
64
  def sync(opts={})
65
+ updated = false
62
66
  if should_sync?
63
67
  case status
64
68
  when :absent
65
69
  install
70
+ updated = true
66
71
  when :outdated
67
72
  upgrade
73
+ updated = true
68
74
  when :mismatched
69
75
  reinstall
76
+ updated = true
70
77
  end
78
+ maybe_delete_spec_dir
71
79
  end
80
+ updated
72
81
  end
73
82
 
74
83
  def properties
@@ -13,6 +13,21 @@ class R10K::Module::Git < R10K::Module::Base
13
13
  false
14
14
  end
15
15
 
16
+ # Will be called if self.implement? above returns true. Will return
17
+ # the version info, if version is statically defined in the modules
18
+ # declaration.
19
+ def self.statically_defined_version(name, args)
20
+ if !args[:type] && (args[:ref] || args[:tag] || args[:commit])
21
+ if args[:ref] && args[:ref].to_s.match(/[0-9a-f]{40}/)
22
+ args[:ref]
23
+ else
24
+ args[:tag] || args[:commit]
25
+ end
26
+ elsif args[:type].to_s == 'git' && args[:version] && args[:version].to_s.match(/[0-9a-f]{40}/)
27
+ args[:version]
28
+ end
29
+ end
30
+
16
31
  # @!attribute [r] repo
17
32
  # @api private
18
33
  # @return [R10K::Git::StatefulRepository]
@@ -83,9 +98,16 @@ class R10K::Module::Git < R10K::Module::Base
83
98
  end
84
99
 
85
100
  # @param [Hash] opts Deprecated
101
+ # @return [Boolean] true if the module was updated, false otherwise
86
102
  def sync(opts={})
87
103
  force = opts[:force] || @force
88
- @repo.sync(version, force) if should_sync?
104
+ if should_sync?
105
+ updated = @repo.sync(version, force)
106
+ else
107
+ updated = false
108
+ end
109
+ maybe_delete_spec_dir
110
+ updated
89
111
  end
90
112
 
91
113
  def status
@@ -1,5 +1,4 @@
1
1
  require 'r10k/module'
2
- require 'r10k/logging'
3
2
 
4
3
  # A dummy module type that can be used to "protect" Puppet modules that exist
5
4
  # inside of the Puppetfile "moduledir" location. Local modules will not be
@@ -12,10 +11,12 @@ class R10K::Module::Local < R10K::Module::Base
12
11
  args.is_a?(Hash) && (args[:local] || args[:type].to_s == 'local')
13
12
  end
14
13
 
15
- include R10K::Logging
14
+ def self.statically_defined_version(*)
15
+ "0.0.0"
16
+ end
16
17
 
17
18
  def version
18
- "0.0.0"
19
+ self.class.statically_defined_version
19
20
  end
20
21
 
21
22
  def properties
@@ -31,7 +32,9 @@ class R10K::Module::Local < R10K::Module::Base
31
32
  end
32
33
 
33
34
  # @param [Hash] opts Deprecated
35
+ # @return [Boolean] false, because local modules are always considered in-sync
34
36
  def sync(opts={})
35
37
  logger.debug1 _("Module %{title} is a local module, always indicating synced.") % {title: title}
38
+ false
36
39
  end
37
40
  end
@@ -10,6 +10,10 @@ class R10K::Module::SVN < R10K::Module::Base
10
10
  args.has_key?(:svn) || args[:type].to_s == 'svn'
11
11
  end
12
12
 
13
+ def self.statically_defined_version(name, args)
14
+ nil
15
+ end
16
+
13
17
  # @!attribute [r] expected_revision
14
18
  # @return [String] The SVN revision that the repo should have checked out
15
19
  attr_reader :expected_revision
@@ -70,17 +74,24 @@ class R10K::Module::SVN < R10K::Module::Base
70
74
  end
71
75
 
72
76
  # @param [Hash] opts Deprecated
77
+ # @return [Boolean] true if the module was updated, false otherwise
73
78
  def sync(opts={})
79
+ updated = false
74
80
  if should_sync?
75
81
  case status
76
82
  when :absent
77
83
  install
84
+ updated = true
78
85
  when :mismatched
79
86
  reinstall
87
+ updated = true
80
88
  when :outdated
81
89
  update
90
+ updated = true
82
91
  end
92
+ maybe_delete_spec_dir
83
93
  end
94
+ updated
84
95
  end
85
96
 
86
97
  def exist?