r10k 2.3.1 → 2.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (104) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/CHANGELOG.mkd +40 -13
  4. data/Gemfile +1 -2
  5. data/README.mkd +25 -0
  6. data/Rakefile +3 -0
  7. data/doc/dynamic-environments/configuration.mkd +91 -14
  8. data/doc/dynamic-environments/workflow-guide.mkd +3 -3
  9. data/doc/puppetfile.mkd +89 -40
  10. data/integration/Gemfile +2 -0
  11. data/integration/Rakefile +41 -0
  12. data/integration/tests/purging/content_not_purged_at_root.rb +89 -0
  13. data/integration/tests/purging/default_purging.rb +125 -0
  14. data/integration/tests/user_scenario/basic_workflow/negative/neg_bad_git_module_ref.rb +1 -1
  15. data/lib/r10k.rb +8 -1
  16. data/lib/r10k/action/deploy/deploy_helpers.rb +3 -3
  17. data/lib/r10k/action/deploy/environment.rb +35 -14
  18. data/lib/r10k/action/deploy/module.rb +4 -4
  19. data/lib/r10k/action/puppetfile/check.rb +1 -1
  20. data/lib/r10k/action/puppetfile/install.rb +1 -1
  21. data/lib/r10k/action/runner.rb +3 -3
  22. data/lib/r10k/deployment.rb +8 -4
  23. data/lib/r10k/deployment/config.rb +1 -1
  24. data/lib/r10k/environment/base.rb +32 -3
  25. data/lib/r10k/environment/git.rb +19 -6
  26. data/lib/r10k/feature.rb +6 -6
  27. data/lib/r10k/forge/module_release.rb +4 -6
  28. data/lib/r10k/git.rb +7 -7
  29. data/lib/r10k/git/alternates.rb +1 -1
  30. data/lib/r10k/git/cache.rb +2 -2
  31. data/lib/r10k/git/rugged/bare_repository.rb +29 -9
  32. data/lib/r10k/git/rugged/credentials.rb +8 -8
  33. data/lib/r10k/git/rugged/thin_repository.rb +17 -3
  34. data/lib/r10k/git/rugged/working_repository.rb +12 -5
  35. data/lib/r10k/git/shellgit/thin_repository.rb +5 -1
  36. data/lib/r10k/git/shellgit/working_repository.rb +16 -2
  37. data/lib/r10k/git/stateful_repository.rb +33 -23
  38. data/lib/r10k/initializers.rb +1 -1
  39. data/lib/r10k/keyed_factory.rb +2 -2
  40. data/lib/r10k/logging.rb +1 -1
  41. data/lib/r10k/module.rb +4 -3
  42. data/lib/r10k/module/base.rb +3 -2
  43. data/lib/r10k/module/forge.rb +9 -4
  44. data/lib/r10k/module/git.rb +39 -22
  45. data/lib/r10k/module/local.rb +1 -1
  46. data/lib/r10k/module/metadata_file.rb +1 -1
  47. data/lib/r10k/module/svn.rb +3 -1
  48. data/lib/r10k/puppetfile.rb +72 -11
  49. data/lib/r10k/settings.rb +20 -0
  50. data/lib/r10k/settings/collection.rb +2 -2
  51. data/lib/r10k/settings/container.rb +1 -1
  52. data/lib/r10k/settings/enum_definition.rb +11 -3
  53. data/lib/r10k/settings/helpers.rb +2 -2
  54. data/lib/r10k/settings/list.rb +1 -1
  55. data/lib/r10k/settings/loader.rb +6 -6
  56. data/lib/r10k/settings/uri_definition.rb +1 -1
  57. data/lib/r10k/source/base.rb +2 -2
  58. data/lib/r10k/source/git.rb +4 -4
  59. data/lib/r10k/svn/working_dir.rb +1 -1
  60. data/lib/r10k/util/basedir.rb +9 -9
  61. data/lib/r10k/util/license.rb +3 -3
  62. data/lib/r10k/util/purgeable.rb +50 -24
  63. data/lib/r10k/util/setopts.rb +1 -1
  64. data/lib/r10k/util/subprocess.rb +3 -3
  65. data/lib/r10k/util/symbolize_keys.rb +1 -1
  66. data/lib/r10k/version.rb +1 -1
  67. data/locales/config.yaml +21 -0
  68. data/r10k.gemspec +3 -0
  69. data/spec/fixtures/unit/puppetfile/valid-forge-with-version/Puppetfile +1 -0
  70. data/spec/fixtures/unit/puppetfile/valid-forge-without-version/Puppetfile +1 -0
  71. data/spec/fixtures/unit/util/purgeable/managed_one/expected_1 +0 -0
  72. data/spec/fixtures/unit/util/purgeable/managed_one/managed_subdir_1/subdir_expected_1 +0 -0
  73. data/spec/fixtures/unit/util/purgeable/managed_one/managed_subdir_1/subdir_unmanaged_1 +0 -0
  74. data/spec/fixtures/unit/util/purgeable/managed_one/unmanaged_1 +0 -0
  75. data/spec/fixtures/unit/util/purgeable/managed_two/expected_2 +0 -0
  76. data/spec/fixtures/unit/util/purgeable/managed_two/unmanaged_2 +0 -0
  77. data/spec/integration/git/stateful_repository_spec.rb +39 -21
  78. data/spec/r10k-mocks/mock_config.rb +4 -0
  79. data/spec/spec_helper.rb +3 -0
  80. data/spec/unit/action/deploy/environment_spec.rb +71 -13
  81. data/spec/unit/environment/base_spec.rb +71 -0
  82. data/spec/unit/forge/module_release_spec.rb +7 -10
  83. data/spec/unit/git/stateful_repository_spec.rb +6 -6
  84. data/spec/unit/module/git_spec.rb +156 -5
  85. data/spec/unit/module_spec.rb +3 -3
  86. data/spec/unit/puppetfile_spec.rb +115 -0
  87. data/spec/unit/util/purgeable_spec.rb +230 -0
  88. metadata +45 -18
  89. data/integration/test_run_scripts/README.mkd +0 -5
  90. data/integration/test_run_scripts/all_tests-rugged-pe-centos6.sh +0 -20
  91. data/integration/test_run_scripts/all_tests-rugged-pe-rhel7.sh +0 -20
  92. data/integration/test_run_scripts/all_tests-rugged-pe-sles11.sh +0 -20
  93. data/integration/test_run_scripts/all_tests-rugged-pe-ubuntu1204.sh +0 -20
  94. data/integration/test_run_scripts/all_tests-rugged-pe-ubuntu1404.sh +0 -20
  95. data/integration/test_run_scripts/all_tests-shellgit-pe-centos6.sh +0 -20
  96. data/integration/test_run_scripts/all_tests-shellgit-pe-rhel7.sh +0 -20
  97. data/integration/test_run_scripts/all_tests-shellgit-pe-sles11.sh +0 -20
  98. data/integration/test_run_scripts/all_tests-shellgit-pe-ubuntu1204.sh +0 -20
  99. data/integration/test_run_scripts/all_tests-shellgit-pe-ubuntu1404.sh +0 -20
  100. data/integration/test_run_scripts/basic_functionality/all_tests-pe-centos6.sh +0 -20
  101. data/integration/test_run_scripts/command_line/all_tests-pe-centos6.sh +0 -20
  102. data/integration/test_run_scripts/git_source/all_tests-pe-centos6.sh +0 -20
  103. data/integration/test_run_scripts/user_scenario/basic_workflow/all_tests-pe-centos6.sh +0 -20
  104. data/integration/test_run_scripts/user_scenario/complex_workflow/all_tests-pe-centos6.sh +0 -20
