r10k 3.7.0 → 3.9.3

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 (85) hide show
  1. checksums.yaml +4 -4
  2. data/.github/pull_request_template.md +1 -1
  3. data/.github/workflows/docker.yml +4 -1
  4. data/.github/workflows/release.yml +3 -2
  5. data/.github/workflows/rspec_tests.yml +1 -1
  6. data/.github/workflows/stale.yml +19 -0
  7. data/.travis.yml +8 -1
  8. data/CHANGELOG.mkd +32 -0
  9. data/CODEOWNERS +2 -2
  10. data/doc/common-patterns.mkd +1 -0
  11. data/doc/dynamic-environments/configuration.mkd +114 -42
  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 +4 -3
  16. data/docker/docker-compose.yml +18 -0
  17. data/docker/r10k/Dockerfile +1 -1
  18. data/docker/r10k/docker-entrypoint.sh +0 -1
  19. data/docker/r10k/release.Dockerfile +1 -1
  20. data/docker/spec/dockerfile_spec.rb +26 -32
  21. data/integration/tests/git_source/git_source_repeated_remote.rb +2 -2
  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/lib/r10k/action/base.rb +10 -0
  27. data/lib/r10k/action/deploy/display.rb +49 -10
  28. data/lib/r10k/action/deploy/environment.rb +101 -51
  29. data/lib/r10k/action/deploy/module.rb +54 -30
  30. data/lib/r10k/action/puppetfile/check.rb +3 -1
  31. data/lib/r10k/action/puppetfile/install.rb +20 -23
  32. data/lib/r10k/action/puppetfile/purge.rb +8 -2
  33. data/lib/r10k/action/runner.rb +33 -0
  34. data/lib/r10k/cli/deploy.rb +13 -7
  35. data/lib/r10k/cli/puppetfile.rb +5 -5
  36. data/lib/r10k/content_synchronizer.rb +83 -0
  37. data/lib/r10k/deployment.rb +1 -1
  38. data/lib/r10k/environment/base.rb +29 -2
  39. data/lib/r10k/environment/git.rb +17 -5
  40. data/lib/r10k/environment/name.rb +22 -4
  41. data/lib/r10k/environment/svn.rb +11 -4
  42. data/lib/r10k/environment/with_modules.rb +46 -30
  43. data/lib/r10k/git.rb +1 -0
  44. data/lib/r10k/git/rugged/credentials.rb +39 -2
  45. data/lib/r10k/initializers.rb +1 -0
  46. data/lib/r10k/module.rb +1 -1
  47. data/lib/r10k/module/base.rb +17 -1
  48. data/lib/r10k/module/forge.rb +29 -11
  49. data/lib/r10k/module/git.rb +50 -27
  50. data/lib/r10k/module/local.rb +2 -1
  51. data/lib/r10k/module/svn.rb +24 -18
  52. data/lib/r10k/puppetfile.rb +66 -83
  53. data/lib/r10k/settings.rb +18 -2
  54. data/lib/r10k/source/base.rb +9 -0
  55. data/lib/r10k/source/git.rb +18 -7
  56. data/lib/r10k/source/hash.rb +5 -5
  57. data/lib/r10k/source/svn.rb +5 -3
  58. data/lib/r10k/util/cleaner.rb +21 -0
  59. data/lib/r10k/util/setopts.rb +33 -12
  60. data/lib/r10k/version.rb +1 -1
  61. data/locales/r10k.pot +98 -82
  62. data/r10k.gemspec +1 -1
  63. data/spec/fixtures/unit/action/r10k_creds.yaml +9 -0
  64. data/spec/r10k-mocks/mock_source.rb +1 -1
  65. data/spec/shared-examples/puppetfile-action.rb +7 -7
  66. data/spec/unit/action/deploy/display_spec.rb +35 -5
  67. data/spec/unit/action/deploy/environment_spec.rb +199 -38
  68. data/spec/unit/action/deploy/module_spec.rb +162 -28
  69. data/spec/unit/action/puppetfile/check_spec.rb +2 -2
  70. data/spec/unit/action/puppetfile/install_spec.rb +31 -10
  71. data/spec/unit/action/puppetfile/purge_spec.rb +25 -5
  72. data/spec/unit/action/runner_spec.rb +48 -1
  73. data/spec/unit/environment/git_spec.rb +19 -2
  74. data/spec/unit/environment/name_spec.rb +28 -0
  75. data/spec/unit/environment/svn_spec.rb +12 -0
  76. data/spec/unit/environment/with_modules_spec.rb +74 -0
  77. data/spec/unit/git/rugged/credentials_spec.rb +78 -1
  78. data/spec/unit/module/forge_spec.rb +21 -13
  79. data/spec/unit/module/git_spec.rb +63 -8
  80. data/spec/unit/module_spec.rb +77 -10
  81. data/spec/unit/puppetfile_spec.rb +63 -60
  82. data/spec/unit/util/purgeable_spec.rb +2 -8
  83. data/spec/unit/util/setopts_spec.rb +25 -1
  84. metadata +11 -12
  85. data/azure-pipelines.yml +0 -87
