r10k 3.5.1 → 3.9.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 (84) hide show
  1. checksums.yaml +4 -4
  2. data/.github/pull_request_template.md +4 -1
  3. data/.github/workflows/docker.yml +4 -1
  4. data/.github/workflows/release.yml +3 -2
  5. data/.github/workflows/rspec_tests.yml +81 -0
  6. data/.travis.yml +8 -1
  7. data/CHANGELOG.mkd +43 -2
  8. data/CODEOWNERS +2 -2
  9. data/README.mkd +13 -4
  10. data/doc/common-patterns.mkd +1 -0
  11. data/doc/dynamic-environments/configuration.mkd +143 -39
  12. data/doc/dynamic-environments/usage.mkd +12 -11
  13. data/doc/puppetfile.mkd +23 -3
  14. data/docker/Gemfile +1 -1
  15. data/docker/Makefile +7 -4
  16. data/docker/docker-compose.yml +18 -0
  17. data/docker/r10k/Dockerfile +4 -3
  18. data/docker/r10k/docker-entrypoint.sh +0 -1
  19. data/docker/r10k/release.Dockerfile +3 -2
  20. data/docker/spec/dockerfile_spec.rb +26 -32
  21. data/integration/tests/git_source/git_source_repeated_remote.rb +68 -0
  22. data/integration/tests/user_scenario/basic_workflow/multi_env_custom_forge_git_module.rb +2 -1
  23. data/integration/tests/user_scenario/basic_workflow/multi_env_custom_forge_git_module_static.rb +2 -1
  24. data/integration/tests/user_scenario/basic_workflow/multi_source_custom_forge_git_module.rb +1 -1
  25. data/integration/tests/user_scenario/basic_workflow/single_env_custom_forge_git_module.rb +2 -1
  26. data/integration/tests/user_scenario/complex_workflow/multi_env_add_change_remove.rb +1 -1
  27. data/integration/tests/user_scenario/complex_workflow/multi_env_remove_re-add.rb +1 -1
  28. data/integration/tests/user_scenario/complex_workflow/multi_env_unamanaged.rb +1 -1
  29. data/lib/r10k/action/deploy/display.rb +9 -3
  30. data/lib/r10k/action/deploy/environment.rb +39 -14
  31. data/lib/r10k/action/deploy/module.rb +4 -1
  32. data/lib/r10k/action/runner.rb +34 -0
  33. data/lib/r10k/cli/deploy.rb +14 -7
  34. data/lib/r10k/cli/puppetfile.rb +5 -5
  35. data/lib/r10k/environment/base.rb +9 -2
  36. data/lib/r10k/environment/git.rb +17 -2
  37. data/lib/r10k/environment/name.rb +22 -4
  38. data/lib/r10k/environment/svn.rb +11 -2
  39. data/lib/r10k/environment/with_modules.rb +28 -20
  40. data/lib/r10k/forge/module_release.rb +2 -2
  41. data/lib/r10k/git.rb +1 -0
  42. data/lib/r10k/git/cache.rb +12 -4
  43. data/lib/r10k/git/rugged/credentials.rb +39 -2
  44. data/lib/r10k/git/stateful_repository.rb +4 -0
  45. data/lib/r10k/initializers.rb +2 -0
  46. data/lib/r10k/module/base.rb +8 -0
  47. data/lib/r10k/module/forge.rb +16 -4
  48. data/lib/r10k/module/git.rb +42 -24
  49. data/lib/r10k/module/local.rb +1 -1
  50. data/lib/r10k/module/svn.rb +14 -11
  51. data/lib/r10k/puppetfile.rb +30 -12
  52. data/lib/r10k/settings.rb +30 -3
  53. data/lib/r10k/source/base.rb +5 -0
  54. data/lib/r10k/source/git.rb +26 -3
  55. data/lib/r10k/source/hash.rb +4 -2
  56. data/lib/r10k/source/svn.rb +5 -1
  57. data/lib/r10k/util/setopts.rb +33 -12
  58. data/lib/r10k/version.rb +1 -1
  59. data/locales/r10k.pot +71 -43
  60. data/r10k.gemspec +1 -1
  61. data/spec/fixtures/unit/action/r10k_creds.yaml +9 -0
  62. data/spec/shared-examples/subprocess-runner.rb +11 -5
  63. data/spec/unit/action/deploy/display_spec.rb +4 -0
  64. data/spec/unit/action/deploy/environment_spec.rb +154 -12
  65. data/spec/unit/action/deploy/module_spec.rb +40 -1
  66. data/spec/unit/action/puppetfile/install_spec.rb +1 -0
  67. data/spec/unit/action/runner_spec.rb +48 -1
  68. data/spec/unit/environment/git_spec.rb +19 -2
  69. data/spec/unit/environment/name_spec.rb +28 -0
  70. data/spec/unit/environment/svn_spec.rb +12 -0
  71. data/spec/unit/environment/with_modules_spec.rb +74 -0
  72. data/spec/unit/forge/module_release_spec.rb +14 -10
  73. data/spec/unit/git/cache_spec.rb +10 -0
  74. data/spec/unit/git/rugged/credentials_spec.rb +79 -2
  75. data/spec/unit/git_spec.rb +3 -3
  76. data/spec/unit/module/forge_spec.rb +6 -0
  77. data/spec/unit/module/git_spec.rb +56 -1
  78. data/spec/unit/module_spec.rb +59 -9
  79. data/spec/unit/puppetfile_spec.rb +61 -7
  80. data/spec/unit/settings_spec.rb +12 -0
  81. data/spec/unit/source/git_spec.rb +49 -1
  82. data/spec/unit/util/setopts_spec.rb +25 -1
  83. metadata +9 -11
  84. data/azure-pipelines.yml +0 -86