@@ -29,30 +29,39 @@ class Puppetfile
29
29
  # @return [String] The path to the Puppetfile
30
30
  attr_reader :puppetfile_path
31
31
 
32
+ # @!attribute [rw] environment
33
+ # @return [R10K::Environment] Optional R10K::Environment that this Puppetfile belongs to.
34
+ attr_accessor :environment
35
+
32
36
  # @param [String] basedir
33
- # @param [String] puppetfile The path to the Puppetfile, default to #{basedir}/Puppetfile
34
- def initialize(basedir, moduledir = nil, puppetfile = nil)
37
+ # @param [String] moduledir The directory to install the modules, default to #{basedir}/modules
38
+ # @param [String] puppetfile_path The path to the Puppetfile, default to #{basedir}/Puppetfile
39
+ def initialize(basedir, moduledir = nil, puppetfile_path = nil)
35
40
  @basedir = basedir
36
41
  @moduledir = moduledir || File.join(basedir, 'modules')
37
- @puppetfile_path = puppetfile || File.join(basedir, 'Puppetfile')
42
+ @puppetfile_path = puppetfile_path || File.join(basedir, 'Puppetfile')
38
43
 
39
44
  @modules = []
45
+ @managed_content = {}
40
46
  @forge = 'forgeapi.puppetlabs.com'
