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
@@ -111,6 +111,6 @@ class R10K::Git::Cache
111
111
 
112
112
  # Reformat the remote name into something that can be used as a directory
113
113
  def sanitized_dirname
114
- @sanitized_dirname ||= @remote.gsub(/[^@\w\.-]/, '-')
114
+ @sanitized_dirname ||= @remote.gsub(/(\w+:\/\/)(.*)(@)/, '\1').gsub(/[^@\w\.-]/, '-')
115
115
  end
116
116
  end
@@ -63,6 +63,13 @@ module R10K
63
63
  def call
64
64
  with_setting(:baseurl) { |value| PuppetForge.host = value }
65
65
  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
+ }
66
73
  end
67
74
  end
68
75
  end
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
@@ -13,7 +13,12 @@ class R10K::Module::Forge < R10K::Module::Base
13
13
  R10K::Module.register(self)
14
14
 
15
15
  def self.implement?(name, args)
16
- (args.is_a?(Hash) && args[:type].to_s == 'forge') || (!!(name.match %r[\w+[/-]\w+]) && valid_version?(args))
16
+ args[:type].to_s == 'forge' ||
17
+ (!!
18
+ ((args.keys & %i{git svn type}).empty? &&
19
+ args.has_key?(:version) &&
20
+ name.match(%r[\w+[/-]\w+]) &&
21
+ valid_version?(args[:version])))
17
22
  end
18
23
 
19
24
  def self.valid_version?(expected_version)
@@ -40,28 +45,29 @@ class R10K::Module::Forge < R10K::Module::Base
40
45
  @metadata_file = R10K::Module::MetadataFile.new(path + 'metadata.json')
41
46
  @metadata = @metadata_file.read
42
47
 
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
48
+ setopts(opts, {
49
+ # Standard option interface
50
+ :version => :expected_version,
51
+ :source => ::R10K::Util::Setopts::Ignore,
52
+ :type => ::R10K::Util::Setopts::Ignore,
53
+ }, :raise_on_unhandled => false)
54
+
55
+ @expected_version ||= current_version || :latest
53
56
 
54
57
  @v3_module = PuppetForge::V3::Module.new(:slug => @title)
55
58
  end
56
59
 
60
+ # @param [Hash] opts Deprecated
57
61
  def sync(opts={})
58
- case status
59
- when :absent
60
- install
61
- when :outdated
62
- upgrade
63
- when :mismatched
64
- reinstall
62
+ if should_sync?
63
+ case status
64
+ when :absent
65
+ install
66
+ when :outdated
67
+ upgrade
68
+ when :mismatched
69
+ reinstall
70
+ end
65
71
  end
66
72
  end
67
73
 
@@ -77,7 +83,11 @@ class R10K::Module::Forge < R10K::Module::Base
77
83
  def expected_version
78
84
  if @expected_version == :latest
79
85
  begin
80
- @expected_version = @v3_module.current_release.version
86
+ if @v3_module.current_release
87
+ @expected_version = @v3_module.current_release.version
88
+ else
89
+ raise PuppetForge::ReleaseNotFound, _("The module %{title} does not appear to have any published releases, cannot determine latest version.") % { title: @title }
90
+ end
81
91
  rescue Faraday::ResourceNotFound => e
82
92
  raise PuppetForge::ReleaseNotFound, _("The module %{title} does not exist on %{url}.") % {title: @title, url: PuppetForge::V3::Release.conn.url_prefix}, e.backtrace
83
93
  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) && (args.has_key?(:git) || args[:type].to_s == 'git')
11
+ args.has_key?(:git) || args[:type].to_s == 'git'
12
12
  rescue
13
13
  false
14
14
  end
@@ -36,27 +36,35 @@ class R10K::Module::Git < R10K::Module::Base
36
36
  include R10K::Util::Setopts
37
37
 
38
38
  def initialize(title, dirname, opts, environment=nil)
39
+
39
40
  super
40
41
  setopts(opts, {
41
42
  # Standard option interface
42
- :version => :desired_ref,
43
- :source => :remote,
44
- :type => ::R10K::Util::Setopts::Ignore,
43
+ :version => :desired_ref,
44
+ :source => :remote,
45
+ :type => ::R10K::Util::Setopts::Ignore,
45
46
 
46
47
  # Type-specific options
47
- :branch => :desired_ref,
48
- :tag => :desired_ref,
49
- :commit => :desired_ref,
50
- :ref => :desired_ref,
51
- :git => :remote,
48
+ :branch => :desired_ref,
49
+ :tag => :desired_ref,
50
+ :commit => :desired_ref,
51
+ :ref => :desired_ref,
52
+ :git => :remote,
52
53
  :default_branch => :default_ref,
53
54
  :default_branch_override => :default_override_ref,
54
- })
55
+ }, :raise_on_unhandled => false)
56
+
57
+ force = @overrides.dig(:modules, :force)
58
+ @force = force == false ? false : true
55
59
 
56
60
  @desired_ref ||= 'master'
57
61
 
