r10k 3.11.0 → 3.14.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (106) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/docker.yml +1 -1
  3. data/.github/workflows/rspec_tests.yml +2 -2
  4. data/.travis.yml +1 -1
  5. data/CHANGELOG.mkd +34 -0
  6. data/doc/dynamic-environments/configuration.mkd +35 -7
  7. data/doc/dynamic-environments/usage.mkd +26 -0
  8. data/doc/puppetfile.mkd +16 -4
  9. data/integration/tests/basic_functionality/basic_deployment.rb +176 -0
  10. data/lib/r10k/action/base.rb +1 -1
  11. data/lib/r10k/action/deploy/deploy_helpers.rb +4 -0
  12. data/lib/r10k/action/deploy/display.rb +1 -1
  13. data/lib/r10k/action/deploy/environment.rb +13 -8
  14. data/lib/r10k/action/deploy/module.rb +14 -10
  15. data/lib/r10k/action/puppetfile/check.rb +7 -5
  16. data/lib/r10k/action/puppetfile/install.rb +22 -16
  17. data/lib/r10k/action/puppetfile/purge.rb +12 -9
  18. data/lib/r10k/action/runner.rb +6 -0
  19. data/lib/r10k/action/visitor.rb +3 -0
  20. data/lib/r10k/cli/deploy.rb +1 -0
  21. data/lib/r10k/cli/puppetfile.rb +0 -1
  22. data/lib/r10k/content_synchronizer.rb +16 -4
  23. data/lib/r10k/environment/bare.rb +4 -7
  24. data/lib/r10k/environment/base.rb +64 -11
  25. data/lib/r10k/environment/name.rb +14 -9
  26. data/lib/r10k/environment/plain.rb +16 -0
  27. data/lib/r10k/environment/tarball.rb +78 -0
  28. data/lib/r10k/environment/with_modules.rb +6 -10
  29. data/lib/r10k/environment.rb +2 -0
  30. data/lib/r10k/errors.rb +5 -0
  31. data/lib/r10k/forge/module_release.rb +2 -1
  32. data/lib/r10k/git/cache.rb +4 -13
  33. data/lib/r10k/git/rugged/base_repository.rb +12 -1
  34. data/lib/r10k/git/rugged/cache.rb +8 -0
  35. data/lib/r10k/git/stateful_repository.rb +9 -0
  36. data/lib/r10k/initializers.rb +21 -7
  37. data/lib/r10k/logging.rb +78 -1
  38. data/lib/r10k/module/base.rb +5 -1
  39. data/lib/r10k/module/definition.rb +64 -0
  40. data/lib/r10k/module/forge.rb +10 -2
  41. data/lib/r10k/module/git.rb +22 -1
  42. data/lib/r10k/module/local.rb +6 -3
  43. data/lib/r10k/module/svn.rb +10 -0
  44. data/lib/r10k/module/tarball.rb +101 -0
  45. data/lib/r10k/module.rb +21 -2
  46. data/lib/r10k/module_loader/puppetfile/dsl.rb +8 -3
  47. data/lib/r10k/module_loader/puppetfile.rb +109 -30
  48. data/lib/r10k/puppetfile.rb +1 -2
  49. data/lib/r10k/settings.rb +46 -0
  50. data/lib/r10k/source/git.rb +18 -18
  51. data/lib/r10k/source/yaml.rb +1 -1
  52. data/lib/r10k/tarball.rb +183 -0
  53. data/lib/r10k/util/cacheable.rb +31 -0
  54. data/lib/r10k/util/downloader.rb +134 -0
  55. data/lib/r10k/util/purgeable.rb +6 -2
  56. data/lib/r10k/util/setopts.rb +2 -0
  57. data/lib/r10k/util/subprocess.rb +1 -0
  58. data/lib/r10k/version.rb +1 -1
  59. data/locales/r10k.pot +119 -71
  60. data/r10k.gemspec +2 -2
  61. data/r10k.yaml.example +28 -0
  62. data/spec/fixtures/tarball/tarball.tar.gz +0 -0
  63. data/spec/fixtures/unit/action/r10k_logging.yaml +12 -0
  64. data/spec/fixtures/unit/puppetfile/forge-override/Puppetfile +8 -0
  65. data/spec/fixtures/unit/puppetfile/various-modules/Puppetfile +10 -0
  66. data/spec/fixtures/unit/puppetfile/various-modules/Puppetfile.new +10 -0
  67. data/spec/fixtures/unit/util/purgeable/managed_one/managed_subdir_1/managed_symlink_file +1 -0
  68. data/spec/fixtures/unit/util/purgeable/managed_one/managed_subdir_1/{managed_subdir_2 → subdir_allowlisted_2}/ignored_1 +0 -0
  69. data/spec/fixtures/unit/util/purgeable/managed_one/managed_subdir_1/unmanaged_symlink_dir +1 -0
  70. data/spec/fixtures/unit/util/purgeable/managed_one/managed_symlink_dir +1 -0
  71. data/spec/fixtures/unit/util/purgeable/managed_one/unmanaged_symlink_file +1 -0
  72. data/spec/integration/git/rugged/cache_spec.rb +33 -0
  73. data/spec/integration/util/purageable_spec.rb +41 -0
  74. data/spec/r10k-mocks/mock_env.rb +3 -0
  75. data/spec/r10k-mocks/mock_source.rb +7 -3
  76. data/spec/shared-contexts/tarball.rb +32 -0
  77. data/spec/spec_helper.rb +1 -0
  78. data/spec/unit/action/deploy/environment_spec.rb +80 -30
  79. data/spec/unit/action/deploy/module_spec.rb +52 -64
  80. data/spec/unit/action/puppetfile/check_spec.rb +17 -5
  81. data/spec/unit/action/puppetfile/install_spec.rb +42 -36
  82. data/spec/unit/action/puppetfile/purge_spec.rb +15 -17
  83. data/spec/unit/action/runner_spec.rb +52 -9
  84. data/spec/unit/environment/bare_spec.rb +13 -0
  85. data/spec/unit/environment/base_spec.rb +30 -17
  86. data/spec/unit/environment/git_spec.rb +2 -2
  87. data/spec/unit/environment/name_spec.rb +18 -0
  88. data/spec/unit/environment/plain_spec.rb +8 -0
  89. data/spec/unit/environment/svn_spec.rb +4 -3
  90. data/spec/unit/environment/tarball_spec.rb +45 -0
  91. data/spec/unit/environment/with_modules_spec.rb +3 -2
  92. data/spec/unit/git/cache_spec.rb +2 -15
  93. data/spec/unit/git/rugged/cache_spec.rb +19 -0
  94. data/spec/unit/module/base_spec.rb +8 -8
  95. data/spec/unit/module/forge_spec.rb +32 -4
  96. data/spec/unit/module/git_spec.rb +51 -10
  97. data/spec/unit/module/svn_spec.rb +18 -6
  98. data/spec/unit/module/tarball_spec.rb +70 -0
  99. data/spec/unit/module_loader/puppetfile_spec.rb +96 -31
  100. data/spec/unit/puppetfile_spec.rb +2 -2
  101. data/spec/unit/settings_spec.rb +25 -2
  102. data/spec/unit/tarball_spec.rb +57 -0
  103. data/spec/unit/util/cacheable_spec.rb +23 -0
  104. data/spec/unit/util/downloader_spec.rb +98 -0
  105. data/spec/unit/util/purgeable_spec.rb +22 -11
  106. metadata +44 -17