47
+
48
+ @loaded = false
41
49
  end
42
50
 
43
51
  def load
44
52
  if File.readable? @puppetfile_path
45
53
  self.load!
46
54
  else
47
- logger.debug "Puppetfile #{@puppetfile_path.inspect} missing or unreadable"
55
+ logger.debug _("Puppetfile %{path} missing or unreadable") % {path: @puppetfile_path.inspect}
48
56
  end
49
57
  end
50
58
 
51
59
  def load!
52
60
  dsl = R10K::Puppetfile::DSL.new(self)
53
61
  dsl.instance_eval(puppetfile_contents, @puppetfile_path)
62
+ @loaded = true
54
63
  rescue SyntaxError, LoadError, ArgumentError => e
55
- raise R10K::Error.wrap(e, "Failed to evaluate #{@puppetfile_path}")
64
+ raise R10K::Error.wrap(e, _("Failed to evaluate %{path}") % {path: @puppetfile_path})
56
65
  end
57
66
 
58
67
  # @param [String] forge
@@ -72,20 +81,49 @@ class Puppetfile
72
81
  # @param [String] name
73
82
  # @param [*Object] args
74
83
  def add_module(name, args)
75
- @modules << R10K::Module.new(name, @moduledir, args)
84
+ if args.is_a?(Hash) && install_path = args.delete(:install_path)
85
+ install_path = resolve_install_path(install_path)
86
+ validate_install_path(install_path, name)
87
+ else
88
+ install_path = @moduledir
89
+ end
90
+
91
+ # Keep track of all the content this Puppetfile is managing to enable purging.
92
+ @managed_content[install_path] = Array.new unless @managed_content.has_key?(install_path)
93
+
94
+ mod = R10K::Module.new(name, install_path, args, @environment)
95
+
96
+ @managed_content[install_path] << mod.name
97
+ @modules << mod
76
98
  end
77
99
 
78
100
  include R10K::Util::Purgeable
79
101
 
80
- def managed_directory
81
- @moduledir
102
+ def managed_directories
103
+ self.load unless @loaded
104
+
105
+ @managed_content.keys
82
106
  end
83
107
 
84
- # List all modules that should exist in the module directory
108
+ # Returns an array of the full paths to all the content being managed.
85
109
  # @note This implements a required method for the Purgeable mixin
86
110
  # @return [Array<String>]
87
111
  def desired_contents