58
- if @desired_ref == :control_branch && @environment && @environment.respond_to?(:ref)
59
- @desired_ref = @environment.ref
62
+ if @desired_ref == :control_branch
63
+ if @environment && @environment.respond_to?(:ref)
64
+ @desired_ref = @environment.ref
65
+ else
66
+ logger.warn _("Cannot track control repo branch for content '%{name}' when not part of a git-backed environment, will use default if available." % {name: name})
67
+ end
60
68
  end
61
69
 
62
70
  @repo = R10K::Git::StatefulRepository.new(@remote, @dirname, @name)
@@ -74,9 +82,10 @@ class R10K::Module::Git < R10K::Module::Base
74
82
  }
75
83
  end
76
84
 
85
+ # @param [Hash] opts Deprecated
77
86
  def sync(opts={})
78
- force = opts && opts.fetch(:force, true)
79
- @repo.sync(version, force)
87
+ force = opts[:force] || @force
88
+ @repo.sync(version, force) if should_sync?
80
89
  end
81
90
 
82
91
  def status
@@ -30,6 +30,7 @@ class R10K::Module::Local < R10K::Module::Base
30
30
  :insync
31
31
  end
32
32
 
33
+ # @param [Hash] opts Deprecated
33
34
  def sync(opts={})
34
35
  logger.debug1 _("Module %{title} is a local module, always indicating synced.") % {title: title}
35
36
  end
@@ -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) && (args.has_key?(:svn) || args[:type].to_s == 'svn')
10
+ args.has_key?(:svn) || args[:type].to_s == 'svn'
11
11
  end
12
12
 
13
13
  # @!attribute [r] expected_revision
@@ -50,7 +50,7 @@ class R10K::Module::SVN < R10K::Module::Base
50
50
  :revision => :expected_revision,
51
51
  :username => :self,
52
52
  :password => :self
53
- })
53
+ }, :raise_on_unhandled => false)
54
54
 
55
55
  @working_dir = R10K::SVN::WorkingDir.new(@path, :username => @username, :password => @password)
56
56
  end
@@ -69,14 +69,17 @@ class R10K::Module::SVN < R10K::Module::Base
69
69
  end
70
70
  end
71
71
 
72
+ # @param [Hash] opts Deprecated
72
73
  def sync(opts={})
73
- case status
74
- when :absent
75
- install
76
- when :mismatched
77
- reinstall
78
- when :outdated
79
- update
74
+ if should_sync?
75
+ case status
76
+ when :absent
77
+ install
78
+ when :mismatched
79
+ reinstall
80
+ when :outdated
81
+ update
82
+ end
80
83
  end
81
84
  end
82
85
 