@@ -212,14 +212,14 @@ module R10K
212
212
 
213
213
  # Remove the temporary directory used for unpacking the module.
214
214
  def cleanup_unpack_path
215
- if unpack_path.exist?
215
+ if unpack_path.parent.exist?
216
216
  unpack_path.parent.rmtree
217
217
  end
218
218
  end
219
219
 
220
220
  # Remove the downloaded module release.
221
221
  def cleanup_download_path
222
- if download_path.exist?
222
+ if download_path.parent.exist?
223
223
  download_path.parent.rmtree
224
224
  end
225
225
  end
data/lib/r10k/git.rb CHANGED
@@ -134,6 +134,7 @@ module R10K
134
134
  extend R10K::Settings::Mixin::ClassMethods
135
135
 
136
136
  def_setting_attr :private_key
137
+ def_setting_attr :oauth_token
137
138
  def_setting_attr :proxy
138
139
  def_setting_attr :username
139
140
  def_setting_attr :repositories, {}
@@ -16,7 +16,17 @@ class R10K::Git::Cache
16
16
 
17
17
  include R10K::Settings::Mixin
18
18
 
19
- def_setting_attr :cache_root, File.expand_path(ENV['HOME'] ? '~/.r10k/git': '/root/.r10k/git')
19
+ #@api private
20
+ def self.determine_cache_root
21
+ if R10K::Util::Platform.windows?
22
+ File.join(ENV['LOCALAPPDATA'], 'r10k', 'git')
23
+ else
24
+ File.expand_path(ENV['HOME'] ? '~/.r10k/git': '/root/.r10k/git')
25
+ end
26
+ end
27
+ private_class_method :determine_cache_root
28
+
29
+ def_setting_attr :cache_root, determine_cache_root
20
30
 
21
31
  @instance_cache = R10K::InstanceCache.new(self)
22
32
 
@@ -99,10 +109,8 @@ class R10K::Git::Cache
99
109
 
100
110
  alias cached? exist?
101
111
 
102
- private
103
-
104
112
  # Reformat the remote name into something that can be used as a directory
105
113
  def sanitized_dirname