88
- @modules.map { |mod| mod.name }
112
+ self.load unless @loaded
113
+
114
+ @managed_content.flat_map do |install_path, modnames|
115
+ modnames.collect { |name| File.join(install_path, name) }
116
+ end
117
+ end
118
+
119
+ def purge_exclusions
120
+ exclusions = managed_directories
121
+
122
+ if environment && environment.respond_to?(:desired_contents)
123
+ exclusions += environment.desired_contents
124
+ end
125
+
126
+ exclusions
89
127
  end
90
128
 
91
129
  def accept(visitor)
@@ -102,6 +140,29 @@ class Puppetfile
102
140
  File.read(@puppetfile_path)
103
141
  end
104
142
 
143
+ def resolve_install_path(path)
144
+ pn = Pathname.new(path)
145
+
146
+ unless pn.absolute?
147
+ pn = Pathname.new(File.join(basedir, path))
148
+ end
149
+
150
+ # .cleanpath is as good as we can do without touching the filesystem.
151
+ # The .realpath methods will also choke if some of the intermediate
152
+ # paths are missing, even though we will create them later as needed.
153
+ pn.cleanpath.to_s
154
+ end
155
+
156
+ def validate_install_path(path, modname)
157
+ real_basedir = Pathname.new(basedir).cleanpath.to_s
158
+
159
+ unless /^#{Regexp.escape(real_basedir)}.*/ =~ path
160
+ raise R10K::Error.new("Puppetfile cannot manage content '#{modname}' outside of containing environment: #{path} is not within #{real_basedir}")
161
+ end
162
+
163
+ true
164
+ end
165
+
105
166
  class DSL
106
167
  # A barebones implementation of the Puppetfile DSL
107
168
  #
@@ -124,7 +185,7 @@ class Puppetfile
124
185
  end
125
186
 
126
187
  def method_missing(method, *args)
127
- raise NoMethodError, "unrecognized declaration '#{method}'"
188
+ raise NoMethodError, _("unrecognized declaration '%{method}'") % {method: method}
128
189
  end
129
190
  end
130
191
  end
@@ -84,6 +84,26 @@ module R10K
84
84
  end
85
85
  end
86
86
  }),
87
+
88
+ EnumDefinition.new(:purge_levels, {
89
+ :desc => "Controls how aggressively r10k will purge unmanaged content from the target directory. Should be a list of values indicating at what levels unmanaged content should be purged. Options are 'deployment', 'environment', and 'puppetfile'. For backwards compatibility, the default is ['deployment', 'puppetfile'].",
90
+ :multi => true,
91
+ :enum => [:deployment, :environment, :puppetfile],
92
+ :default => [:deployment, :puppetfile],
93
+ :normalize => lambda do |input|
94
+ if input.respond_to?(:collect)
95
+ input.collect { |val| val.to_sym }
96
+ else
97
+ # Convert single values to a list of one symbolized value.
98
+ [input.to_sym]
99
+ end
100
+ end,
101
+ }),
102
+
103
+ Definition.new(:purge_whitelist, {
104
+ :desc => "A list of filename patterns to be excluded from any purge operations. Patterns are matched relative to the root of each deployed environment, if you want a pattern to match recursively you need to use the '**' glob in your pattern. Basic shell style globs are supported.",
105
+ :default => [],
106
+ }),
87
107
  ])
88
108
  end
89
109
 
@@ -74,9 +74,9 @@ module R10K
74
74
 
75
75
  if !errors.empty?
76
76
  if @name
77
- msg = "Validation failed for '#{@name}' settings group"
77
+ msg = _("Validation failed for '%{name}' settings group") % {name: @name}
78
78
  else
79
- msg = "Validation failed for settings group"
79
+ msg = _("Validation failed for settings group")
80
80
  end
81
81
 
82
82
  raise ValidationError.new(msg, :errors => errors)
@@ -84,7 +84,7 @@ class R10K::Settings::Container
84
84
 
85
85
  def validate_key!(key)
86
86
  unless valid_key?(key)
87
- raise InvalidKey, "Key #{key} is not a valid key"
87
+ raise InvalidKey, _("Key %{key} is not a valid key") % {key: key}
88
88
  end