@@ -1,10 +1,13 @@
1
1
  require 'r10k/util/subprocess'
2
+ require 'r10k/logging'
2
3
 
3
4
  # This class defines a common interface for environment implementations.
4
5
  #
5
6
  # @since 1.3.0
6
7
  class R10K::Environment::Base
7
8
 
9
+ include R10K::Logging
10
+
8
11
  # @!attribute [r] name
9
12
  # @return [String] A name for this environment that is unique to the given source
10
13
  attr_reader :name
@@ -43,12 +46,16 @@ class R10K::Environment::Base
43
46
  @basedir = basedir
44
47
  @dirname = dirname
45
48
  @options = options
46
- @puppetfile_name = options[:puppetfile_name]
49
+ @puppetfile_name = options.delete(:puppetfile_name)
50
+ @overrides = options.delete(:overrides) || {}
47
51
 
48
52
  @full_path = File.join(@basedir, @dirname)
49
53
  @path = Pathname.new(File.join(@basedir, @dirname))
50
54
 
51
- @puppetfile = R10K::Puppetfile.new(@full_path, nil, nil, @puppetfile_name)
55
+ @puppetfile = R10K::Puppetfile.new(@full_path,
56
+ {overrides: @overrides,
57
+ force: @overrides.dig(:modules, :force),
58
+ puppetfile_name: @puppetfile_name})
52
59
  @puppetfile.environment = self
53
60
  end
54
61
 
@@ -103,12 +110,32 @@ class R10K::Environment::Base
103
110
  @puppetfile.modules
104
111
  end
105
112
 
113
+ # @return [Array<R10K::Module::Base>] Whether or not the given module
114
+ # conflicts with any modules already defined in the r10k environment
115
+ # object.
116
+ def module_conflicts?(mod)
117
+ false
118
+ end
119
+
106
120
  def accept(visitor)
107
121
  visitor.visit(:environment, self) do
108
122
  puppetfile.accept(visitor)
109
123
  end
110
124
  end
111
125
 
126
+ def deploy
127
+ puppetfile.load(@overrides.dig(:environments, :default_branch_override))
128
+
129
+ puppetfile.sync
130
+
131
+ if (@overrides.dig(:purging, :purge_levels) || []).include?(:puppetfile)
132
+ logger.debug("Purging unmanaged Puppetfile content for environment '#{dirname}'...")
133
+ R10K::Util::Cleaner.new(puppetfile.managed_directories,
134
+ puppetfile.desired_contents,
135
+ puppetfile.purge_exclusions).purge!
136
+ end
137
+ end
138
+
112
139
  def whitelist(user_whitelist=[])
113
140
  user_whitelist.collect { |pattern| File.join(@full_path, pattern) }
114
141
  end
@@ -1,4 +1,3 @@
1
- require 'r10k/logging'
2
1
  require 'r10k/puppetfile'
3
2
  require 'r10k/git/stateful_repository'
4
3
  require 'forwardable'
@@ -8,8 +7,6 @@ require 'forwardable'
8
7
  # @since 1.3.0
9
8
  class R10K::Environment::Git < R10K::Environment::WithModules
10
9
 
11
- include R10K::Logging
12
-
13
10
  R10K::Environment.register(:git, self)
14
11
  # Register git as the default environment type
15
12
  R10K::Environment.register(nil, self)
@@ -27,6 +24,8 @@ class R10K::Environment::Git < R10K::Environment::WithModules
27
24
  # @return [R10K::Git::StatefulRepository] The git repo backing this environment