@@ -1,4 +1,9 @@
1
+ require 'r10k/errors'
1
2
  require 'r10k/logging'
3
+ require 'r10k/module'
4
+ require 'r10k/module_loader/puppetfile/dsl'
5
+
6
+ require 'pathname'
2
7
 
3
8
  module R10K
4
9
  module ModuleLoader
@@ -8,11 +13,11 @@ module R10K
8
13
 
9
14
  DEFAULT_MODULEDIR = 'modules'
10
15
  DEFAULT_PUPPETFILE_NAME = 'Puppetfile'
11
- DEFAULT_FORGE_API = 'forgeapi.puppetlabs.com'
12
16
 
13
17
  attr_accessor :default_branch_override, :environment
14
18
  attr_reader :modules, :moduledir, :puppetfile_path,
15
- :managed_directories, :desired_contents, :purge_exclusions
19
+ :managed_directories, :desired_contents, :purge_exclusions,
20
+ :environment_name
16
21
 
17
22
  # @param basedir [String] The path that contains the moduledir &
18
23
  # Puppetfile by default. May be an environment, project, or
@@ -28,34 +33,41 @@ module R10K
28
33
  def initialize(basedir:,
29
34
  moduledir: DEFAULT_MODULEDIR,
30
35
  puppetfile: DEFAULT_PUPPETFILE_NAME,
31
- forge: DEFAULT_FORGE_API,
32
36
  overrides: {},
33
37
  environment: nil)