106
- @remote.gsub(/[^@\w\.-]/, '-')
114
+ @sanitized_dirname ||= @remote.gsub(/[^@\w\.-]/, '-')
107
115
  end
108
116
  end
@@ -61,11 +61,48 @@ class R10K::Git::Rugged::Credentials
61
61
  end
62
62
 
63
63
  def get_plaintext_credentials(url, username_from_url)
64
- user = get_git_username(url, username_from_url)
65
- password = URI.parse(url).password || ''
64
+ per_repo_oauth_token = nil
65
+ if per_repo_settings = R10K::Git.get_repo_settings(url)
66
+ per_repo_oauth_token = per_repo_settings[:oauth_token]
67
+ end
68
+
69
+ if token_path = per_repo_oauth_token || R10K::Git.settings[:oauth_token]
70
+ @oauth_token ||= extract_token(token_path, url)
71
+
72
+ user = 'x-oauth-token'
73
+ password = @oauth_token
74
+ else
75
+ user = get_git_username(url, username_from_url)
76
+ password = URI.parse(url).password || ''
77
+ end
66
78
  Rugged::Credentials::UserPassword.new(username: user, password: password)
67
79
  end
68
80
 
81
+ def extract_token(token_path, url)
82
+ if token_path == '-'
83
+ token = $stdin.read.strip
84
+ logger.debug2 _("Using OAuth token from stdin for URL %{url}") % { url: url }
85
+ elsif File.readable?(token_path)
86
+ token = File.read(token_path).strip
87
+ logger.debug2 _("Using OAuth token from %{token_path} for URL %{url}") % { token_path: token_path, url: url }
88
+ else
89
+ raise R10K::Git::GitError, _("%{path} is missing or unreadable, cannot load OAuth token") % { path: token_path }
90
+ end
91
+
92
+ unless valid_token?(token)
93
+ raise R10K::Git::GitError, _("Supplied OAuth token contains invalid characters.")
94
+ end
95
+
96
+ token
97
+ end
98
+
99
+ # This regex is the only real requirement for OAuth token format,
100
+ # per https://www.oauth.com/oauth2-servers/access-tokens/access-token-response/
101
+ # Bitbucket's tokens also can include an underscore, so that is added here.
102
+ def valid_token?(token)
103
+ return token =~ /^[\w\-\.~_\+\/]+$/
104
+ end
105
+
69
106
  def get_default_credentials(url, username_from_url)
70
107
  Rugged::Credentials::Default.new
71
108
  end
@@ -12,6 +12,10 @@ class R10K::Git::StatefulRepository
12
12
  # @api private
13
13
  attr_reader :repo
14
14
 
15
+ # @!attribute [r] cache
16
+ # @api private
17
+ attr_reader :cache
18
+
15
19
  extend Forwardable
16
20
  def_delegators :@repo, :head, :tracked_paths
17
21
 
@@ -44,6 +44,7 @@ module R10K
44
44
  class DeployInitializer < BaseInitializer
45
45
  def call
46
46
  with_setting(:puppet_path) { |value| R10K::Settings.puppet_path = value }
47
+ with_setting(:puppet_conf) { |value| R10K::Settings.puppet_conf = value }
47
48
  end
48
49
  end
49
50
 
@@ -54,6 +55,7 @@ module R10K
54
55
  with_setting(:private_key) { |value| R10K::Git.settings[:private_key] = value }
55
56
  with_setting(:proxy) { |value| R10K::Git.settings[:proxy] = value }
56
57
  with_setting(:repositories) { |value| R10K::Git.settings[:repositories] = value }
58
+ with_setting(:oauth_token) { |value| R10K::Git.settings[:oauth_token] = value }
57
59
  end
58
60
  end
59
61
 
@@ -99,6 +99,14 @@ class R10K::Module::Base
99
99
  raise NotImplementedError
100
100
  end
101
101
 
102
+ # Return the module's cachedir. Subclasses that implement a cache
103
+ # will override this to return a real directory location.
104
+ #
105
+ # @return [String, :none]
106
+ def cachedir
107
+ :none
108
+ end
109
+
102
110
  private