@@ -0,0 +1,195 @@
1
+ module R10K
2
+ module ModuleLoader
3
+ class Puppetfile
4
+
5
+ DEFAULT_MODULEDIR = 'modules'
6
+ DEFAULT_PUPPETFILE_NAME = 'Puppetfile'
7
+ DEFAULT_FORGE_API = 'forgeapi.puppetlabs.com'
8
+
9
+ attr_accessor :default_branch_override, :environment
10
+ attr_reader :modules, :moduledir,
11
+ :managed_directories, :desired_contents, :purge_exclusions
12
+
13
+ # @param basedir [String] The path that contains the moduledir &
14
+ # Puppetfile by default. May be an environment, project, or
15
+ # simple directory.
16
+ # @param puppetfile [String] The path to the Puppetfile, either an
17
+ # absolute full path or a relative path with regards to the basedir.
18
+ # @param moduledir [String] The path to the moduledir, either an
19
+ # absolute full path or a relative path with regards to the basedir.
20
+ # @param forge [String] The url (without protocol) to the Forge
21
+ # @param overrides [Hash] Configuration for loaded modules' behavior
22
+ # @param environment [R10K::Environment] When provided, the environment
23
+ # in which loading takes place
24
+ def initialize(basedir:,
25
+ moduledir: DEFAULT_MODULEDIR,
26
+ puppetfile: DEFAULT_PUPPETFILE_NAME,
27
+ forge: DEFAULT_FORGE_API,
28
+ overrides: {},
29
+ environment: nil)
30
+
31
+ @basedir = cleanpath(basedir)
32
+ @moduledir = resolve_path(@basedir, moduledir)
33
+ @puppetfile = resolve_path(@basedir, puppetfile)
34
+ @forge = forge
35
+ @overrides = overrides
36
+ @environment = environment
37
+ @default_branch_override = @overrides.dig(:environments, :default_branch_override)
38
+
39
+ @modules = []
40
+
41
+ @managed_directories = []
42
+ @desired_contents = []
43
+ @purge_exclusions = []
44
+ end
45
+
46
+ def load
47
+ dsl = R10K::ModuleLoader::Puppetfile::DSL.new(self)
48
+ dsl.instance_eval(puppetfile_content(@puppetfile), @puppetfile)
49
+
50
+ validate_no_duplicate_names(@modules)
51
+ @modules
52
+
53
+ managed_content = @modules.group_by(&:dirname)
54
+
55
+ @managed_directories = determine_managed_directories(managed_content)
56
+ @desired_contents = determine_desired_contents(managed_content)
57
+ @purge_exclusions = determine_purge_exclusions(@managed_directories)
58
+
59
+ {
60
+ modules: @modules,
61
+ managed_directories: @managed_directories,
62
+ desired_contents: @desired_contents,
63
+ purge_exclusions: @purge_exclusions
64
+ }
65
+
66
+ rescue SyntaxError, LoadError, ArgumentError, NameError => e
67
+ raise R10K::Error.wrap(e, _("Failed to evaluate %{path}") % {path: @puppetfile})
68
+ end
69
+
70
+
71
+ ##
72
+ ## set_forge, set_moduledir, and add_module are used directly by the DSL class
73
+ ##
74
+
75
+ # @param [String] forge
76
+ def set_forge(forge)
77
+ @forge = forge
78
+ end
79
+
80
+ # @param [String] moduledir
81
+ def set_moduledir(moduledir)
82
+ @moduledir = resolve_path(@basedir, moduledir)
83
+ end
84
+
85
+ # @param [String] name
86
+ # @param [Hash, String, Symbol, nil] module_info Calling with
87
+ # anything but a Hash is deprecated. The DSL will now convert
88
+ # String and Symbol versions to Hashes of the shape
89
+ # { version: <String or Symbol> }
90
+ #
91
+ # String inputs should be valid module versions, the Symbol
92
+ # `:latest` is allowed, as well as `nil`.
93
+ #
94
+ # Non-Hash inputs are only ever used by Forge modules. In
95
+ # future versions this method will require the caller (the
96
+ # DSL class, not the Puppetfile author) to do this conversion
97
+ # itself.
98
+ #
99
+ def add_module(name, module_info)
100
+ if !module_info.is_a?(Hash)
101
+ module_info = { version: module_info }
102
+ end
103
+
104
+ module_info[:overrides] = @overrides
105
+
106
+ if install_path = module_info.delete(:install_path)
107
+ install_path = resolve_path(@basedir, install_path)
108
+ validate_install_path(install_path, name)
109
+ else
110
+ install_path = @moduledir
111
+ end
112
+
113
+ if @default_branch_override
114
+ module_info[:default_branch_override] = @default_branch_override
115
+ end
116
+
117
+ mod = R10K::Module.new(name, install_path, module_info, @environment)
118
+ mod.origin = :puppetfile
119
+
120
+ # Do not save modules if they would conflict with the attached
121
+ # environment
122
+ if @environment && @environment.module_conflicts?(mod)
123
+ return @modules
124
+ end
125
+
126
+ @modules << mod
127
+ end
128
+
129
+ private
130
+
131
+ # @param [Array<R10K::Module>] modules
132
+ def validate_no_duplicate_names(modules)
133
+ dupes = modules
134
+ .group_by { |mod| mod.name }
135
+ .select { |_, mods| mods.size > 1 }
136
+ .map(&:first)
137
+ unless dupes.empty?
138
+ msg = _('Puppetfiles cannot contain duplicate module names.')
139
+ msg += ' '
140
+ msg += _("Remove the duplicates of the following modules: %{dupes}" % { dupes: dupes.join(' ') })
141
+ raise R10K::Error.new(msg)
142
+ end
143
+ end
144
+
145
+ def resolve_path(base, path)
146
+ if Pathname.new(path).absolute?
147
+ cleanpath(path)
148
+ else
149
+ cleanpath(File.join(base, path))
150
+ end
151
+ end
152
+
153
+ def validate_install_path(path, modname)
154
+ unless /^#{Regexp.escape(@basedir)}.*/ =~ path
155
+ raise R10K::Error.new("Puppetfile cannot manage content '#{modname}' outside of containing environment: #{path} is not within #{@basedir}")
156
+ end
157
+
158
+ true
159
+ end
160
+
161
+ def determine_managed_directories(managed_content)
162
+ managed_content.keys.reject { |dir| dir == @basedir }
163
+ end
164
+
165
+ # Returns an array of the full paths to all the content being managed.
166
+ # @return [Array<String>]
167
+ def determine_desired_contents(managed_content)
168
+ managed_content.flat_map do |install_path, mods|
169
+ mods.collect { |mod| File.join(install_path, mod.name) }
170
+ end
171
+ end
172
+
173
+ def determine_purge_exclusions(managed_dirs)
174
+ if environment && environment.respond_to?(:desired_contents)
175
+ managed_dirs + environment.desired_contents
176
+ else
177
+ managed_dirs
178
+ end
179
+ end
180
+
181
+ # .cleanpath is as close to a canonical path as we can do without touching
182
+ # the filesystem. The .realpath methods will choke if some of the
183
+ # intermediate paths are missing, even though in some cases we will create
184
+ # them later as needed.
185
+ def cleanpath(path)
186
+ Pathname.new(path).cleanpath.to_s
187
+ end
188
+
189
+ # For testing purposes only
190
+ def puppetfile_content(path)
191
+ File.read(path)
192
+ end
193
+ end
194
+ end
195
+ end