34
38
 
35
39
  @basedir = cleanpath(basedir)
36
40
  @moduledir = resolve_path(@basedir, moduledir)
37
41
  @puppetfile_path = resolve_path(@basedir, puppetfile)
38
- @forge = forge
39
42
  @overrides = overrides
40
43
  @environment = environment
44
+ @environment_name = @environment&.name
41
45
  @default_branch_override = @overrides.dig(:environments, :default_branch_override)
46
+ @allow_puppetfile_forge = @overrides.dig(:forge, :allow_puppetfile_override)
42
47
 
48
+ @existing_module_metadata = []
49
+ @existing_module_versions_by_name = {}
43
50
  @modules = []
44
51
 
45
52
  @managed_directories = []
46
53
  @desired_contents = []
47
54
  @purge_exclusions = []
55
+ end
48
56
 
57
+ def load
58
+ with_readable_puppetfile(@puppetfile_path) do
59
+ self.load!
60
+ end
61
+ end
62
+
63
+ def load!
49
64
  logger.info _("Using Puppetfile '%{puppetfile}'") % {puppetfile: @puppetfile_path}
50
65
  logger.debug _("Using moduledir '%{moduledir}'") % {moduledir: @moduledir}
51
- end
52
66
 
53
- def load
54
67
  dsl = R10K::ModuleLoader::Puppetfile::DSL.new(self)
55
68
  dsl.instance_eval(puppetfile_content(@puppetfile_path), @puppetfile_path)
56
69
 
57
70
  validate_no_duplicate_names(@modules)
58
- @modules
59
71
 
60
72
  managed_content = @modules.group_by(&:dirname)
61
73
 
@@ -74,6 +86,30 @@ module R10K
74
86
  raise R10K::Error.wrap(e, _("Failed to evaluate %{path}") % {path: @puppetfile_path})
75
87
  end
76
88
 
89
+ def load_metadata
90
+ with_readable_puppetfile(@puppetfile_path) do
91
+ self.load_metadata!
92
+ end
93
+ end
94
+
95
+ def load_metadata!
96
+ dsl = R10K::ModuleLoader::Puppetfile::DSL.new(self, metadata_only: true)
97
+ dsl.instance_eval(puppetfile_content(@puppetfile_path), @puppetfile_path)
98
+
99
+ @existing_module_versions_by_name = @existing_module_metadata.map {|mod| [ mod.name, mod.version ] }.to_h
100
+ empty_load_output.merge(modules: @existing_module_metadata)
101
+
102
+ rescue SyntaxError, LoadError, ArgumentError, NameError => e
103
+ logger.warn _("Unable to preload Puppetfile because of %{msg}" % { msg: e.message })
104
+ end
105
+
106
+ def add_module_metadata(name, info)
107
+ install_path, metadata_info, _ = parse_module_definition(name, info)
108
+
109
+ mod = R10K::Module.from_metadata(name, install_path, metadata_info, @environment)
110
+
111
+ @existing_module_metadata << mod
112
+ end
77
113
 
