r10k 3.9.0 → 3.10.0

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 (65) hide show
  1. checksums.yaml +4 -4
  2. data/.github/pull_request_template.md +1 -1
  3. data/.github/workflows/rspec_tests.yml +1 -1
  4. data/.github/workflows/stale.yml +19 -0
  5. data/CHANGELOG.mkd +24 -0
  6. data/doc/dynamic-environments/configuration.mkd +13 -6
  7. data/integration/Rakefile +1 -1
  8. data/integration/tests/user_scenario/basic_workflow/negative/neg_specify_deleted_forge_module.rb +3 -9
  9. data/integration/tests/user_scenario/basic_workflow/single_env_purge_unmanaged_modules.rb +8 -14
  10. data/lib/r10k/action/base.rb +10 -0
  11. data/lib/r10k/action/deploy/display.rb +42 -9
  12. data/lib/r10k/action/deploy/environment.rb +70 -41
  13. data/lib/r10k/action/deploy/module.rb +51 -29
  14. data/lib/r10k/action/puppetfile/check.rb +3 -1
  15. data/lib/r10k/action/puppetfile/install.rb +20 -23
  16. data/lib/r10k/action/puppetfile/purge.rb +8 -2
  17. data/lib/r10k/action/runner.rb +11 -6
  18. data/lib/r10k/content_synchronizer.rb +83 -0
  19. data/lib/r10k/deployment.rb +1 -1
  20. data/lib/r10k/environment/base.rb +21 -1
  21. data/lib/r10k/environment/git.rb +0 -3
  22. data/lib/r10k/environment/svn.rb +4 -6
  23. data/lib/r10k/environment/with_modules.rb +18 -10
  24. data/lib/r10k/git/cache.rb +1 -1
  25. data/lib/r10k/initializers.rb +7 -0
  26. data/lib/r10k/module.rb +1 -1
  27. data/lib/r10k/module/base.rb +17 -1
  28. data/lib/r10k/module/forge.rb +29 -19
  29. data/lib/r10k/module/git.rb +23 -14
  30. data/lib/r10k/module/local.rb +1 -0
  31. data/lib/r10k/module/svn.rb +12 -9
  32. data/lib/r10k/module_loader/puppetfile.rb +195 -0
  33. data/lib/r10k/module_loader/puppetfile/dsl.rb +37 -0
  34. data/lib/r10k/puppetfile.rb +111 -202
  35. data/lib/r10k/settings.rb +3 -0
  36. data/lib/r10k/source/base.rb +14 -0
  37. data/lib/r10k/source/git.rb +19 -6
  38. data/lib/r10k/source/hash.rb +1 -3
  39. data/lib/r10k/source/svn.rb +4 -2
  40. data/lib/r10k/util/cleaner.rb +21 -0
  41. data/lib/r10k/util/purgeable.rb +70 -8
  42. data/lib/r10k/version.rb +1 -1
  43. data/locales/r10k.pot +67 -71
  44. data/spec/fixtures/unit/action/r10k_forge_auth.yaml +4 -0
  45. data/spec/fixtures/unit/action/r10k_forge_auth_no_url.yaml +3 -0
  46. data/spec/fixtures/unit/util/purgeable/managed_one/managed_subdir_1/managed_subdir_2/ignored_1 +0 -0
  47. data/spec/fixtures/unit/util/purgeable/managed_two/.hidden/unmanaged_3 +0 -0
  48. data/spec/r10k-mocks/mock_source.rb +1 -1
  49. data/spec/shared-examples/puppetfile-action.rb +7 -7
  50. data/spec/unit/action/deploy/display_spec.rb +32 -6
  51. data/spec/unit/action/deploy/environment_spec.rb +85 -48
  52. data/spec/unit/action/deploy/module_spec.rb +163 -31
  53. data/spec/unit/action/puppetfile/check_spec.rb +2 -2
  54. data/spec/unit/action/puppetfile/install_spec.rb +31 -10
  55. data/spec/unit/action/puppetfile/purge_spec.rb +25 -5
  56. data/spec/unit/action/runner_spec.rb +49 -25
  57. data/spec/unit/git/cache_spec.rb +14 -0
  58. data/spec/unit/module/forge_spec.rb +23 -14
  59. data/spec/unit/module/git_spec.rb +8 -8
  60. data/spec/unit/module_loader/puppetfile_spec.rb +330 -0
  61. data/spec/unit/module_spec.rb +22 -5
  62. data/spec/unit/puppetfile_spec.rb +123 -203
  63. data/spec/unit/settings_spec.rb +6 -2
  64. data/spec/unit/util/purgeable_spec.rb +40 -14
  65. metadata +12 -2