103
111
 
104
112
  def parse_title(title)
@@ -13,7 +13,7 @@ class R10K::Module::Forge < R10K::Module::Base
13
13
  R10K::Module.register(self)
14
14
 
15
15
  def self.implement?(name, args)
16
- !!(name.match %r[\w+[/-]\w+]) && valid_version?(args)
16
+ (args.is_a?(Hash) && args[:type].to_s == 'forge') || (!!(name.match %r[\w+[/-]\w+]) && valid_version?(args))
17
17
  end
18
18
 
19
19
  def self.valid_version?(expected_version)
@@ -32,13 +32,25 @@ class R10K::Module::Forge < R10K::Module::Base
32
32
 
33
33
  include R10K::Logging
34
34
 
35
- def initialize(title, dirname, expected_version, environment=nil)
35
+ include R10K::Util::Setopts
36
+
37
+ def initialize(title, dirname, opts, environment=nil)
36
38
  super
37
39
 
38
40
  @metadata_file = R10K::Module::MetadataFile.new(path + 'metadata.json')
39
41
  @metadata = @metadata_file.read
40
42
 
41
- @expected_version = expected_version || current_version || :latest
43
+ if opts.is_a?(Hash)
44
+ setopts(opts, {
45
+ # Standard option interface
46
+ :version => :expected_version,
47
+ :source => ::R10K::Util::Setopts::Ignore,
48
+ :type => ::R10K::Util::Setopts::Ignore,
49
+ })
50
+ else
51
+ @expected_version = opts || current_version || :latest
52
+ end
53
+
42
54
  @v3_module = PuppetForge::V3::Module.new(:slug => @title)
43
55
  end
44
56
 
@@ -171,7 +183,7 @@ class R10K::Module::Forge < R10K::Module::Base
171
183
  if (match = title.match(/\A(\w+)[-\/](\w+)\Z/))
172
184
  [match[1], match[2]]
173
185
  else
174
- raise ArgumentError, _("Forge module names must match 'owner/modulename'")
186
+ raise ArgumentError, _("Forge module names must match 'owner/modulename', instead got #{title}")
175
187
  end
176
188
  end
177
189
  end
@@ -8,7 +8,7 @@ class R10K::Module::Git < R10K::Module::Base
8
8
  R10K::Module.register(self)
9
9
 
10
10
  def self.implement?(name, args)
11
- args.is_a? Hash and args.has_key?(:git)
11
+ args.is_a?(Hash) && (args.has_key?(:git) || args[:type].to_s == 'git')
12
12
  rescue
13
13
  false
14
14
  end
@@ -28,16 +28,42 @@ class R10K::Module::Git < R10K::Module::Base
28
28
  # @return [String]
29
29
  attr_reader :default_ref
30
30
 
31
- def initialize(title, dirname, args, environment=nil)
31
+ # @!attribute [r] default_override_ref
32
+ # @api private
33
+ # @return [String]
34
+ attr_reader :default_override_ref
35
+
36
+ include R10K::Util::Setopts
37
+
38
+ def initialize(title, dirname, opts, environment=nil)
32
39
  super
40
+ setopts(opts, {
41
+ # Standard option interface
42
+ :version => :desired_ref,
43
+ :source => :remote,
44
+ :type => ::R10K::Util::Setopts::Ignore,
45
+
46
+ # Type-specific options
47
+ :branch => :desired_ref,
48
+ :tag => :desired_ref,
49
+ :commit => :desired_ref,
50
+ :ref => :desired_ref,
51
+ :git => :remote,
52
+ :default_branch => :default_ref,
53
+ :default_branch_override => :default_override_ref,
54
+ })
55
+
56
+ @desired_ref ||= 'master'
33
57
 
34
- parse_options(@args)
58
+ if @desired_ref == :control_branch && @environment && @environment.respond_to?(:ref)
59
+ @desired_ref = @environment.ref
60
+ end
35
61
 