89
89
  end
90
90
 
@@ -6,8 +6,16 @@ module R10K
6
6
 
7
7
  def validate
8
8
  if @value
9
- if !@enum.include?(@value)
10
- raise ArgumentError, "Setting #{@name} should be one of #{@enum.inspect}, not '#{@value}'"
9
+ if @multi && @value.respond_to?(:select)
10
+ invalid = @value.select { |val| !@enum.include?(val) }
11
+
12
+ if invalid.size > 0
13
+ raise ArgumentError, _("Setting %{name} may only contain %{enums}; the disallowed values %{invalid} were present") % {name: @name, enums: @enum.inspect, invalid: invalid.inspect}
14
+ end
15
+ else
16
+ if !@enum.include?(@value)
17
+ raise ArgumentError, _("Setting %{name} should be one of %{enums}, not '%{value}'") % {name: @name, enums: @enum.inspect, value: @value}
18
+ end
11
19
  end
12
20
  end
13
21
  end
@@ -15,7 +23,7 @@ module R10K
15
23
  private
16
24
 
17
25
  def allowed_initialize_opts
18
- super.merge({:enum => true})
26
+ super.merge({:enum => true, :multi => true})
19
27
  end
20
28
  end
21
29
  end
@@ -16,11 +16,11 @@ module R10K
16
16
  # @param new_parent [R10K::Settings::Collection] Parent collection
17
17
  def parent=(new_parent)
18
18
  unless @parent.nil?
19
- raise R10K::Error.new("#{self.class} instances cannot be reassigned to a new parent.")
19
+ raise R10K::Error.new(_("%{class} instances cannot be reassigned to a new parent.") % {class: self.class} )
20
20
  end
21
21
 
22
22
  unless new_parent.is_a?(R10K::Settings::Collection) || new_parent.is_a?(R10K::Settings::List)
23
- raise R10K::Error.new("#{self.class} instances may only belong to a settings collection or list.")
23
+ raise R10K::Error.new(_("%{class} instances may only belong to a settings collection or list.") % {class: self.class} )
24
24
  end
25
25
 
26
26
  @parent = new_parent
@@ -63,7 +63,7 @@ module R10K
63
63
  end
64
64
 
65
65
  if !errors.empty?
66
- raise ValidationError.new("Validation failed for '#{@name}' settings list", :errors => errors)
66
+ raise ValidationError.new(_("Validation failed for '%{name}' settings list") % {name: @name}, :errors => errors)
67
67
  end
68
68
  end
69
69
 
@@ -42,15 +42,15 @@ module R10K
42
42
 
43
43
  # If both default files are present, issue a warning.
44
44
  if (File.file? DEFAULT_LOCATION) && (File.file? OLD_DEFAULT_LOCATION)
45
- logger.warn "Both #{DEFAULT_LOCATION} and #{OLD_DEFAULT_LOCATION} configuration files exist."
46
- logger.warn "#{DEFAULT_LOCATION} will be used."
45
+ logger.warn _("Both %{default_path} and %{old_default_path} configuration files exist.") % {default_path: DEFAULT_LOCATION, old_default_path: OLD_DEFAULT_LOCATION}
46
+ logger.warn _("%{default_path} will be used.") % {default_path: DEFAULT_LOCATION}
47
47
  end
48
48
 
49
49
  path = @loadpath.find {|filename| File.file? filename}
50
50
 
51
51
  if path == OLD_DEFAULT_LOCATION
52
- logger.warn "The r10k configuration file at #{OLD_DEFAULT_LOCATION} is deprecated."
53
- logger.warn "Please move your r10k configuration to #{DEFAULT_LOCATION}."
52
+ logger.warn _("The r10k configuration file at %{old_default_path} is deprecated.") % {old_default_path: OLD_DEFAULT_LOCATION}
53
+ logger.warn _("Please move your r10k configuration to %{default_path}.") % {default_path: DEFAULT_LOCATION}
54
54
  end
