cookbook-omnifetch 0.10.0 → 0.12.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/cookbook-omnifetch/artifactory.rb +2 -2
- data/lib/cookbook-omnifetch/artifactserver.rb +2 -2
- data/lib/cookbook-omnifetch/exceptions.rb +5 -0
- data/lib/cookbook-omnifetch/git.rb +1 -1
- data/lib/cookbook-omnifetch/metadata_based_installer.rb +49 -36
- data/lib/cookbook-omnifetch/staging_area.rb +192 -0
- data/lib/cookbook-omnifetch/version.rb +1 -1
- metadata +5 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a84aa2f4551b271e3c13a1260d6070ed4a637ca777ce81343dc3087bf73d9122
|
4
|
+
data.tar.gz: b93c4da8a8ab2a64f7c62e4b9a677ca0276f8a7a9ae6c82f73e21a644f03e07b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e5043f1653e630012f06582d1917ddb72a7904e0a6b283ad5db6f2020a7b26859280bcf50e8ee19298dd00798d1738f644aa5348aafc5571c003addc3fe69fbb
|
7
|
+
data.tar.gz: 7568ed165159ef9800da6211ad8d3bb53ffedde3cd6923a37dd7506dd425f53f0994f49c05cdff8858fbb63ceacf271b4e4f7c54add4ebeb646e6597dc4f1daa
|
@@ -1,4 +1,6 @@
|
|
1
1
|
require_relative "threaded_job_queue"
|
2
|
+
require_relative "staging_area"
|
3
|
+
require "digest/md5" unless defined?(Digest::MD5)
|
2
4
|
|
3
5
|
module CookbookOmnifetch
|
4
6
|
|
@@ -27,7 +29,7 @@ module CookbookOmnifetch
|
|
27
29
|
next unless @metadata.key?(type.to_s)
|
28
30
|
|
29
31
|
@metadata[type.to_s].each do |file|
|
30
|
-
yield file["url"], file["path"]
|
32
|
+
yield file["url"], file["path"], file["checksum"]
|
31
33
|
end
|
32
34
|
end
|
33
35
|
end
|
@@ -46,50 +48,61 @@ module CookbookOmnifetch
|
|
46
48
|
end
|
47
49
|
|
48
50
|
def install
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
51
|
+
StagingArea.stage(install_path) do |staging_path|
|
52
|
+
FileUtils.cp_r("#{install_path}/.", staging_path) if Dir.exist?(install_path)
|
53
|
+
metadata = http_client.get(url_path)
|
54
|
+
clean_cache(staging_path, metadata)
|
55
|
+
sync_cache(staging_path, metadata)
|
56
|
+
end
|
57
|
+
end
|
54
58
|
|
55
|
-
|
56
|
-
|
57
|
-
|
59
|
+
# Removes files from cache that are not supposed to be there, based on
|
60
|
+
# files in metadata.
|
61
|
+
def clean_cache(staging_path, metadata)
|
62
|
+
actual_file_list = Dir.glob(File.join(staging_path, "**/*"))
|
63
|
+
expected_file_list = []
|
64
|
+
CookbookMetadata.new(metadata).files { |_, path, _| expected_file_list << File.join(staging_path, path) }
|
65
|
+
|
66
|
+
extra_files = actual_file_list - expected_file_list
|
67
|
+
extra_files.each do |path|
|
68
|
+
if File.file?(path)
|
69
|
+
FileUtils.rm(path)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
58
73
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
74
|
+
# Downloads any out-of-date files into installer cache, overwriting
|
75
|
+
# those that don't match the checksum provided the metadata @ url_path
|
76
|
+
def sync_cache(staging_path, metadata)
|
77
|
+
queue = ThreadedJobQueue.new
|
78
|
+
CookbookMetadata.new(metadata).files do |url, path, checksum|
|
79
|
+
dest_path = File.join(staging_path, path)
|
80
|
+
FileUtils.mkdir_p(File.dirname(dest_path))
|
81
|
+
if file_outdated?(dest_path, checksum)
|
82
|
+
queue << lambda do |_lock|
|
83
|
+
http_client.streaming_request(url) do |tempfile|
|
84
|
+
tempfile.close
|
85
|
+
FileUtils.mv(tempfile.path, dest_path)
|
86
|
+
end
|
63
87
|
end
|
64
88
|
end
|
65
89
|
end
|
66
|
-
|
67
90
|
queue.process(CookbookOmnifetch.chef_server_download_concurrency)
|
68
|
-
|
69
|
-
FileUtils.mv(staging_path, install_path)
|
70
91
|
end
|
71
92
|
|
72
|
-
#
|
73
|
-
# better chance of getting an atomic move if your temporary working
|
74
|
-
# directory is on the same device/volume as the destination. To support
|
75
|
-
# this, we use a staging directory located under the cache path under the
|
76
|
-
# rather mild assumption that everything under the cache path is going to
|
77
|
-
# be on one device.
|
93
|
+
# Check if a given file (at absolute path) is missing or does has a mismatched md5sum
|
78
94
|
#
|
79
|
-
# @return [
|
80
|
-
def
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
# processes installing the same cookbook at the same time.
|
91
|
-
def staging_cache_key
|
92
|
-
"#{url_path.gsub(/[^[:alnum:]]/, "_")}_#{slug}"
|
95
|
+
# @return [TrueClass, FalseClass]
|
96
|
+
def file_outdated?(path, expected_md5sum)
|
97
|
+
return true unless File.exist?(path)
|
98
|
+
|
99
|
+
md5 = Digest::MD5.new
|
100
|
+
File.open(path, "r") do |file|
|
101
|
+
while (chunk = file.read(1024))
|
102
|
+
md5.update chunk
|
103
|
+
end
|
104
|
+
end
|
105
|
+
md5.to_s != expected_md5sum
|
93
106
|
end
|
94
107
|
end
|
95
108
|
end
|
@@ -0,0 +1,192 @@
|
|
1
|
+
require_relative "exceptions"
|
2
|
+
|
3
|
+
module CookbookOmnifetch
|
4
|
+
# A staging area in which the caller can stage files and publish them to a
|
5
|
+
# local directory.
|
6
|
+
#
|
7
|
+
# When performing long operations such as installing or updating a cookbook
|
8
|
+
# from the web, {StagingArea} allows you to minimize the risk that a process
|
9
|
+
# running in parallel might retrieve an incomplete cookbook from the local
|
10
|
+
# cache before it is completely installed. (See {publish!} for details.)
|
11
|
+
#
|
12
|
+
# {StagingArea} allocates temporary directories on the local file system. It
|
13
|
+
# is the caller's responsibility to use {discard!} when it is done to remove
|
14
|
+
# those directories. The {.stage} method handles directory cleanup for the
|
15
|
+
# staging area it creates before returning.
|
16
|
+
#
|
17
|
+
# @example installing files using the {.stage} helper
|
18
|
+
# CookbookOmnifetch::StagingArea.stage(install_path) do |staging_path|
|
19
|
+
# # Copy files to staging_path
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# @example creating a staging area and publishing it manually
|
23
|
+
# stage = CookbookOmnifetch::StagingArea.new
|
24
|
+
# # Copy files to stage.path
|
25
|
+
# stage.publish!(install_path)
|
26
|
+
# stage.discard!
|
27
|
+
class StagingArea
|
28
|
+
# Creates a staging area, calls a block to populate it, then publishes it.
|
29
|
+
#
|
30
|
+
# {stage} creates a staging area and calls the provided block to populate it
|
31
|
+
# with files. If the staging area does not contain any changes for
|
32
|
+
# +target_path+ (see {#match?}), it cleans up the staging area without
|
33
|
+
# modifying +target_path+. Otherwise, it publishes its contents to
|
34
|
+
# +target_path+ and deletes the staging area. As a safety measure, {stage}
|
35
|
+
# will not publish an empty staging area.
|
36
|
+
#
|
37
|
+
# @param [Pathname] target_path
|
38
|
+
# directory to which the staging area will publish its contents
|
39
|
+
#
|
40
|
+
# @yieldparam staging_path [Pathname]
|
41
|
+
# the directory in which the block should stage its files
|
42
|
+
def self.stage(target_path)
|
43
|
+
sa = new
|
44
|
+
begin
|
45
|
+
yield(sa.path)
|
46
|
+
sa.publish!(target_path) unless sa.empty? || sa.match?(target_path)
|
47
|
+
ensure
|
48
|
+
sa.discard!
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Returns true if the staging area is no longer available for use.
|
53
|
+
#
|
54
|
+
# The staging area is no longer available once {discard!} removes it from
|
55
|
+
# the file system.
|
56
|
+
#
|
57
|
+
# @return [Boolean] whether the staging area is unavailable
|
58
|
+
def unavailable?
|
59
|
+
!!@unavailable
|
60
|
+
end
|
61
|
+
|
62
|
+
# Returns true if the staging area is empty.
|
63
|
+
#
|
64
|
+
# A staging area is considered empty when it has no files or directories in
|
65
|
+
# its path or the staging directory does not exist.
|
66
|
+
#
|
67
|
+
# @raise [StagingAreaNotAvailable]
|
68
|
+
# when called after the staging area destroyed with {discard!}
|
69
|
+
#
|
70
|
+
# @return [Boolean] whether the staging area is empty
|
71
|
+
def empty?
|
72
|
+
!path.exist? || path.empty?
|
73
|
+
end
|
74
|
+
|
75
|
+
# Returns true if the staging area's contents match those of a given path.
|
76
|
+
#
|
77
|
+
# {#match?} compares the contents of the staging area with the contents of
|
78
|
+
# the +compare_path+. It considers the staging area to match if it contains
|
79
|
+
# all of and nothing more than the files and directories present in
|
80
|
+
# +compare_path+ and the content of each file is the same as that of its
|
81
|
+
# corresponding file in +compare_path+. {match?} does not compare file
|
82
|
+
# metadata or the contents of special files.
|
83
|
+
#
|
84
|
+
# @param [String] compare_path
|
85
|
+
# the directory to which the staging area will compare its contents
|
86
|
+
#
|
87
|
+
# @raise [StagingAreaNotAvailable]
|
88
|
+
# when called after the staging area destroyed with {discard!}
|
89
|
+
#
|
90
|
+
# @return [Boolean] whether the staging area matches +compare_path+
|
91
|
+
def match?(compare_path)
|
92
|
+
raise StagingAreaNotAvailable if unavailable?
|
93
|
+
|
94
|
+
target = Pathname(compare_path)
|
95
|
+
return false unless target.exist?
|
96
|
+
|
97
|
+
files = Dir.glob("**/*", File::FNM_DOTMATCH, base: path)
|
98
|
+
target_files = Dir.glob("**/*", File::FNM_DOTMATCH, base: target)
|
99
|
+
return false unless files.sort == target_files.sort
|
100
|
+
|
101
|
+
files.each do |subpath|
|
102
|
+
return false if files_different?(path, target, subpath)
|
103
|
+
end
|
104
|
+
|
105
|
+
true
|
106
|
+
end
|
107
|
+
|
108
|
+
# Path to the staging folder on the file system.
|
109
|
+
#
|
110
|
+
# @raise [StagingAreaNotAvailable]
|
111
|
+
# when called after the staging area destroyed with {discard!}
|
112
|
+
#
|
113
|
+
# @return [Pathname] path to the staging folder
|
114
|
+
def path
|
115
|
+
raise StagingAreaNotAvailable if unavailable?
|
116
|
+
|
117
|
+
return @path unless @path.nil?
|
118
|
+
|
119
|
+
# Dir.mktmpdir returns a directory with restrictive permissions that it
|
120
|
+
# doesn't support modifying, so create a subdirectory under it with
|
121
|
+
# regular permissions for staging.
|
122
|
+
@stage_tmp = Dir.mktmpdir
|
123
|
+
@path = Pathname.new(File.join(@stage_tmp, "staging"))
|
124
|
+
FileUtils.mkdir(@path)
|
125
|
+
@path
|
126
|
+
end
|
127
|
+
|
128
|
+
# Removes the staging area and its contents from the file system.
|
129
|
+
#
|
130
|
+
# The staging area is no longer available once {discard!} removes it from
|
131
|
+
# the file system. Future attempts to use it will raise
|
132
|
+
# {StagingAreaNotAvailable}.
|
133
|
+
def discard!
|
134
|
+
FileUtils.rm_rf(@stage_tmp) unless @stage_tmp.nil?
|
135
|
+
@unavailable = true
|
136
|
+
end
|
137
|
+
|
138
|
+
# Replaces +install_path+ with the contents of the staging area.
|
139
|
+
#
|
140
|
+
# {publish!} removes the target and copies the new content into place using
|
141
|
+
# two atomic file system operations. This eliminates much of the risk
|
142
|
+
# associated with updating the target in a multiprocess environment by
|
143
|
+
# ensuring that another process does not see a partially removed or
|
144
|
+
# populated directory at the +target_path+ while this operation is being
|
145
|
+
# performed.
|
146
|
+
#
|
147
|
+
# Note that it is still possible for the {publish!} to interrupt another
|
148
|
+
# process performing a long operation, such as creating a recursive copy of
|
149
|
+
# the target. In this situation, the other process may create a copy that
|
150
|
+
# consists of a combination of content from the old target directory and the
|
151
|
+
# newly staged files. The other process may also raise an exception should
|
152
|
+
# it try to access the target during a small window in the {publish!}
|
153
|
+
# operation where the target directory does not exist, or tries to open a
|
154
|
+
# file that is no longer part of the target tree after {publish!} completes.
|
155
|
+
# The other process can detect this situation by verifying that the content
|
156
|
+
# of its copy matches the content of +target_path+ after its copy is
|
157
|
+
# complete.
|
158
|
+
#
|
159
|
+
# @param [String] install_path
|
160
|
+
# directory to which the staging area will publish its contents
|
161
|
+
#
|
162
|
+
# @raise [StagingAreaNotAvailable]
|
163
|
+
# when called after the staging area destroyed with {discard!}
|
164
|
+
def publish!(install_path)
|
165
|
+
target = Pathname(install_path)
|
166
|
+
cache_dir = target.parent
|
167
|
+
cache_dir.mkpath
|
168
|
+
Dir.mktmpdir("_STAGING_TMP_", cache_dir) do |tmpdir|
|
169
|
+
newtmp = File.join(tmpdir, "new_cookbook")
|
170
|
+
oldtmp = File.join(tmpdir, "old_cookbook")
|
171
|
+
FileUtils.cp_r(path, newtmp)
|
172
|
+
|
173
|
+
# We could achieve an atomic replace using symbolic links, if they are
|
174
|
+
# supported on all platforms.
|
175
|
+
File.rename(target, oldtmp) if target.exist?
|
176
|
+
File.rename(newtmp, target)
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
private
|
181
|
+
|
182
|
+
# compares two files
|
183
|
+
def files_different?(base1, base2, subpath)
|
184
|
+
file1 = File.join(base1, subpath)
|
185
|
+
file2 = File.join(base2, subpath)
|
186
|
+
return true unless File.ftype(file1) == File.ftype(file2)
|
187
|
+
return true if File.file?(file1) && !FileUtils.cmp(file1, file2)
|
188
|
+
|
189
|
+
false
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: cookbook-omnifetch
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.12.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jamie Winsor
|
@@ -13,7 +13,7 @@ authors:
|
|
13
13
|
autorequire:
|
14
14
|
bindir: bin
|
15
15
|
cert_chain: []
|
16
|
-
date:
|
16
|
+
date: 2022-03-16 00:00:00.000000000 Z
|
17
17
|
dependencies:
|
18
18
|
- !ruby/object:Gem::Dependency
|
19
19
|
name: mixlib-archive
|
@@ -60,6 +60,7 @@ files:
|
|
60
60
|
- lib/cookbook-omnifetch/integration.rb
|
61
61
|
- lib/cookbook-omnifetch/metadata_based_installer.rb
|
62
62
|
- lib/cookbook-omnifetch/path.rb
|
63
|
+
- lib/cookbook-omnifetch/staging_area.rb
|
63
64
|
- lib/cookbook-omnifetch/threaded_job_queue.rb
|
64
65
|
- lib/cookbook-omnifetch/version.rb
|
65
66
|
homepage: https://github.com/chef/cookbook-omnifetch
|
@@ -74,14 +75,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
74
75
|
requirements:
|
75
76
|
- - ">="
|
76
77
|
- !ruby/object:Gem::Version
|
77
|
-
version: '2.
|
78
|
+
version: '2.5'
|
78
79
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
79
80
|
requirements:
|
80
81
|
- - ">="
|
81
82
|
- !ruby/object:Gem::Version
|
82
83
|
version: '0'
|
83
84
|
requirements: []
|
84
|
-
rubygems_version: 3.
|
85
|
+
rubygems_version: 3.1.4
|
85
86
|
signing_key:
|
86
87
|
specification_version: 4
|
87
88
|
summary: Library code to fetch Chef cookbooks from a variety of sources to a local
|