28
25
  attr_reader :repo
29
26
 
27
+ include R10K::Util::Setopts
28
+
30
29
  # Initialize the given Git environment.
31
30
  #
32
31
  # @param name [String] The unique name describing this environment.
@@ -38,8 +37,21 @@ class R10K::Environment::Git < R10K::Environment::WithModules
38
37
  # @param options [String] :ref The git reference to use for this environment
39
38
  def initialize(name, basedir, dirname, options = {})
40
39
  super
41
- @remote = options[:remote]
42
- @ref = options[:ref]
40
+ setopts(options, {
41
+ # Standard option interface
42
+ :version => :ref,
43
+ :source => :remote,
44
+ :type => ::R10K::Util::Setopts::Ignore,
45
+
46
+ # Type-specific options
47
+ :ref => :self,
48
+ :remote => :self,
49
+
50
+ }, raise_on_unhandled: false)
51
+ # TODO: in r10k 4.0.0, a major version bump, stop allowing garbage options.
52
+ # We only allow them now, here, on this object, because prior to adopting
53
+ # setopts in the constructor, this object type didn't do any validation
54
+ # checking of options passed, and would permit garbage parameters.
43
55
 
44
56
  @repo = R10K::Git::StatefulRepository.new(@remote, @basedir, @dirname)
45
57
  end
@@ -12,13 +12,13 @@ module R10K
12
12
  INVALID_CHARACTERS = %r[\W]
13
13
 
14
14
  def initialize(name, opts)
15
- @name = name
16
- @opts = opts
17
-
18
15
  @source = opts[:source]
19
16
  @prefix = opts[:prefix]
20
17
  @invalid = opts[:invalid]
21
18
 
19
+ @name = derive_name(name, opts[:strip_component])
20
+ @opts = opts
21
+
22
22
  case @invalid
23
23
  when 'correct_and_warn'
24
24
  @validate = true
@@ -71,8 +71,26 @@ module R10K
71
71
 
72
72
  private
73
73
 
74
- def derive_prefix(source,prefix)
74
+ def derive_name(name, strip_component)
75
+ return name unless strip_component
76
+
77
+ unless strip_component.is_a?(String)
78
+ raise _('Improper configuration value given for strip_component setting in %{src} source. ' \
79
+ 'Value must be a string, a /regex/, false, or omitted. Got "%{val}" (%{type})' \
80
+ % {src: @source, val: strip_component, type: strip_component.class})
81
+ end
75
82
 
83
+ if %r{^/.*/$}.match(strip_component)
84
+ regex = Regexp.new(strip_component[1..-2])
85
+ name.gsub(regex, '')
86
+ elsif name.start_with?(strip_component)
87
+ name[strip_component.size..-1]
88
+ else
89
+ name
90
+ end
91
+ end
92
+
93
+ def derive_prefix(source,prefix)
76
94
  if prefix == true
77
95
  "#{source}_"
78
96
  elsif prefix.is_a? String
@@ -7,8 +7,6 @@ require 'r10k/util/setopts'
7
7
  # @since 1.3.0
8
8
  class R10K::Environment::SVN < R10K::Environment::Base
9
9
 
10
- include R10K::Logging
11
-
12
10
  R10K::Environment.register(:svn, self)
13
11
 
14
12
  # @!attribute [r] remote
@@ -44,8 +42,17 @@ class R10K::Environment::SVN < R10K::Environment::Base
44
42
  # @option options [String] :password The SVN password
45
43
  def initialize(name, basedir, dirname, options = {})
46
44
  super
45
+ setopts(options, {
46
+ # Standard option interface
47
+ :source => :remote,
48
+ :version => :expected_revision,
49
+ :type => ::R10K::Util::Setopts::Ignore,
47
50
 
48
- setopts(options, {:remote => :self, :username => :self, :password => :self, :puppetfile_name => :self })
51
+ # Type-specific options
52
+ :remote => :self,
53
+ :username => :self,
54
+ :password => :self,
55
+ })
49
56
 
50
57
  @working_dir = R10K::SVN::WorkingDir.new(Pathname.new(@full_path), :username => @username, :password => @password)
51
58
  end
