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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9491fb99bc2194cfb805a87ab4d543be68e51a4fc4c3d27e954c62e96b42df33
4
- data.tar.gz: 1f364b47c66fe2c7ac35d45f9a343dc8a14e6eaad3050007db5c4c59f255029b
3
+ metadata.gz: a84aa2f4551b271e3c13a1260d6070ed4a637ca777ce81343dc3087bf73d9122
4
+ data.tar.gz: b93c4da8a8ab2a64f7c62e4b9a677ca0276f8a7a9ae6c82f73e21a644f03e07b
5
5
  SHA512:
6
- metadata.gz: bfe5d751f1891af706db45ebf57867207078876419eaa7f7629ee4ae9b5437897528782062186e2eec04cee2516dae01c602d68581b6b759f7b1a8eb7eea6fea
7
- data.tar.gz: 6f1668723a48ca94e337e1aba18138a745b82a65008ad88c41fea1e63318120002a516334d238ad61c3faf8b623f5625220a291fa53fd1e17ae6229151389b8d
6
+ metadata.gz: e5043f1653e630012f06582d1917ddb72a7904e0a6b283ad5db6f2020a7b26859280bcf50e8ee19298dd00798d1738f644aa5348aafc5571c003addc3fe69fbb
7
+ data.tar.gz: 7568ed165159ef9800da6211ad8d3bb53ffedde3cd6923a37dd7506dd425f53f0994f49c05cdff8858fbb63ceacf271b4e4f7c54add4ebeb646e6597dc4f1daa
@@ -1,7 +1,7 @@
1
1
  require_relative "base"
2
2
 
3
- require "mixlib/archive"
4
- require "tmpdir"
3
+ require "mixlib/archive" unless defined?(Mixlib::Archive)
4
+ require "tmpdir" unless defined?(Dir.mktmpdir)
5
5
 
6
6
  module CookbookOmnifetch
7
7
 
@@ -1,7 +1,7 @@
1
1
  require_relative "base"
2
2
 
3
- require "mixlib/archive"
4
- require "tmpdir"
3
+ require "mixlib/archive" unless defined?(Mixlib::Archive)
4
+ require "tmpdir" unless defined?(Dir.mktmpdir)
5
5
 
6
6
  module CookbookOmnifetch
7
7
 
@@ -106,4 +106,9 @@ module CookbookOmnifetch
106
106
  end
107
107
  end
108
108
 
109
+ class StagingAreaNotAvailable < OmnifetchError
110
+ def initialize
111
+ super "failed to access a StagingArea that is no longer available"
112
+ end
113
+ end
109
114
  end
@@ -1,4 +1,4 @@
1
- require "tmpdir"
1
+ require "tmpdir" unless defined?(Dir.mktmpdir)
2
2
  require_relative "../cookbook-omnifetch"
3
3
  require_relative "base"
4
4
  require_relative "exceptions"
@@ -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
- FileUtils.rm_rf(staging_path) # ensure we have a clean dir, just in case
50
- FileUtils.mkdir_p(staging_root) unless staging_root.exist?
51
- md = http_client.get(url_path)
52
-
53
- queue = ThreadedJobQueue.new
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
- CookbookMetadata.new(md).files do |url, path|
56
- stage = staging_path.join(path)
57
- FileUtils.mkdir_p(File.dirname(stage))
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
- queue << lambda do |_lock|
60
- http_client.streaming_request(url) do |tempfile|
61
- tempfile.close
62
- FileUtils.mv(tempfile.path, stage)
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
- # The path where files are downloaded to. On certain platforms you have a
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 [Pathname]
80
- def staging_root
81
- Pathname.new(CookbookOmnifetch.cache_path).join(".cache_tmp", "metadata-installer")
82
- end
83
-
84
- def staging_path
85
- staging_root.join(staging_cache_key)
86
- end
87
-
88
- # Convert the URL to a safe name for a file and append our random slug.
89
- # This helps us avoid colliding in the case that there are multiple
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
@@ -1,3 +1,3 @@
1
1
  module CookbookOmnifetch
2
- VERSION = "0.10.0".freeze
2
+ VERSION = "0.12.2".freeze
3
3
  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.10.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: 2020-07-15 00:00:00.000000000 Z
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.4'
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.0.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