r10k 3.9.0 → 3.10.0

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