78
114
  ##
79
115
  ## set_forge, set_moduledir, and add_module are used directly by the DSL class
@@ -81,7 +117,12 @@ module R10K
81
117
 
82
118
  # @param [String] forge
83
119
  def set_forge(forge)
84
- @forge = forge
120
+ if @allow_puppetfile_forge
121
+ logger.debug _("Using Forge from Puppetfile: %{forge}") % { forge: forge }
122
+ PuppetForge.host = forge
123
+ else
124
+ logger.debug _("Ignoring Forge declaration in Puppetfile, using value from settings: %{forge}.") % { forge: PuppetForge.host }
125
+ end
85
126
  end
86
127
 
87
128
  # @param [String] moduledir
@@ -90,7 +131,7 @@ module R10K
90
131
  end
91
132
 
92
133
  # @param [String] name
93
- # @param [Hash, String, Symbol, nil] module_info Calling with
134
+ # @param [Hash, String, Symbol, nil] info Calling with
94
135
  # anything but a Hash is deprecated. The DSL will now convert
95
136
  # String and Symbol versions to Hashes of the shape
96
137
  # { version: <String or Symbol> }
@@ -103,28 +144,10 @@ module R10K
103
144
  # DSL class, not the Puppetfile author) to do this conversion
104
145
  # itself.
105
146
  #
106
- def add_module(name, module_info)
107
- if !module_info.is_a?(Hash)
108
- module_info = { version: module_info }
109
- end
110
-
111
- module_info[:overrides] = @overrides
112
-
113
- spec_deletable = false
114
-
115
- if install_path = module_info.delete(:install_path)
116
- install_path = resolve_path(@basedir, install_path)
117
- validate_install_path(install_path, name)
118
- else
119
- install_path = @moduledir
120
- spec_deletable = true
121
- end
147
+ def add_module(name, info)
148
+ install_path, metadata_info, spec_deletable = parse_module_definition(name, info)
122
149
 
123
- if @default_branch_override
124
- module_info[:default_branch_override] = @default_branch_override
125
- end
126
-
127
- mod = R10K::Module.new(name, install_path, module_info, @environment)
150
+ mod = R10K::Module.from_metadata(name, install_path, metadata_info, @environment)
128
151
  mod.origin = :puppetfile
129
152
  mod.spec_deletable = spec_deletable
130
153
 
@@ -134,11 +157,67 @@ module R10K
134
157
  return @modules
135
158
  end
136
159
 
160
+ # If this module's metadata has a static version and that version
161
+ # matches the existing module declaration use it, otherwise create
162
+ # a regular module to sync.
163
+ unless mod.version && (mod.version == @existing_module_versions_by_name[mod.name])
164
+ mod = mod.to_implementation
165
+ end
166
+
137
167
  @modules << mod
138
168
  end
139
169
 
170
+ # @deprecated
171
+ # @return [String] The base directory that contains the Puppetfile
172
+ def basedir
173
+ logger.warn _('"basedir" is deprecated. Please use "environment_name" instead. "basedir" will be removed in a future version.')
174
+ @basedir
175
+ end
176
+
140
177
  private
141
178
 