55
55
 
56
56
  path
@@ -60,13 +60,13 @@ module R10K
60
60
  path = search(override)
61
61
 
62
62
  if path.nil?
63
- raise ConfigError, "No configuration file given, no config file found in current directory, and no global config present"
63
+ raise ConfigError, _("No configuration file given, no config file found in current directory, and no global config present")
64
64
  end
65
65
 
66
66
  begin
67
67
  contents = ::YAML.load_file(path)
68
68
  rescue => e
69
- raise ConfigError, "Couldn't load config file: #{e.message}"
69
+ raise ConfigError, _("Couldn't load config file: %{error_msg}") % {error_msg: e.message}
70
70
  end
71
71
 
72
72
  R10K::Util::SymbolizeKeys.symbolize_keys!(contents, true)
@@ -9,7 +9,7 @@ module R10K
9
9
  begin
10
10
  URI.parse(@value)
11
11
  rescue URI::Error
12
- raise ArgumentError, "Setting #{@name} requires a URL but '#{@value}' could not be parsed as a URL"
12
+ raise ArgumentError, _("Setting %{name} requires a URL but '%{value}' could not be parsed as a URL") % {name: @name, value: @value}
13
13
  end
14
14
  end
15
15
  super
@@ -28,7 +28,7 @@ class R10K::Source::Base
28
28
  # Defaults to false for no environment prefix.
29
29
  def initialize(name, basedir, options = {})
30
30
  @name = name
31
- @basedir = basedir
31
+ @basedir = Pathname.new(basedir).cleanpath.to_s
32
32
  @prefix = options.delete(:prefix)
33
33
  @options = options
34
34
  end
@@ -55,7 +55,7 @@ class R10K::Source::Base
55
55
  # @return [Array<R10K::Environment::Base>] An array of environments created
56
56
  # from this source.
57
57
  def environments
58
- raise NotImplementedError, "#{self.class} has not implemented method #{__method__}"
58
+ raise NotImplementedError, _("%{class} has not implemented method %{method}") % {class: self.class, method: __method__}
59
59
  end
60
60
 
61
61
  def accept(visitor)
@@ -63,10 +63,10 @@ class R10K::Source::Git < R10K::Source::Base
63
63
  #
64
64
  # @return [void]
65
65
  def preload!
66
- logger.debug "Fetching '#{@remote}' to determine current branches."
66
+ logger.debug _("Fetching '%{remote}' to determine current branches.") % {remote: @remote}
67
67
  @cache.sync
68
68
  rescue => e
69
- raise R10K::Error.wrap(e, "Unable to determine current branches for Git source '#{@name}' (#{@basedir})")
69
+ raise R10K::Error.wrap(e, _("Unable to determine current branches for Git source '%{name}' (%{basedir})") % {name: @name, basedir: @basedir})
70
70
  end
71
71
  alias fetch_remote preload!
72
72
 
@@ -91,11 +91,11 @@ class R10K::Source::Git < R10K::Source::Base
91
91
  envs << R10K::Environment::Git.new(bn.name, @basedir, bn.dirname,
92
92
  {:remote => remote, :ref => bn.name})
93
93
  elsif bn.correct?
94
- logger.warn "Environment #{bn.name.inspect} contained non-word characters, correcting name to #{bn.dirname}"
94
+ logger.warn _("Environment %{env_name} contained non-word characters, correcting name to %{corrected_env_name}") % {env_name: bn.name.inspect, corrected_env_name: bn.dirname}
95
95
  envs << R10K::Environment::Git.new(bn.name, @basedir, bn.dirname,
96
96
  {:remote => remote, :ref => bn.name})
97
97
  elsif bn.validate?