@@ -0,0 +1,37 @@
1
+ module R10K
2
+ module ModuleLoader
3
+ class Puppetfile
4
+ class DSL
5
+ # A barebones implementation of the Puppetfile DSL
6
+ #
7
+ # @api private
8
+
9
+ def initialize(librarian)
10
+ @librarian = librarian
11
+ end
12
+
13
+ def mod(name, args = nil)
14
+ if args.is_a?(Hash)
15
+ opts = args
16
+ else
17
+ opts = { version: args }
18
+ end
19
+
20
+ @librarian.add_module(name, opts)
21
+ end
22
+
23
+ def forge(location)
24
+ @librarian.set_forge(location)
25
+ end
26
+
27
+ def moduledir(location)
28
+ @librarian.set_moduledir(location)
29
+ end
30
+
31
+ def method_missing(method, *args)
32
+ raise NoMethodError, _("unrecognized declaration '%{method}'") % {method: method}
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -3,10 +3,18 @@ require 'pathname'
3
3
  require 'r10k/module'
4
4
  require 'r10k/util/purgeable'
5
5
  require 'r10k/errors'
6
+ require 'r10k/content_synchronizer'
7
+ require 'r10k/module_loader/puppetfile/dsl'
8
+ require 'r10k/module_loader/puppetfile'
6
9
 
7
10
  module R10K
11
+
12
+ # Deprecated, use R10K::ModuleLoader::Puppetfile#load to load content,
13
+ # provide the `:modules` key of the returned Hash to
14
+ # R10K::ContentSynchronizer (either the `serial_sync` or `concurrent_sync`)
15
+ # and the remaining keys (`:managed_directories`, `:desired_contents`, and
16
+ # `:purge_exclusions`) to R10K::Util::Cleaner.
8
17
  class Puppetfile
9
- # Defines the data members of a Puppetfile
10
18
 
11
19
  include R10K::Settings::Mixin
12
20
 
@@ -18,287 +26,188 @@ class Puppetfile
18
26
  # @return [String] The URL to use for the Puppet Forge
19
27
  attr_reader :forge
20
28
 
21
- # @!attribute [r] modules
22
- # @return [Array<R10K::Module>]
23
- attr_reader :modules
24
-
25
29
  # @!attribute [r] basedir
26
30
  # @return [String] The base directory that contains the Puppetfile
27
31
  attr_reader :basedir
28
32
 
29
- # @!attribute [r] moduledir
30
- # @return [String] The directory to install the modules #{basedir}/modules
31
- attr_reader :moduledir
32
-
33
33
  # @!attrbute [r] puppetfile_path
34
34
  # @return [String] The path to the Puppetfile
35
35
  attr_reader :puppetfile_path
36
36
 
37
- # @!attribute [rw] environment
37
+ # @!attribute [r] environment
38
38
  # @return [R10K::Environment] Optional R10K::Environment that this Puppetfile belongs to.
39
- attr_accessor :environment
39
+ attr_reader :environment
40
40
 
41
41
  # @!attribute [rw] force
42
42
  # @return [Boolean] Overwrite any locally made changes
43
43
  attr_accessor :force
44
44
 
45
+ # @!attribute [r] overrides
46
+ # @return [Hash] Various settings overridden from normal configs
47
+ attr_reader :overrides
48
+
49
+ # @!attribute [r] loader
50
+ # @return [R10K::ModuleLoader::Puppetfile] The internal module loader
51
+ attr_reader :loader
52
+
45
53
  # @param [String] basedir
46
- # @param [String] moduledir The directory to install the modules, default to #{basedir}/modules
47
- # @param [String] puppetfile_path The path to the Puppetfile, default to #{basedir}/Puppetfile
48
- # @param [String] puppetfile_name The name of the Puppetfile, default to 'Puppetfile'
49
- # @param [Boolean] force Shall we overwrite locally made changes?
50
- def initialize(basedir, moduledir = nil, puppetfile_path = nil, puppetfile_name = nil, force = nil )
54
+ # @param [Hash, String, nil] options_or_moduledir The directory to install the modules or a Hash of options.
55
+ # Usage as moduledir is deprecated. Only use as options, defaults to nil
56
+ # @param [String, nil] puppetfile_path Deprecated - The path to the Puppetfile, defaults to nil
57
+ # @param [String, nil] puppetfile_name Deprecated - The name of the Puppetfile, defaults to nil
58
+ # @param [Boolean, nil] force Deprecated - Shall we overwrite locally made changes?
59
+ def initialize(basedir, options_or_moduledir = nil, deprecated_path_arg = nil, deprecated_name_arg = nil, deprecated_force_arg = nil)
51
60
  @basedir = basedir
