cookbook-omnifetch 0.11.1 → 0.12.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/cookbook-omnifetch/chef_server.rb +1 -2
- data/lib/cookbook-omnifetch/chef_server_artifact.rb +1 -2
- data/lib/cookbook-omnifetch/exceptions.rb +5 -0
- data/lib/cookbook-omnifetch/metadata_based_installer.rb +13 -9
- data/lib/cookbook-omnifetch/staging_area.rb +192 -0
- data/lib/cookbook-omnifetch/version.rb +1 -1
- metadata +4 -3
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,5 +1,6 @@
|
|
1
1
|
require_relative "threaded_job_queue"
|
2
|
-
|
2
|
+
require_relative "staging_area"
|
3
|
+
require "digest/md5" unless defined?(Digest::MD5)
|
3
4
|
|
4
5
|
module CookbookOmnifetch
|
5
6
|
|
@@ -47,17 +48,20 @@ module CookbookOmnifetch
|
|
47
48
|
end
|
48
49
|
|
49
50
|
def install
|
50
|
-
|
51
|
-
|
52
|
-
|
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
|
53
57
|
end
|
54
58
|
|
55
59
|
# Removes files from cache that are not supposed to be there, based on
|
56
60
|
# files in metadata.
|
57
|
-
def clean_cache(metadata)
|
58
|
-
actual_file_list = Dir.glob(File.join(
|
61
|
+
def clean_cache(staging_path, metadata)
|
62
|
+
actual_file_list = Dir.glob(File.join(staging_path, "**/*"))
|
59
63
|
expected_file_list = []
|
60
|
-
CookbookMetadata.new(metadata).files { |_, path, _| expected_file_list << File.join(
|
64
|
+
CookbookMetadata.new(metadata).files { |_, path, _| expected_file_list << File.join(staging_path, path) }
|
61
65
|
|
62
66
|
extra_files = actual_file_list - expected_file_list
|
63
67
|
extra_files.each do |path|
|
@@ -69,10 +73,10 @@ module CookbookOmnifetch
|
|
69
73
|
|
70
74
|
# Downloads any out-of-date files into installer cache, overwriting
|
71
75
|
# those that don't match the checksum provided the metadata @ url_path
|
72
|
-
def sync_cache(metadata)
|
76
|
+
def sync_cache(staging_path, metadata)
|
73
77
|
queue = ThreadedJobQueue.new
|
74
78
|
CookbookMetadata.new(metadata).files do |url, path, checksum|
|
75
|
-
dest_path = File.join(
|
79
|
+
dest_path = File.join(staging_path, path)
|
76
80
|
FileUtils.mkdir_p(File.dirname(dest_path))
|
77
81
|
if file_outdated?(dest_path, checksum)
|
78
82
|
queue << lambda do |_lock|
|
@@ -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
|
@@ -81,7 +82,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
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
|