179
+ def empty_load_output
180
+ {
181
+ modules: [],
182
+ managed_directories: [],
183
+ desired_contents: [],
184
+ purge_exclusions: []
185
+ }
186
+ end
187
+
188
+ def with_readable_puppetfile(puppetfile_path, &block)
189
+ if File.readable?(puppetfile_path)
190
+ block.call
191
+ else
192
+ logger.debug _("Puppetfile %{path} missing or unreadable") % {path: puppetfile_path.inspect}
193
+
194
+ empty_load_output
195
+ end
196
+ end
197
+
198
+ def parse_module_definition(name, info)
199
+ if !info.is_a?(Hash)
200
+ info = { version: info }
201
+ end
202
+
203
+ info[:overrides] = @overrides
204
+
205
+ if @default_branch_override
206
+ info[:default_branch_override] = @default_branch_override
207
+ end
208
+
209
+ spec_deletable = false
210
+ if install_path = info.delete(:install_path)
211
+ install_path = resolve_path(@basedir, install_path)
212
+ validate_install_path(install_path, name)
213
+ else
214
+ install_path = @moduledir
215
+ spec_deletable = true
216
+ end
217
+
218
+ return [ install_path, info, spec_deletable ]
219
+ end
220
+
142
221
  # @param [Array<R10K::Module>] modules
143
222
  def validate_no_duplicate_names(modules)
144
223
  dupes = modules
@@ -78,7 +78,6 @@ class Puppetfile
78
78
  basedir: @basedir,
79
79
  moduledir: @moduledir,
80
80
  puppetfile: @puppetfile,
81
- forge: @forge,
82
81
  overrides: @overrides,
83
82
  environment: @environment
84
83
  )
@@ -120,7 +119,7 @@ class Puppetfile
120
119
  @loader.default_branch_override = default_branch_override
121
120
  end
122
121
 
123
- @loaded_content = @loader.load
122
+ @loaded_content = @loader.load!
124
123
  @loaded = true
125
124
 
126
125
  @loaded_content
data/lib/r10k/settings.rb CHANGED
@@ -126,8 +126,19 @@ module R10K
126
126
  URIDefinition.new(:baseurl, {
127
127
  :desc => "The URL to the Puppet Forge to use for downloading modules."
128
128
  }),
129
+
129
130
  Definition.new(:authorization_token, {
130
131
  :desc => "The token for Puppet Forge authorization. Leave blank for unauthorized or license-based connections."
132
+ }),
133
+
134
+ Definition.new(:allow_puppetfile_override, {
135
+ :desc => "Whether to use `forge` declarations in the Puppetfile as an override of `baseurl`.",
136
+ :default => false,
137
+ :validate => lambda do |value|
138
+ unless !!value == value
139
+ raise ArgumentError, "`allow_puppetfile_override` can only be a boolean value, not '#{value}'"
140
+ end
141
+ end
131
142
  })
132
143
  ])
133
144
  end
@@ -206,6 +217,39 @@ module R10K
206
217
  })])
207
218
  end
208
219
 
220
+ def self.logging_settings
221
+ R10K::Settings::Collection.new(:logging, [
222
+ Definition.new(:level, {
223
+ desc: 'What logging level should R10k run on if not specified at runtime.',
224
+ validate: lambda do |value|
225
+ if R10K::Logging.parse_level(value).nil?
226
+ raise ArgumentError, "`level` must be a valid log level.
227
+ Valid levels are #{R10K::Logging::LOG_LEVELS.map(&:downcase).inspect}"
228
+ end
229
+ end
230
+ }),
231
+
232
+ Definition.new(:outputs, {
233
+ desc: 'Additional log outputs to use.',
234
+ validate: lambda do |value|
235
+ unless value.is_a?(Array)
236
+ raise ArgumentError, "The `outputs` setting should be an array of outputs, not a #{value.class}"
237
+ end
238
+ end
239
+ }),
240
+
241
+ Definition.new(:disable_default_stderr, {
242
+ desc: 'Disable the default stderr logging output',
243
+ default: false,
244
+ validate: lambda do |value|
245
+ unless !!value == value
246
+ raise ArgumentError, "`disable_default_stderr` can only be a boolean value, not '#{value}'"
247
+ end
248
+ end
249
+ })
250
+ ])
251
+ end
252
+
209
253
  def self.global_settings