52
- @force = force || false
53
- @moduledir = moduledir || File.join(basedir, 'modules')
54
- @puppetfile_name = puppetfile_name || 'Puppetfile'
55
- @puppetfile_path = puppetfile_path || File.join(basedir, @puppetfile_name)
61
+ if options_or_moduledir.is_a? Hash
62
+ options = options_or_moduledir
63
+ deprecated_moduledir_arg = nil
64
+ else
65
+ options = {}
66
+ deprecated_moduledir_arg = options_or_moduledir
67
+ end
68
+
69
+ @force = deprecated_force_arg || options.delete(:force) || false
70
+ @moduledir = deprecated_moduledir_arg || options.delete(:moduledir) || File.join(basedir, 'modules')
71
+ @puppetfile_name = deprecated_name_arg || options.delete(:puppetfile_name) || 'Puppetfile'
72
+ @puppetfile_path = deprecated_path_arg || options.delete(:puppetfile_path) || File.join(basedir, @puppetfile_name)
73
+ @environment = options.delete(:environment)
74
+
75
+ @overrides = options.delete(:overrides) || {}
76
+ @default_branch_override = @overrides.dig(:environments, :default_branch_override)
56
77
 
57
78
  logger.info _("Using Puppetfile '%{puppetfile}'") % {puppetfile: @puppetfile_path}
58
79
 
59
- @modules = []
60
- @managed_content = {}
61
80
  @forge = 'forgeapi.puppetlabs.com'
62
81
 
82
+ @loader = ::R10K::ModuleLoader::Puppetfile.new(
83
+ basedir: @basedir,
84
+ moduledir: @moduledir,
85
+ puppetfile: @puppetfile_path,
86
+ forge: @forge,
87
+ overrides: @overrides,
88
+ environment: @environment
89
+ )
90
+
91
+ @loaded_content = {
92
+ modules: [],
93
+ managed_directories: [],
94
+ desired_contents: [],
95
+ purge_exclusions: []
96
+ }
97
+
63
98
  @loaded = false
64
99
  end
65
100
 
101
+ # @param [String] default_branch_override The default branch to use
102
+ # instead of one specified in the module declaration, if applicable.
103
+ # Deprecated, use R10K::ModuleLoader::Puppetfile directly and pass
104
+ # the default_branch_override as an option on initialization.
66
105
  def load(default_branch_override = nil)
67
- return true if self.loaded?
68
- if File.readable? @puppetfile_path
69
- self.load!(default_branch_override)
106
+ if self.loaded?
107
+ return @loaded_content
70
108
  else
71
- logger.debug _("Puppetfile %{path} missing or unreadable") % {path: @puppetfile_path.inspect}
109
+ if !File.readable?(@puppetfile_path)
110
+ logger.debug _("Puppetfile %{path} missing or unreadable") % {path: @puppetfile_path.inspect}
111
+ else
112
+ self.load!(default_branch_override)
113
+ end
72
114
  end
73
115
  end
74
116
 
117
+ # @param [String] default_branch_override The default branch to use
118
+ # instead of one specified in the module declaration, if applicable.
119
+ # Deprecated, use R10K::ModuleLoader::Puppetfile directly and pass
120
+ # the default_branch_override as an option on initialization.
75
121
  def load!(default_branch_override = nil)
76
- @default_branch_override = default_branch_override
77
122
 
78
- dsl = R10K::Puppetfile::DSL.new(self)
79
- dsl.instance_eval(puppetfile_contents, @puppetfile_path)
123
+ if default_branch_override && (default_branch_override != @default_branch_override)
124
+ logger.warn("Mismatch between passed and initialized default branch overrides, preferring passed value.")
125
+ @loader.default_branch_override = default_branch_override
126
+ end
80
127
 
81
- validate_no_duplicate_names(@modules)
128
+ @loaded_content = @loader.load
82
129
  @loaded = true
83
- rescue SyntaxError, LoadError, ArgumentError, NameError => e
84
- raise R10K::Error.wrap(e, _("Failed to evaluate %{path}") % {path: @puppetfile_path})
130
+
131
+ @loaded_content
85
132
  end
86
133
 
87
134
  def loaded?
88
135
  @loaded