98
- logger.error "Environment #{bn.name.inspect} contained non-word characters, ignoring it."
98
+ logger.error _("Environment %{env_name} contained non-word characters, ignoring it.") % {env_name: bn.name.inspect}
99
99
  end
100
100
  end
101
101
 
@@ -40,7 +40,7 @@ module R10K
40
40
  setopts(opts, {:username => :self, :password => :self})
41
41
 
42
42
  if !!(@username) ^ !!(@password)
43
- raise ArgumentError, "Both username and password must be specified"
43
+ raise ArgumentError, _("Both username and password must be specified")
44
44
  end
45
45
  end
46
46
 
@@ -31,7 +31,7 @@ module R10K
31
31
  # @param sources [Array<#desired_contents>] A list of objects that may create filesystem entries
32
32
  def initialize(path, sources)
33
33
  if sources.is_a? R10K::Deployment
34
- raise ArgumentError, "Expected Array<#desired_contents>, got R10K::Deployment"
34
+ raise ArgumentError, _("Expected Array<#desired_contents>, got R10K::Deployment")
35
35
  end
36
36
  @path = path
37
37
  @sources = sources
@@ -39,25 +39,25 @@ module R10K
39
39
 
40
40
  # Return the path of the basedir
41
41
  # @note This implements a required method for the Purgeable mixin
42
- # @return [String]
43
- def managed_directory
44
- @path
42
+ # @return [Array]
43
+ def managed_directories
44
+ [@path]
45
45
  end
46
46
 
47
47
  # List all environments that should exist in this basedir
48
48
  # @note This implements a required method for the Purgeable mixin
49
49
  # @return [Array<String>]
50
50
  def desired_contents
51
- @sources.map(&:desired_contents).flatten
51
+ @sources.flat_map do |src|
52
+ src.desired_contents.collect { |env| File.join(@path, env) }
53
+ end
52
54
  end
53
55
 
54
56
  def purge!
55
57
  @sources.each do |source|
56
- logger.debug1 "Source #{source.name} in #{@path} manages contents #{source.desired_contents.inspect}"
57
- end
58
- if !stale_contents.empty?
59
- logger.debug "The path #{@path} has unmanaged contents #{stale_contents.inspect}"
58
+ logger.debug1 _("Source %{source_name} in %{path} manages contents %{contents}") % {source_name: source.name, path: @path, contents: source.desired_contents.inspect}
60
59
  end
60
+
61
61
  super
62
62
  end
63
63
  end
@@ -8,14 +8,14 @@ module R10K
8
8
 
9
9
  def self.load
10
10
  if R10K::Features.available?(:pe_license)
11
- logger.debug2 "pe_license feature is available, loading PE license key"
11
+ logger.debug2 _("pe_license feature is available, loading PE license key")
12
12
  begin
13
13
  return PELicense.load_license_key
14
14
  rescue PELicense::InvalidLicenseError => e
15
- raise R10K::Error.wrap(e, "Invalid PE license detected: #{e.message}")
15
+ raise R10K::Error.wrap(e, _("Invalid PE license detected: %{error_msg}") % {error_msg: e.message} )
16
16
  end
17
17
  else
18
- logger.debug2 "pe_license feature is not available, PE only Puppet modules will not be downloadable."
18
+ logger.debug2 _("pe_license feature is not available, PE only Puppet modules will not be downloadable.")
19
19
  nil
20
20
  end
21
21
  end
@@ -15,43 +15,69 @@ module R10K
15
15
 
16
16
  # @!method desired_contents
17
17
  # @abstract Including classes must implement this method to list the
18
- # expected filenames of managed_directory
19
- # @return [Array<String>] A list of directory contents that should be present
18
+ # expected filenames of managed_directories
19
+ # @return [Array<String>] The full paths to all the content this object is managing
20
20
 
21
- # @!method managed_directory
22
- # @abstract Including classes must implement this method to return the
23
- # path to the directory that can be purged
24
- # @return [String] The path to the directory to be purged
21
+ # @!method managed_directories
22
+ # @abstract Including classes must implement this method to return an array of
23
+ # paths that can be purged
24
+ # @return [Array<String>] The paths to the directories to be purged
25
25
 