@@ -61,7 +68,7 @@ class R10K::Environment::SVN < R10K::Environment::Base
61
68
  if @working_dir.is_svn?
62
69
  @working_dir.update
63
70
  else
64
- @working_dir.checkout(@remote)
71
+ @working_dir.checkout(@remote, @expected_revision)
65
72
  end
66
73
  @synced = true
67
74
  end
@@ -1,4 +1,3 @@
1
- require 'r10k/logging'
2
1
  require 'r10k/util/purgeable'
3
2
 
4
3
  # This abstract base class implements an environment that can include module
@@ -7,8 +6,6 @@ require 'r10k/util/purgeable'
7
6
  # @since 3.4.0
8
7
  class R10K::Environment::WithModules < R10K::Environment::Base
9
8
 
10
- include R10K::Logging
11
-
12
9
  # @!attribute [r] moduledir
13
10
  # @return [String] The directory to install environment-defined modules
14
11
  # into (default: #{basedir}/modules)
@@ -24,7 +21,7 @@ class R10K::Environment::WithModules < R10K::Environment::Base
24
21
  # @param options [String] :moduledir The path to install modules to
25
22
  # @param options [Hash] :modules Modules to add to the environment
26
23
  def initialize(name, basedir, dirname, options = {})
27
- super(name, basedir, dirname, options)
24
+ super
28
25
 
29
26
  @managed_content = {}
30
27
  @modules = []
@@ -46,38 +43,71 @@ class R10K::Environment::WithModules < R10K::Environment::Base
46
43
  # - The r10k environment object
47
44
  # - A Puppetfile in the environment's content
48
45
  def modules
49
- return @modules if @puppetfile.nil?
46
+ return @modules if puppetfile.nil?
47
+
48
+ puppetfile.load unless puppetfile.loaded?
49
+ @modules + puppetfile.modules
50
+ end
51
+
52
+ def module_conflicts?(mod_b)
53
+ conflict = @modules.any? { |mod_a| mod_a.name == mod_b.name }
54
+ return false unless conflict
55
+
56
+ msg_vars = {src: mod_b.origin, name: mod_b.name}
57
+ msg_error = _('Environment and %{src} both define the "%{name}" module' % msg_vars)
58
+ msg_continue = _("#{msg_error}. The %{src} definition will be ignored" % msg_vars)
59
+
60
+ case conflict_opt = @options[:module_conflicts]
61
+ when 'override_and_warn', nil
62
+ logger.warn msg_continue
63
+ when 'override'
64
+ logger.debug msg_continue
65
+ when 'error'
66
+ raise R10K::Error, msg_error
67
+ else
68
+ raise R10K::Error, _('Unexpected value for `module_conflicts` setting in %{env} ' \
69
+ 'environment: %{val}' % {env: self.name, val: conflict_opt})
70
+ end
50
71
 
51
- @puppetfile.load unless @puppetfile.loaded?
52
- @modules + @puppetfile.modules
72
+ true
53
73
  end
54
74
 
55
75
  def accept(visitor)
56
76
  visitor.visit(:environment, self) do
57
77
  @modules.each do |mod|
58
- mod.accept(visitor)
78
+ mod.sync
59
79
  end
60
80
 
61
81
  puppetfile.accept(visitor)
62
- validate_no_module_conflicts
63
82
  end
64
83
  end
65
84
 
85
+ def deploy
86
+ @modules.each do |mod|
87
+ mod.sync
88
+ end
89
+
90
+ super
91
+ end
92
+
66
93
  def load_modules(module_hash)
67
94
  module_hash.each do |name, args|
95
+ if !args.is_a?(Hash)
96
+ args = { version: args }
97
+ end
98
+
68
99
  add_module(name, args)
69
100
  end
70
101
  end
71
102
 
72
103
  # @param [String] name
73
- # @param [*Object] args
104
+ # @param [Hash] args
74
105
  def add_module(name, args)
75
- if args.is_a?(Hash)
76
- # symbolize keys in the args hash
77
- args = args.inject({}) { |memo,(k,v)| memo[k.to_sym] = v; memo }
78
- end
106
+ # symbolize keys in the args hash
107
+ args = args.inject({}) { |memo,(k,v)| memo[k.to_sym] = v; memo }
108
+ args[:overrides] = @overrides
79
109
 