36
62
  @repo = R10K::Git::StatefulRepository.new(@remote, @dirname, @name)
37
63
  end
38
64
 
39
65
  def version
40
- validate_ref(@desired_ref, @default_ref)
66
+ validate_ref(@desired_ref, @default_ref, @default_override_ref)
41
67
  end
42
68
 
43
69
  def properties
@@ -57,11 +83,17 @@ class R10K::Module::Git < R10K::Module::Base
57
83
  @repo.status(version)
58
84
  end
59
85
 
86
+ def cachedir
87
+ @repo.cache.sanitized_dirname
88
+ end
89
+
60
90
  private
61
91
 
62
- def validate_ref(desired, default)
92
+ def validate_ref(desired, default, default_override)
63
93
  if desired && desired != :control_branch && @repo.resolve(desired)
64
94
  return desired
95
+ elsif default_override && @repo.resolve(default_override)
96
+ return default_override
65
97
  elsif default && @repo.resolve(default)
66
98
  return default
67
99
  else
@@ -77,6 +109,11 @@ class R10K::Module::Git < R10K::Module::Base
77
109
  msg << "Could not determine desired ref"
78
110
  end
79
111
 
112
+ if default_override
113
+ msg << "or resolve the default branch override '%{default_override}',"
114
+ vars[:default_override] = default_override
115
+ end
116
+
80
117
  if default
81
118
  msg << "or resolve default ref '%{default}'"
82
119
  vars[:default] = default
@@ -87,23 +124,4 @@ class R10K::Module::Git < R10K::Module::Base
87
124
  raise ArgumentError, _(msg.join(' ')) % vars
88
125
  end
89
126
  end
90
-
91
- def parse_options(options)
92
- ref_opts = [:branch, :tag, :commit, :ref]
93
- known_opts = [:git, :default_branch] + ref_opts
94
-
95
- unhandled = options.keys - known_opts
96
- unless unhandled.empty?
97
- raise ArgumentError, _("Unhandled options %{unhandled} specified for %{class}") % {unhandled: unhandled, class: self.class}
98
- end
99
-
100
- @remote = options[:git]
101
-
102
- @desired_ref = ref_opts.find { |key| break options[key] if options.has_key?(key) } || 'master'
103
- @default_ref = options[:default_branch]
104
-
105
- if @desired_ref == :control_branch && @environment && @environment.respond_to?(:ref)
106
- @desired_ref = @environment.ref
107
- end
108
- end
109
127
  end
@@ -9,7 +9,7 @@ class R10K::Module::Local < R10K::Module::Base
9
9
  R10K::Module.register(self)
10
10
 
11
11
  def self.implement?(name, args)
12
- args.is_a?(Hash) && args[:local]
12
+ args.is_a?(Hash) && (args[:local] || args[:type].to_s == 'local')
13
13
  end
14
14
 
15
15
  include R10K::Logging
@@ -7,7 +7,7 @@ class R10K::Module::SVN < R10K::Module::Base
7
7
  R10K::Module.register(self)
8
8
 
9
9
  def self.implement?(name, args)
10
- args.is_a? Hash and args.has_key? :svn
10
+ args.is_a?(Hash) && (args.has_key?(:svn) || args[:type].to_s == 'svn')
11
11
  end
12
12
 
13
13
  # @!attribute [r] expected_revision
@@ -36,18 +36,21 @@ class R10K::Module::SVN < R10K::Module::Base
36
36
 
37
37
  include R10K::Util::Setopts
38
38
 
39
- INITIALIZE_OPTS = {
40
- :svn => :url,
41
- :rev => :expected_revision,
42
- :revision => :expected_revision,
43
- :username => :self,
44
- :password => :self
45
- }
46
-
47
39
  def initialize(name, dirname, opts, environment=nil)
48
40
  super