26
- # @return [Array<String>] The present directory entries in `self.managed_directory`
27
- def current_contents
28
- dir = self.managed_directory
29
- glob_exp = File.join(dir, '*')
26
+ # @return [Array<String>] The present directory entries in `self.managed_directories`
27
+ def current_contents(recurse)
28
+ dirs = self.managed_directories
30
29
 
31
- Dir.glob(glob_exp).map do |fname|
32
- File.basename(fname)
30
+ dirs.flat_map do |dir|
31
+ if recurse
32
+ glob_exp = File.join(dir, '**', '{*,.[^.]*}')
33
+ else
34
+ glob_exp = File.join(dir, '*')
35
+ end
36
+
37
+ Dir.glob(glob_exp)
33
38
  end
34
39
  end
35
40
 
36
41
  # @return [Array<String>] Directory contents that are expected but not present
37
- def pending_contents
38
- desired_contents - current_contents
42
+ def pending_contents(recurse)
43
+ desired_contents - current_contents(recurse)
39
44
  end
40
45
 
41
46
  # @return [Array<String>] Directory contents that are present but not expected
42
- def stale_contents
43
- current_contents - desired_contents
47
+ def stale_contents(recurse, exclusions, whitelist)
48
+ (current_contents(recurse) - desired_contents).reject do |item|
49
+ if exclusion_match = exclusions.find { |ex_item| File.fnmatch?(ex_item, item, File::FNM_PATHNAME | File::FNM_DOTMATCH) }
50
+ logger.debug2 _("Not purging %{item} due to internal exclusion match: %{exclusion_match}") % {item: item, exclusion_match: exclusion_match}
51
+ elsif whitelist_match = whitelist.find { |wl_item| File.fnmatch?(wl_item, item, File::FNM_PATHNAME | File::FNM_DOTMATCH) }
52
+ logger.debug _("Not purging %{item} due to whitelist match: %{whitelist_match}") % {item: item, whitelist_match: whitelist_match}
53
+ end
54
+
55
+ !!exclusion_match || !!whitelist_match
56
+ end
44
57
  end
45
58
 
46
- # Forcibly remove all unmanaged content in `self.managed_directory`
47
- def purge!
48
- if stale_contents.empty?
49
- logger.debug1 "No unmanaged contents in #{managed_directory}, nothing to purge"
59
+ # Forcibly remove all unmanaged content in `self.managed_directories`
60
+ def purge!(opts={})
61
+ recurse = opts[:recurse] || false
62
+ whitelist = opts[:whitelist] || []
63
+
64
+ exclusions = self.respond_to?(:purge_exclusions) ? purge_exclusions : []
65
+
66
+ stale = stale_contents(recurse, exclusions, whitelist)
67
+
68
+ if stale.empty?
69
+ logger.debug1 _("No unmanaged contents in %{managed_dirs}, nothing to purge") % {managed_dirs: managed_directories.join(', ')}
50
70
  else
51
- stale_contents.each do |fname|
52
- fpath = File.join(self.managed_directory, fname)
53
- logger.info "Removing unmanaged path #{fpath}"
54
- FileUtils.rm_rf(fpath, :secure => true)
71
+ stale.each do |fpath|
72
+ begin
73
+ FileUtils.rm_r(fpath, :secure => true)
74
+ logger.info _("Removing unmanaged path %{path}") % {path: fpath}
75
+ rescue Errno::ENOENT
76
+ # Don't log on ENOENT since we may encounter that from recursively deleting
77
+ # this item's parent earlier in the purge.
78
+ rescue
79
+ logger.debug1 _("Unable to remove unmanaged path: %{path}") % {path: fpath}
80
+ end
55
81
  end
56
82
  end
57
83
  end