80
- if args.is_a?(Hash) && install_path = args.delete(:install_path)
110
+ if install_path = args.delete(:install_path)
81
111
  install_path = resolve_install_path(install_path)
82
112
  validate_install_path(install_path, name)
83
113
  else
@@ -88,26 +118,12 @@ class R10K::Environment::WithModules < R10K::Environment::Base
88
118
  @managed_content[install_path] = Array.new unless @managed_content.has_key?(install_path)
89
119
 
90
120
  mod = R10K::Module.new(name, install_path, args, self.name)
91
- mod.origin = 'Environment'
121
+ mod.origin = :environment
92
122
 
93
123
  @managed_content[install_path] << mod.name
94
124
  @modules << mod
95
125
  end
96
126
 
97
- def validate_no_module_conflicts
98
- @puppetfile.load unless @puppetfile.loaded?
99
- conflicts = (@modules + @puppetfile.modules)
100
- .group_by { |mod| mod.name }
101
- .select { |_, v| v.size > 1 }
102
- .map(&:first)
103
- unless conflicts.empty?
104
- msg = _('Puppetfile cannot contain module names defined by environment %{name}') % {name: self.name}
105
- msg += ' '
106
- msg += _("Remove the conflicting definitions of the following modules: %{conflicts}" % { conflicts: conflicts.join(' ') })
107
- raise R10K::Error.new(msg)
108
- end
109
- end
110
-
111
127
  include R10K::Util::Purgeable
112
128
 
113
129
  # Returns an array of the full paths that can be purged.
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, {}
@@ -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
@@ -55,6 +55,7 @@ module R10K
55
55
  with_setting(:private_key) { |value| R10K::Git.settings[:private_key] = value }
56
56
  with_setting(:proxy) { |value| R10K::Git.settings[:proxy] = value }
57
57
  with_setting(:repositories) { |value| R10K::Git.settings[:repositories] = value }
58
+ with_setting(:oauth_token) { |value| R10K::Git.settings[:oauth_token] = value }
58
59
  end
59
60
  end
60
61
 
data/lib/r10k/module.rb CHANGED
@@ -17,7 +17,7 @@ module R10K::Module
17
17
  #
18
18
  # @param [String] name The unique name of the module
19
19
  # @param [String] basedir The root to install the module in
20
- # @param [Object] args An arbitary value or set of values that specifies the implementation
20
+ # @param [Hash] args An arbitary Hash that specifies the implementation
21
21
  # @param [R10K::Environment] environment Optional environment that this module is a part of
22
22
  #
23
23
  # @return [Object < R10K::Module] A member of the implementing subclass
@@ -51,7 +51,11 @@ class R10K::Module::Base
51
51
  @owner, @name = parse_title(@title)
52
52
  @path = Pathname.new(File.join(@dirname, @name))
53
53
  @environment = environment
54
+ @overrides = args.delete(:overrides) || {}
54
55
  @origin = 'external' # Expect Puppetfile or R10k::Environment to set this to a specific value
56
+
57
+ @requested_modules = @overrides.dig(:modules, :requested_modules) || []
58
+ @should_sync = (@requested_modules.empty? || @requested_modules.include?(@name))
55
59
  end
56
60
 
57
61
  # @deprecated
@@ -61,11 +65,22 @@ class R10K::Module::Base
61
65
  end
62
66
 
63
67
  # Synchronize this module with the indicated state.
64
- # @abstract
68
+ # @param [Hash] opts Deprecated
65
69
  def sync(opts={})
66
70
  raise NotImplementedError
67
71
  end
68
72
 
73
+ def should_sync?
74
+ if @should_sync
75
+ logger.info _("Deploying module to %{path}") % {path: path}
76
+ true
77
+ else
78
+ logger.debug1(_("Only updating modules %{modules}, skipping module %{name}") % {modules: @requested_modules.inspect, name: name})
79
+ false
80
+ end
81
+ end
82
+
83
+
69
84
  # Return the desired version of this module
70
85
  # @abstract
71
86
  def version
@@ -87,6 +102,7 @@ class R10K::Module::Base
87
102
  raise NotImplementedError
88
103
  end
89
104
 
105
+ # Deprecated
90
106
  def accept(visitor)
91
107
  visitor.visit(:module, self)
92
108
  end