r10k 3.12.1 → 3.14.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/docker.yml +1 -1
- data/.github/workflows/rspec_tests.yml +5 -5
- data/.github/workflows/stale.yml +2 -0
- data/.gitignore +1 -0
- data/.travis.yml +1 -1
- data/CHANGELOG.mkd +32 -0
- data/doc/dynamic-environments/configuration.mkd +59 -10
- data/doc/dynamic-environments/usage.mkd +4 -0
- data/doc/git/providers.mkd +22 -0
- data/doc/puppetfile.mkd +13 -0
- data/docker/Makefile +1 -1
- data/docker/r10k/Dockerfile +1 -1
- data/docker/r10k/release.Dockerfile +1 -1
- data/lib/r10k/action/runner.rb +6 -0
- data/lib/r10k/environment/bare.rb +4 -7
- data/lib/r10k/environment/name.rb +14 -9
- data/lib/r10k/environment/plain.rb +16 -0
- data/lib/r10k/environment/tarball.rb +78 -0
- data/lib/r10k/environment/with_modules.rb +1 -1
- data/lib/r10k/environment.rb +2 -0
- data/lib/r10k/errors.rb +5 -0
- data/lib/r10k/forge/module_release.rb +2 -1
- data/lib/r10k/git/cache.rb +4 -13
- data/lib/r10k/git/rugged/bare_repository.rb +1 -1
- data/lib/r10k/git/rugged/base_repository.rb +12 -1
- data/lib/r10k/git/rugged/cache.rb +8 -0
- data/lib/r10k/git/rugged/credentials.rb +1 -1
- data/lib/r10k/git/rugged/working_repository.rb +1 -1
- data/lib/r10k/git/stateful_repository.rb +2 -0
- data/lib/r10k/initializers.rb +20 -0
- data/lib/r10k/logging.rb +78 -1
- data/lib/r10k/module/base.rb +6 -2
- data/lib/r10k/module/forge.rb +10 -10
- data/lib/r10k/module/git.rb +1 -3
- data/lib/r10k/module/local.rb +1 -1
- data/lib/r10k/module/tarball.rb +101 -0
- data/lib/r10k/module.rb +1 -0
- data/lib/r10k/module_loader/puppetfile/dsl.rb +1 -1
- data/lib/r10k/module_loader/puppetfile.rb +21 -7
- data/lib/r10k/settings.rb +35 -0
- data/lib/r10k/source/git.rb +18 -18
- data/lib/r10k/source/yaml.rb +1 -1
- data/lib/r10k/tarball.rb +183 -0
- data/lib/r10k/util/cacheable.rb +31 -0
- data/lib/r10k/util/downloader.rb +134 -0
- data/lib/r10k/util/purgeable.rb +2 -2
- data/lib/r10k/version.rb +1 -1
- data/locales/r10k.pot +85 -57
- data/r10k.gemspec +2 -2
- data/r10k.yaml.example +28 -0
- data/spec/fixtures/tarball/tarball.tar.gz +0 -0
- data/spec/fixtures/unit/action/r10k_logging.yaml +12 -0
- data/spec/fixtures/unit/puppetfile/various-modules/modules/apt/.gitkeep +1 -0
- data/spec/fixtures/unit/puppetfile/various-modules/modules/baz/.gitkeep +1 -0
- data/spec/fixtures/unit/puppetfile/various-modules/modules/buzz/.gitkeep +1 -0
- data/spec/fixtures/unit/puppetfile/various-modules/modules/canary/.gitkeep +1 -0
- data/spec/fixtures/unit/puppetfile/various-modules/modules/fizz/.gitkeep +1 -0
- data/spec/fixtures/unit/puppetfile/various-modules/modules/rpm/.gitkeep +1 -0
- data/spec/fixtures/unit/util/purgeable/managed_one/managed_subdir_1/managed_symlink_file +1 -0
- data/spec/fixtures/unit/util/purgeable/managed_one/managed_subdir_1/{managed_subdir_2 → subdir_allowlisted_2}/ignored_1 +0 -0
- data/spec/fixtures/unit/util/purgeable/managed_one/managed_subdir_1/unmanaged_symlink_dir +1 -0
- data/spec/fixtures/unit/util/purgeable/managed_one/managed_symlink_dir +1 -0
- data/spec/fixtures/unit/util/purgeable/managed_one/unmanaged_symlink_file +1 -0
- data/spec/integration/git/rugged/cache_spec.rb +33 -0
- data/spec/integration/util/purageable_spec.rb +41 -0
- data/spec/shared-contexts/tarball.rb +32 -0
- data/spec/spec_helper.rb +1 -0
- data/spec/unit/action/deploy/module_spec.rb +2 -2
- data/spec/unit/action/puppetfile/install_spec.rb +2 -2
- data/spec/unit/action/runner_spec.rb +52 -1
- data/spec/unit/environment/bare_spec.rb +13 -0
- data/spec/unit/environment/name_spec.rb +18 -0
- data/spec/unit/environment/plain_spec.rb +8 -0
- data/spec/unit/environment/tarball_spec.rb +45 -0
- data/spec/unit/environment/with_modules_spec.rb +1 -1
- data/spec/unit/git/cache_spec.rb +2 -15
- data/spec/unit/git/rugged/cache_spec.rb +19 -0
- data/spec/unit/module/forge_spec.rb +9 -7
- data/spec/unit/module/tarball_spec.rb +70 -0
- data/spec/unit/module_loader/puppetfile_spec.rb +21 -5
- data/spec/unit/module_spec.rb +14 -7
- data/spec/unit/tarball_spec.rb +57 -0
- data/spec/unit/util/cacheable_spec.rb +23 -0
- data/spec/unit/util/downloader_spec.rb +98 -0
- data/spec/unit/util/purgeable_spec.rb +22 -11
- metadata +45 -17
data/lib/r10k/tarball.rb
ADDED
@@ -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
|
@@ -0,0 +1,134 @@
|
|
1
|
+
require 'digest'
|
2
|
+
require 'net/http'
|
3
|
+
|
4
|
+
module R10K
|
5
|
+
module Util
|
6
|
+
|
7
|
+
# Utility mixin for classes that need to download files
|
8
|
+
module Downloader
|
9
|
+
|
10
|
+
# Downloader objects need to checksum downloaded or saved content. The
|
11
|
+
# algorithm used to perform this checksumming (and therefore the kinds of
|
12
|
+
# checksums returned by various methods) is reported by this method.
|
13
|
+
#
|
14
|
+
# @return [Symbol] The checksum algorithm the downloader uses
|
15
|
+
def checksum_algorithm
|
16
|
+
@checksum_algorithm ||= :SHA256
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
# Set the checksum algorithm the downloader should use. It should be a
|
22
|
+
# symbol, and a valid Ruby 'digest' library algorithm. The default is
|
23
|
+
# :SHA256.
|
24
|
+
#
|
25
|
+
# @param algorithm [Symbol] The checksum algorithm the downloader should use
|
26
|
+
def checksum_algorithm=(algorithm)
|
27
|
+
@checksum_algorithm = algorithm
|
28
|
+
end
|
29
|
+
|
30
|
+
CHUNK_SIZE = 64 * 1024 # 64 kb
|
31
|
+
|
32
|
+
# @param src_uri [URI] The URI to download from
|
33
|
+
# @param dst_file [String] The file or path to save to
|
34
|
+
# @return [String] The downloaded file's hex digest
|
35
|
+
def download(src_uri, dst_file)
|
36
|
+
digest = Digest(checksum_algorithm).new
|
37
|
+
http_get(src_uri) do |resp|
|
38
|
+
File.open(dst_file, 'wb') do |output_stream|
|
39
|
+
resp.read_body do |chunk|
|
40
|
+
output_stream.write(chunk)
|
41
|
+
digest.update(chunk)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
digest.hexdigest
|
47
|
+
end
|
48
|
+
|
49
|
+
# @param src_file The file or path to copy from
|
50
|
+
# @param dst_file The file or path to copy to
|
51
|
+
# @return [String] The copied file's sha256 hex digest
|
52
|
+
def copy(src_file, dst_file)
|
53
|
+
digest = Digest(checksum_algorithm).new
|
54
|
+
File.open(src_file, 'rb') do |input_stream|
|
55
|
+
File.open(dst_file, 'wb') do |output_stream|
|
56
|
+
until input_stream.eof?
|
57
|
+
chunk = input_stream.read(CHUNK_SIZE)
|
58
|
+
output_stream.write(chunk)
|
59
|
+
digest.update(chunk)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
digest.hexdigest
|
65
|
+
end
|
66
|
+
|
67
|
+
# Start a Net::HTTP::Get connection, then yield the Net::HTTPSuccess object
|
68
|
+
# to the caller's block. Follow redirects if Net::HTTPRedirection responses
|
69
|
+
# are encountered, and use a proxy if directed.
|
70
|
+
#
|
71
|
+
# @param uri [URI] The URI to download the file from
|
72
|
+
# @param redirect_limit [Integer] How many redirects to permit before failing
|
73
|
+
# @param proxy [URI, String] The URI to use as a proxy
|
74
|
+
def http_get(uri, redirect_limit: 10, proxy: nil, &block)
|
75
|
+
raise "HTTP redirect too deep" if redirect_limit.zero?
|
76
|
+
|
77
|
+
session = Net::HTTP.new(uri.host, uri.port, *proxy_to_array(proxy))
|
78
|
+
session.use_ssl = true if uri.scheme == 'https'
|
79
|
+
session.start
|
80
|
+
|
81
|
+
begin
|
82
|
+
session.request_get(uri) do |response|
|
83
|
+
case response
|
84
|
+
when Net::HTTPRedirection
|
85
|
+
redirect = response['location']
|
86
|
+
session.finish
|
87
|
+
return http_get(URI.parse(redirect), redirect_limit: redirect_limit - 1, proxy: proxy, &block)
|
88
|
+
when Net::HTTPSuccess
|
89
|
+
yield response
|
90
|
+
else
|
91
|
+
raise "Unexpected response code #{response.code}: #{response}"
|
92
|
+
end
|
93
|
+
end
|
94
|
+
ensure
|
95
|
+
session.finish if session.active?
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# Helper method to translate a proxy URI to array arguments for
|
100
|
+
# Net::HTTP#new. A nil argument returns nil array elements.
|
101
|
+
def proxy_to_array(proxy_uri)
|
102
|
+
if proxy_uri
|
103
|
+
px = proxy_uri.is_a?(URI) ? proxy_uri : URI.parse(proxy_uri)
|
104
|
+
[px.host, px.port, px.user, px.password]
|
105
|
+
else
|
106
|
+
[nil, nil, nil, nil]
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# Return the sha256 digest of the file at the given path
|
111
|
+
#
|
112
|
+
# @param path [String] The path to the file
|
113
|
+
# @return [String] The file's sha256 hex digest
|
114
|
+
def file_digest(path)
|
115
|
+
File.open(path) do |file|
|
116
|
+
reader_digest(file)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# Return the sha256 digest of the readable data
|
121
|
+
#
|
122
|
+
# @param reader [String] An object that responds to #read
|
123
|
+
# @return [String] The read data's sha256 hex digest
|
124
|
+
def reader_digest(reader)
|
125
|
+
digest = Digest(checksum_algorithm).new
|
126
|
+
while chunk = reader.read(CHUNK_SIZE)
|
127
|
+
digest.update(chunk)
|
128
|
+
end
|
129
|
+
|
130
|
+
digest.hexdigest
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
data/lib/r10k/util/purgeable.rb
CHANGED
@@ -99,8 +99,8 @@ module R10K
|
|
99
99
|
end
|
100
100
|
|
101
101
|
children.flat_map do |child|
|
102
|
-
if File.directory?(child) && recurse
|
103
|
-
potentially_purgeable(child, exclusion_globs, allowed_globs, desireds_not_to_recurse_into, recurse)
|
102
|
+
if File.directory?(child) && !File.symlink?(child) && recurse
|
103
|
+
potentially_purgeable(child, exclusion_globs, allowed_globs, desireds_not_to_recurse_into, recurse) << child.to_s
|
104
104
|
else
|
105
105
|
child.to_s
|
106
106
|
end
|
data/lib/r10k/version.rb
CHANGED
@@ -2,5 +2,5 @@ module R10K
|
|
2
2
|
# When updating to a new major (X) or minor (Y) version, include `#major` or
|
3
3
|
# `#minor` (respectively) in your commit message to trigger the appropriate
|
4
4
|
# release. Otherwise, a new patch (Z) version will be released.
|
5
|
-
VERSION = '3.
|
5
|
+
VERSION = '3.14.2'
|
6
6
|
end
|