210
254
  R10K::Settings::Collection.new(:global, [
211
255
  Definition.new(:sources, {
@@ -260,6 +304,8 @@ module R10K
260
304
  R10K::Settings.git_settings,
261
305
 
262
306
  R10K::Settings.deploy_settings,
307
+
308
+ R10K::Settings.logging_settings
263
309
  ])
264
310
  end
265
311
  end
@@ -100,26 +100,26 @@ class R10K::Source::Git < R10K::Source::Base
100
100
 
101
101
  def generate_environments
102
102
  envs = []
103
- branch_names.each do |bn|
104
- if bn.valid?
105
- envs << R10K::Environment::Git.new(bn.name,
103
+ environment_names.each do |en|
104
+ if en.valid?
105
+ envs << R10K::Environment::Git.new(en.name,
106
106
  @basedir,
107
- bn.dirname,
107
+ en.dirname,
108
108
  {remote: remote,
109
- ref: bn.name,
109
+ ref: en.original_name,
110
110
  puppetfile_name: puppetfile_name,
111
111
  overrides: @options[:overrides]})
112
- elsif bn.correct?
113
- 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}
114
- envs << R10K::Environment::Git.new(bn.name,
112
+ elsif en.correct?
113
+ logger.warn _("Environment %{env_name} contained non-word characters, correcting name to %{corrected_env_name}") % {env_name: en.name.inspect, corrected_env_name: en.dirname}
114
+ envs << R10K::Environment::Git.new(en.name,
115
115
  @basedir,
116
- bn.dirname,
116
+ en.dirname,
117
117
  {remote: remote,
118
- ref: bn.name,
118
+ ref: en.original_name,
119
119
  puppetfile_name: puppetfile_name,
120
120
  overrides: @options[:overrides]})
121
- elsif bn.validate?
122
- logger.error _("Environment %{env_name} contained non-word characters, ignoring it.") % {env_name: bn.name.inspect}
121
+ elsif en.validate?
122
+ logger.error _("Environment %{env_name} contained non-word characters, ignoring it.") % {env_name: en.name.inspect}
123
123
  end
124
124
  end
125
125
 
@@ -157,22 +157,22 @@ class R10K::Source::Git < R10K::Source::Base
157
157
 
158
158
  private
159
159
 
160
- def branch_names
160
+ def environment_names
161
161
  opts = {prefix: @prefix,
162
162
  invalid: @invalid_branches,
163
163
  source: @name,
164
164
  strip_component: @strip_component}
165
- branches = @cache.branches
165
+ branch_names = @cache.branches
166
166
  if @ignore_branch_prefixes && !@ignore_branch_prefixes.empty?
167
- branches = filter_branches_by_regexp(branches, @ignore_branch_prefixes)
167
+ branch_names = filter_branches_by_regexp(branch_names, @ignore_branch_prefixes)
168
168
  end
169
169
 
170
170
  if @filter_command && !@filter_command.empty?
171
- branches = filter_branches_by_command(branches, @filter_command)
171
+ branch_names = filter_branches_by_command(branch_names, @filter_command)
172
172
  end
173
173
 
174
- branches.map do |branch|
175
- R10K::Environment::Name.new(branch, opts)
174
+ branch_names.map do |branch_name|
175
+ R10K::Environment::Name.new(branch_name, opts)
176
176
  end
177
177
  end
178
178
  end
@@ -7,7 +7,7 @@ class R10K::Source::Yaml < R10K::Source::Hash
7
7
  begin
8
8
  contents = ::YAML.load_file(config)
9
9
  rescue => e
10
- raise ConfigError, _("Couldn't open environments file %{file}: %{err}") % {file: config, err: e.message}
10
+ raise R10K::ConfigError, _("Couldn't open environments file %{file}: %{err}") % {file: config, err: e.message}
11
11
  end
12
12
 
13
13
  # Set the environments key for the parent class to consume
@@ -0,0 +1,183 @@
1
+ require 'fileutils'
2
+ require 'find'
3
+ require 'minitar'
4
+ require 'tempfile'
5
+ require 'uri'
6
+ require 'zlib'
7
+ require 'r10k/settings'
8
+ require 'r10k/settings/mixin'
9
+ require 'r10k/util/platform'
10
+ require 'r10k/util/cacheable'
11
+ require 'r10k/util/downloader'
12
+
13
+ module R10K
14
+ class Tarball
15
+
16
+ include R10K::Settings::Mixin
17
+ include R10K::Util::Cacheable
18
+ include R10K::Util::Downloader
19
+
20
+ def_setting_attr :proxy # Defaults to global proxy setting
21
+ def_setting_attr :cache_root, R10K::Util::Cacheable.default_cachedir
22
+
23
+ # @!attribute [rw] name
24
+ # @return [String] The tarball's name
25
+ attr_accessor :name
26
+
27
+ # @!attribute [rw] source
28
+ # @return [String] The tarball's source
29
+ attr_accessor :source
30
+
31
+ # @!attribute [rw] checksum
32
+ # @return [String] The tarball's expected sha256 digest
33
+ attr_accessor :checksum
34
+
35
+ # @param name [String] The name of the tarball content
36
+ # @param source [String] The source for the tarball content
37
+ # @param checksum [String] The sha256 digest of the tarball content
38
+ def initialize(name, source, checksum: nil)
39
+ @name = name
40
+ @source = source
41
+ @checksum = checksum
42
+
43
+ # At this time, the only checksum type supported is sha256. In the future,
44
+ # we may decide to support other algorithms if a use case arises. TBD.
45
+ checksum_algorithm = :SHA256
46
+ end
47
+
48
+ # @return [String] Directory. Where the cache_basename file will be created.
49
+ def cache_dirname
50
+ File.join(settings[:cache_root], 'tarball')
51
+ end
52
+
53
+ # The final cache_path should match one of the templates:
54
+ #
55
+ # - {cachedir}/{checksum}.tar.gz
56
+ # - {cachedir}/{source}.tar.gz
57
+ #
58
+ # @return [String] File. The full file path the tarball will be cached to.
59
+ def cache_path
60
+ File.join(cache_dirname, cache_basename)
61
+ end
62
+
63
+ # @return [String] The basename of the tarball cache file.
64
+ def cache_basename
65
+ if checksum.nil?
66
+ sanitized_dirname(source) + '.tar.gz'
67
+ else
68
+ checksum + '.tar.gz'
69
+ end
70
+ end
71
+
72
+ # Extract the cached tarball to the target directory.
73
+ #
74
+ # @param target_dir [String] Where to unpack the tarball
75
+ def unpack(target_dir)
76
+ file = File.open(cache_path, 'rb')
77
+ reader = Zlib::GzipReader.new(file)
78
+ begin
79
+ Minitar.unpack(reader, target_dir)
80
+ ensure
81
+ reader.close
82
+ end
83
+ end
84
+
85
+ # @param target_dir [String] The directory to check if is in sync with the
86
+ # tarball content
87
+ # @param ignore_untracked_files [Boolean] If true, consider the target
88
+ # dir to be in sync as long as all tracked content matches.
89
+ #
90
+ # @return [Boolean]
91
+ def insync?(target_dir, ignore_untracked_files: false)
92
+ target_tree_entries = Find.find(target_dir).map(&:to_s) - [target_dir]
93
+ each_tarball_entry do |entry|
94
+ found = target_tree_entries.delete(File.join(target_dir, entry.full_name.chomp('/')))
95
+ return false if found.nil?
96
+ next if entry.directory?
97
+ return false unless file_digest(found) == reader_digest(entry)
98
+ end
99
+
100
+ if ignore_untracked_files
101
+ # We wouldn't have gotten this far if there were discrepancies in
102
+ # tracked content
103
+ true
104
+ else
105
+ # If there are still files in target_tree_entries, then there is
106
+ # untracked content present in the target tree. If not, we're in sync.
107
+ target_tree_entries.empty?
108
+ end
109
+ end
110
+
111
+ # Download the tarball from @source to @cache_path
112
+ def get
113
+ Tempfile.open(cache_basename) do |tempfile|
114
+ tempfile.binmode
115
+ src_uri = URI.parse(source)
116
+
117
+ temp_digest = case src_uri.scheme
118
+ when 'file', nil
119
+ copy(src_uri.path, tempfile)
120
+ when %r{^[a-z]$} # Windows drive letter
121
+ copy(src_uri.to_s, tempfile)
122
+ when %r{^https?$}
123
+ download(src_uri, tempfile)
124
+ else
125
+ raise "Unexpected source scheme #{src_uri.scheme}"
126
+ end
127
+
128
+ # Verify the download
129
+ unless (checksum == temp_digest) || checksum.nil?
130
+ raise 'Downloaded file does not match checksum'
131
+ end
132
+
133
+ # Move the download to cache_path
134
+ FileUtils::mkdir_p(cache_dirname)
135
+ begin
136
+ FileUtils.mv(tempfile.path, cache_path)
137
+ rescue Errno::EACCES
138
+ # It may be the case that permissions don't permit moving the file
139
+ # into place, but do permit overwriting an existing in-place file.
140
+ FileUtils.cp(tempfile.path, cache_path)
141
+ end
142
+ end
143
+ end
144
+
145
+ # Checks the cached tarball's digest against the expected checksum. Returns
146
+ # false if no cached file is present. If the tarball has no expected
147
+ # checksum, any cached file is assumed to be valid.
148
+ #
149
+ # @return [Boolean]
150
+ def cache_valid?
151
+ return false unless File.exist?(cache_path)
152
+ return true if checksum.nil?
153
+ checksum == file_digest(cache_path)
154
+ end
155
+
156
+ # List all of the files contained in the tarball and their paths. This is
157
+ # useful for implementing R10K::Purgable
158
+ #
159
+ # @return [Array] A normalized list of file paths contained in the archive
160
+ def paths
161
+ names = Array.new
162
+ each_tarball_entry { |entry| names << Pathname.new(entry).cleanpath.to_s }
163
+ names - ['.']
164
+ end
165
+
166
+ def cache_checksum
167
+ raise R10K::Error, _("Cache not present at %{path}") % {path: cache_path} unless File.exist?(cache_path)
168
+ file_digest(cache_path)
169
+ end
170
+
171
+ private
172
+
173
+ def each_tarball_entry(&block)
174
+ File.open(cache_path, 'rb') do |file|
175
+ Zlib::GzipReader.wrap(file) do |reader|
176
+ Archive::Tar::Minitar::Input.each_entry(reader) do |entry|
177
+ yield entry
178
+ end
179
+ end
180
+ end
181
+ end
182
+ end
183
+ end
@@ -0,0 +1,31 @@
1
+ module R10K
2
+ module Util
3
+
4
+ # Utility mixin for classes that need to implement caches
5
+ #
6
+ # @abstract Classes using this mixin need to implement {#managed_directory} and
7
+ # {#desired_contents}
8
+ module Cacheable
9
+
10
+ # Provide a default cachedir location. This is consumed by R10K::Settings
11
+ # for appropriate global default values.
12
+ #
13
+ # @return [String] Path to the default cache directory
14
+ def self.default_cachedir(basename = 'cache')
15
+ if R10K::Util::Platform.windows?
16
+ File.join(ENV['LOCALAPPDATA'], 'r10k', basename)
17
+ else
18
+ File.join(ENV['HOME'] || '/root', '.r10k', basename)
19
+ end
20
+ end
21
+
22
+ # Reformat a string into something that can be used as a directory
23
+ #
24
+ # @param string [String] An identifier to create a sanitized dirname for
25
+ # @return [String] A sanitized dirname for the given string
26
+ def sanitized_dirname(string)
27
+ string.gsub(/(\w+:\/\/)(.*)(@)/, '\1').gsub(/[^@\w\.-]/, '-')
28
+ end
29
+ end
30
+ end
31
+ end