89
136
  end
90
137
 
91
- # @param [Array<String>] modules
92
- def validate_no_duplicate_names(modules)
93
- dupes = modules
94
- .group_by { |mod| mod.name }
95
- .select { |_, v| v.size > 1 }
96
- .map(&:first)
97
- unless dupes.empty?
98
- msg = _('Puppetfiles cannot contain duplicate module names.')
99
- msg += ' '
100
- msg += _("Remove the duplicates of the following modules: %{dupes}" % { dupes: dupes.join(' ') })
101
- raise R10K::Error.new(msg)
102
- end
138
+ def modules
139
+ @loaded_content[:modules]
103
140
  end
104
141
 
105
- # @param [String] forge
106
- def set_forge(forge)
107
- @forge = forge
142
+ # @see R10K::ModuleLoader::Puppetfile#add_module for upcoming signature changes
143
+ def add_module(name, args)
144
+ @loader.add_module(name, args)
108
145
  end
109
146
 
110
- # @param [String] moduledir
111
- def set_moduledir(moduledir)
112
- @moduledir = if Pathname.new(moduledir).absolute?
113
- moduledir
114
- else
115
- File.join(basedir, moduledir)
116
- end
147
+ def set_moduledir(dir)
148
+ @loader.set_moduledir(dir)
117
149
  end
118
150
 
119
- # @param [String] name
120
- # @param [*Object] args
121
- def add_module(name, args)
122
- if args.is_a?(Hash) && install_path = args.delete(:install_path)
123
- install_path = resolve_install_path(install_path)
124
- validate_install_path(install_path, name)
125
- else
126
- install_path = @moduledir
127
- end
128
-
129
- if args.is_a?(Hash) && @default_branch_override != nil
130
- args[:default_branch_override] = @default_branch_override
131
- end
132
-
133
- mod = R10K::Module.new(name, install_path, args, @environment)
134
- mod.origin = :puppetfile
135
-
136
- # Do not load modules if they would conflict with the attached
137
- # environment
138
- if environment && environment.module_conflicts?(mod)
139
- mod = nil
140
- return @modules
141
- end
151
+ def set_forge(forge)
152
+ @loader.set_forge(forge)
153
+ end
142
154
 
143
- # Keep track of all the content this Puppetfile is managing to enable purging.
144
- @managed_content[install_path] = Array.new unless @managed_content.has_key?(install_path)
145
- @managed_content[install_path] << mod.name
155
+ def moduledir
156
+ @loader.moduledir
157
+ end
146
158
 
147
- @modules << mod
159
+ def environment=(env)
160
+ @loader.environment = env
161
+ @environment = env
148
162
  end
149
163
 
150
164
  include R10K::Util::Purgeable
151
165
 
152
166
  def managed_directories
153
- self.load unless @loaded
167
+ self.load
154
168
 
155
- dirs = @managed_content.keys
156
- dirs.delete(real_basedir)
157
- dirs
169
+ @loaded_content[:managed_directories]
158
170
  end
159
171
 
160
172
  # Returns an array of the full paths to all the content being managed.
161
173
  # @note This implements a required method for the Purgeable mixin
162
174
  # @return [Array<String>]
163
175
  def desired_contents
164
- self.load unless @loaded
176
+ self.load
165
177
 
166
- @managed_content.flat_map do |install_path, modnames|
167
- modnames.collect { |name| File.join(install_path, name) }
168
- end
178
+ @loaded_content[:desired_contents]
169
179
  end
170
180
 
171
181
  def purge_exclusions
172
- exclusions = managed_directories
173
-
174
- if environment && environment.respond_to?(:desired_contents)
175
- exclusions += environment.desired_contents
176
- end
182
+ self.load
177
183
 
178
- exclusions
184
+ @loaded_content[:purge_exclusions]
179
185
  end
180
186
 
181
187
  def accept(visitor)
182
188
  pool_size = self.settings[:pool_size]
183
189
  if pool_size > 1
184
- concurrent_accept(visitor, pool_size)
190
+ R10K::ContentSynchronizer.concurrent_accept(modules, visitor, self, pool_size, logger)
185
191
  else
186
- serial_accept(visitor)
192
+ R10K::ContentSynchronizer.serial_accept(modules, visitor, self)
187
193
  end
188
194
  end
189
195
 
