r10k 3.5.1 → 3.9.0

Sign up to get free protection for your applications and to get access to all the features.
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
  #