49
-
50
- setopts(opts, INITIALIZE_OPTS)
41
+ setopts(opts, {
42
+ # Standard option interface
43
+ :source => :url,
44
+ :version => :expected_revision,
45
+ :type => ::R10K::Util::Setopts::Ignore,
46
+
47
+ # Type-specific options
48
+ :svn => :url,
49
+ :rev => :expected_revision,
50
+ :revision => :expected_revision,
51
+ :username => :self,
52
+ :password => :self
53
+ })
51
54
 
52
55
  @working_dir = R10K::SVN::WorkingDir.new(@path, :username => @username, :password => @password)
53
56
  end
@@ -10,7 +10,7 @@ class Puppetfile
10
10
 
11
11
  include R10K::Settings::Mixin
12
12
 
13
- def_setting_attr :pool_size, 1
13
+ def_setting_attr :pool_size, 4
14
14
 
15
15
  include R10K::Logging
16
16
 
@@ -77,7 +77,7 @@ class Puppetfile
77
77
 
78
78
  dsl = R10K::Puppetfile::DSL.new(self)
79
79
  dsl.instance_eval(puppetfile_contents, @puppetfile_path)
80
-
80
+
81
81
  validate_no_duplicate_names(@modules)
82
82
  @loaded = true
83
83
  rescue SyntaxError, LoadError, ArgumentError, NameError => e
@@ -127,16 +127,23 @@ class Puppetfile
127
127
  end
128
128
 
129
129
  if args.is_a?(Hash) && @default_branch_override != nil
130
- args[:default_branch] = @default_branch_override
130
+ args[:default_branch_override] = @default_branch_override
131
131
  end
132
132
 
133
- # Keep track of all the content this Puppetfile is managing to enable purging.
134
- @managed_content[install_path] = Array.new unless @managed_content.has_key?(install_path)
135
-
136
133
  mod = R10K::Module.new(name, install_path, args, @environment)
137
- mod.origin = 'Puppetfile'
134
+ mod.origin = :puppetfile
138
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
142
+
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)
139
145
  @managed_content[install_path] << mod.name
146
+
140
147
  @modules << mod
141
148
  end
142
149
 
@@ -145,7 +152,9 @@ class Puppetfile
145
152
  def managed_directories
146
153
  self.load unless @loaded
147
154
 
148
- @managed_content.keys
155
+ dirs = @managed_content.keys
156
+ dirs.delete(real_basedir)
157
+ dirs
149
158
  end
150
159
 
151
160
  # Returns an array of the full paths to all the content being managed.
@@ -212,15 +221,22 @@ class Puppetfile
212
221
  def modules_queue(visitor)
213
222
  Queue.new.tap do |queue|
214
223
  visitor.visit(:puppetfile, self) do
215
- modules.each { |mod| queue << mod }
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 }
216
229
  end
217
230
  end
218
231
  end
232
+ public :modules_queue
219
233
 
220
234
  def visitor_thread(visitor, mods_queue)
221
235
  Thread.new do
222
236
  begin
223
- while mod = mods_queue.pop(true) do mod.accept(visitor) end
237
+ while mods = mods_queue.pop(true) do
238
+ mods.each {|mod| mod.accept(visitor) }
239
+ end
224
240
  rescue ThreadError => e
225
241
  logger.debug _("Module thread %{id} exiting: %{message}") % {message: e.message, id: Thread.current.object_id}
226
242
  Thread.exit
@@ -248,8 +264,6 @@ class Puppetfile
248
264
  end
249
265
 
250
266
  def validate_install_path(path, modname)
251
- real_basedir = Pathname.new(basedir).cleanpath.to_s
252
-
253
267
  unless /^#{Regexp.escape(real_basedir)}.*/ =~ path
254
268
  raise R10K::Error.new("Puppetfile cannot manage content '#{modname}' outside of containing environment: #{path} is not within #{real_basedir}")
255
269
  end
@@ -257,6 +271,10 @@ class Puppetfile
257
271
  true
258
272
  end
259
273
 
274
+ def real_basedir
275
+ Pathname.new(basedir).cleanpath.to_s
276
+ end
277
+
260
278
  class DSL
261
279
  # A barebones implementation of the Puppetfile DSL
262
280
  #