190
- private
191
-
192
- def serial_accept(visitor)
193
- visitor.visit(:puppetfile, self) do
194
- modules.each do |mod|
195
- mod.accept(visitor)
196
- end
197
- end
198
- end
199
-
200
- def concurrent_accept(visitor, pool_size)
201
- logger.debug _("Updating modules with %{pool_size} threads") % {pool_size: pool_size}
202
- mods_queue = modules_queue(visitor)
203
- thread_pool = pool_size.times.map { visitor_thread(visitor, mods_queue) }
204
- thread_exception = nil
205
-
206
- # If any threads raise an exception the deployment is considered a failure.
207
- # In that event clear the queue, wait for other threads to finish their
208
- # current work, then re-raise the first exception caught.
209
- begin
210
- thread_pool.each(&:join)
211
- rescue => e
212
- logger.error _("Error during concurrent deploy of a module: %{message}") % {message: e.message}
213
- mods_queue.clear
214
- thread_exception ||= e
215
- retry
216
- ensure
217
- raise thread_exception unless thread_exception.nil?
218
- end
219
- end
220
-
221
- def modules_queue(visitor)
222
- Queue.new.tap do |queue|
223
- visitor.visit(:puppetfile, self) do
224
- modules_by_cachedir = modules.group_by { |mod| mod.cachedir }
225
- modules_without_vcs_cachedir = modules_by_cachedir.delete(:none) || []
226
-
227
- modules_without_vcs_cachedir.each {|mod| queue << Array(mod) }
228
- modules_by_cachedir.values.each {|mods| queue << mods }
229
- end
230
- end
231
- end
232
- public :modules_queue
233
-
234
- def visitor_thread(visitor, mods_queue)
235
- Thread.new do
236
- begin
237
- while mods = mods_queue.pop(true) do
238
- mods.each {|mod| mod.accept(visitor) }
239
- end
240
- rescue ThreadError => e
241
- logger.debug _("Module thread %{id} exiting: %{message}") % {message: e.message, id: Thread.current.object_id}
242
- Thread.exit
243
- rescue => e
244
- Thread.main.raise(e)
245
- end
246
- end
247
- end
248
-
249
- def puppetfile_contents
250
- File.read(@puppetfile_path)
251
- end
252
-
253
- def resolve_install_path(path)
254
- pn = Pathname.new(path)
255
-
256
- unless pn.absolute?
257
- pn = Pathname.new(File.join(basedir, path))
196
+ def sync
197
+ pool_size = self.settings[:pool_size]
198
+ if pool_size > 1
199
+ R10K::ContentSynchronizer.concurrent_sync(modules, pool_size, logger)
200
+ else
201
+ R10K::ContentSynchronizer.serial_sync(modules)
258
202
  end
259
-
260
- # .cleanpath is as good as we can do without touching the filesystem.
261
- # The .realpath methods will also choke if some of the intermediate
262
- # paths are missing, even though we will create them later as needed.
263
- pn.cleanpath.to_s
264
203
  end
265
204
 
266
- def validate_install_path(path, modname)
267
- unless /^#{Regexp.escape(real_basedir)}.*/ =~ path
268
- raise R10K::Error.new("Puppetfile cannot manage content '#{modname}' outside of containing environment: #{path} is not within #{real_basedir}")
269
- end
270
-
271
- true
272
- end
205
+ private
273
206
 
274
207
  def real_basedir
275
208
  Pathname.new(basedir).cleanpath.to_s
276
209
  end
277
210
 
278
- class DSL
279
- # A barebones implementation of the Puppetfile DSL
280
- #
281
- # @api private
282
-
283
- def initialize(librarian)
284
- @librarian = librarian
285
- end
286
-
287
- def mod(name, args = nil)
288
- @librarian.add_module(name, args)
289
- end
290
-
291
- def forge(location)
292
- @librarian.set_forge(location)
293
- end
294
-
295
- def moduledir(location)
296
- @librarian.set_moduledir(location)
297
- end
298
-
299
- def method_missing(method, *args)
300
- raise NoMethodError, _("unrecognized declaration '%{method}'") % {method: method}
301
- end
302
- end
211
+ DSL = R10K::ModuleLoader::Puppetfile::DSL
303
212
  end
304
213
  end
data/lib/r10k/settings.rb CHANGED
@@ -92,6 +92,9 @@ module R10K
92
92
  URIDefinition.new(:baseurl, {
93
93
  :desc => "The URL to the Puppet Forge to use for downloading modules."
94
94
  }),
95
+ Definition.new(:authorization_token, {
96
+ :desc => "The token for Puppet Forge authorization. Leave blank for unauthorized or license-based connections."
97
+ })
95
98
  ])
96
99
  end
97
100