cookbook-omnifetch 0.1.0

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.
@@ -0,0 +1,122 @@
1
+ require 'cookbook-omnifetch/base'
2
+
3
+ # TODO: probably this should be hidden behind DI for http stuff
4
+ require 'zlib'
5
+ require 'archive/tar/minitar'
6
+ require 'tmpdir'
7
+
8
+ module CookbookOmnifetch
9
+
10
+ class ArtifactserverLocation < BaseLocation
11
+
12
+ attr_reader :uri
13
+ attr_reader :cookbook_version
14
+
15
+ def initialize(dependency, options = {})
16
+ super
17
+ @uri ||= options[:artifactserver]
18
+ @cookbook_version = options[:version]
19
+ end
20
+
21
+ def repo_host
22
+ @host ||= URI.parse(uri).host
23
+ end
24
+
25
+ def cookbook_name
26
+ dependency.name
27
+ end
28
+
29
+ # Determine if this revision is installed.
30
+ #
31
+ # @return [Boolean]
32
+ def installed?
33
+ install_path.exist?
34
+ end
35
+
36
+ # Install the given cookbook. Subclasses that implement this method should
37
+ # perform all the installation and validation steps required.
38
+ #
39
+ # @return [void]
40
+ def install
41
+ FileUtils.mkdir_p(cache_root) unless cache_root.exist?
42
+
43
+ http = http_client(uri)
44
+ http.streaming_request(nil) do |tempfile|
45
+ tempfile.close
46
+ FileUtils.mv(tempfile.path, cache_path)
47
+ end
48
+
49
+ Dir.mktmpdir do |staging_dir|
50
+ Zlib::GzipReader.open(cache_path) do |gz_file|
51
+ tar = Archive::Tar::Minitar::Input.new(gz_file)
52
+ tar.each do |e|
53
+ tar.extract_entry(staging_dir, e)
54
+ end
55
+ end
56
+ staged_cookbook_path = File.join(staging_dir, cookbook_name)
57
+ validate_cached!(staged_cookbook_path)
58
+ FileUtils.mv(staged_cookbook_path, install_path)
59
+ end
60
+ end
61
+
62
+ # TODO: DI this.
63
+ def http_client(uri)
64
+ Chef::HTTP::Simple.new(uri)
65
+ end
66
+
67
+ def sanitized_version
68
+ cookbook_version
69
+ end
70
+
71
+ # The path where this cookbook would live in the store, if it were
72
+ # installed.
73
+ #
74
+ # @return [Pathname, nil]
75
+ def install_path
76
+ @install_path ||= CookbookOmnifetch.storage_path.join(cache_key)
77
+ end
78
+
79
+ def cache_key
80
+ "#{dependency.name}-#{cookbook_version}-#{repo_host}"
81
+ end
82
+
83
+ # The cached cookbook for this location.
84
+ #
85
+ # @return [CachedCookbook]
86
+ def cached_cookbook
87
+ raise AbstractFunction,
88
+ "#cached_cookbook must be implemented on #{self.class.name}!"
89
+ end
90
+
91
+ # The lockfile representation of this location.
92
+ #
93
+ # @return [string]
94
+ def to_lock
95
+ raise AbstractFunction,
96
+ "#to_lock must be implemented on #{self.class.name}!"
97
+ end
98
+
99
+
100
+ def ==(other)
101
+ raise "TODO"
102
+ other.is_a?(GitLocation) &&
103
+ other.uri == uri &&
104
+ other.branch == branch &&
105
+ other.tag == tag &&
106
+ other.shortref == shortref &&
107
+ other.rel == rel
108
+ end
109
+
110
+ def cache_root
111
+ Pathname.new(CookbookOmnifetch.cache_path).join('.cache', 'artifactserver')
112
+ end
113
+
114
+ # The path where the pristine tarball is cached
115
+ #
116
+ # @return [Pathname]
117
+ def cache_path
118
+ cache_root.join("#{cache_key}.tgz")
119
+ end
120
+
121
+ end
122
+ end
@@ -0,0 +1,77 @@
1
+ require 'cookbook-omnifetch/exceptions'
2
+
3
+ module CookbookOmnifetch
4
+ class BaseLocation
5
+ attr_reader :dependency
6
+ attr_reader :options
7
+
8
+ def initialize(dependency, options = {})
9
+ @dependency = dependency
10
+ @options = options
11
+ end
12
+
13
+ # Determine if this revision is installed.
14
+ #
15
+ # @return [Boolean]
16
+ def installed?
17
+ raise AbstractFunction,
18
+ "#installed? must be implemented on #{self.class.name}!"
19
+ end
20
+
21
+ # Install the given cookbook. Subclasses that implement this method should
22
+ # perform all the installation and validation steps required.
23
+ #
24
+ # @return [void]
25
+ def install
26
+ raise AbstractFunction,
27
+ "#install must be implemented on #{self.class.name}!"
28
+ end
29
+
30
+ # The cached cookbook for this location.
31
+ #
32
+ # @return [CachedCookbook]
33
+ def cached_cookbook
34
+ raise AbstractFunction,
35
+ "#cached_cookbook must be implemented on #{self.class.name}!"
36
+ end
37
+
38
+ # The lockfile representation of this location.
39
+ #
40
+ # @return [string]
41
+ def to_lock
42
+ raise AbstractFunction,
43
+ "#to_lock must be implemented on #{self.class.name}!"
44
+ end
45
+
46
+ # Ensure the given {CachedCookbook} is valid
47
+ #
48
+ # @param [String] path
49
+ # the path to the possible cookbook
50
+ #
51
+ # @raise [NotACookbook]
52
+ # if the cookbook at the path does not have a metadata
53
+ # @raise [CookbookValidationFailure]
54
+ # if given CachedCookbook does not satisfy the constraint of the location
55
+ # @raise [MismatcheCookboookName]
56
+ # if the cookbook does not have a name or if the name is different
57
+ #
58
+ # @return [true]
59
+ def validate_cached!(path)
60
+ unless CookbookOmnifetch.cookbook?(path)
61
+ raise NotACookbook.new(path)
62
+ end
63
+
64
+ cookbook = CookbookOmnifetch.cached_cookbook_class.from_path(path)
65
+
66
+ unless @dependency.version_constraint.satisfies?(cookbook.version)
67
+ raise CookbookValidationFailure.new(dependency, cookbook)
68
+ end
69
+
70
+ unless @dependency.name == cookbook.cookbook_name
71
+ raise MismatchedCookbookName.new(dependency, cookbook)
72
+ end
73
+
74
+ true
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,106 @@
1
+ module CookbookOmnifetch
2
+
3
+ class OmnifetchError < StandardError
4
+ class << self
5
+ # @param [Integer] code
6
+ def status_code(code)
7
+ define_method(:status_code) { code }
8
+ define_singleton_method(:status_code) { code }
9
+ end
10
+ end
11
+
12
+ alias_method :message, :to_s
13
+ end
14
+
15
+ class AbstractFunction < OmnifetchError; end
16
+
17
+ class GitError < OmnifetchError
18
+ status_code(400)
19
+ end
20
+
21
+ class GitNotInstalled < GitError
22
+ def initialize
23
+ super 'You need to install Git before you can download ' \
24
+ 'cookbooks from git repositories. For more information, please ' \
25
+ 'see the Git docs: http://git-scm.org.'
26
+ end
27
+ end
28
+
29
+ class GitCommandError < GitError
30
+ def initialize(command, path, stderr = nil)
31
+ out = "Git error: command `git #{command}` failed. If this error "
32
+ out << "persists, try removing the cache directory at '#{path}'."
33
+
34
+ if stderr
35
+ out << "Output from the command:\n\n"
36
+ out << stderr
37
+ end
38
+
39
+ super(out)
40
+ end
41
+ end
42
+
43
+ # NOTE: This is the only error class copied from berks that is also used
44
+ # elsewhere in berks, in 'berkshelf/init_generator'
45
+
46
+ class NotACookbook < OmnifetchError
47
+ status_code(141)
48
+
49
+ # @param [String] path
50
+ # the path to the thing that is not a cookbook
51
+ def initialize(path)
52
+ @path = File.expand_path(path) rescue path
53
+ end
54
+
55
+ def to_s
56
+ "The resource at '#{@path}' does not appear to be a valid cookbook. " \
57
+ "Does it have a metadata.rb?"
58
+ end
59
+ end
60
+
61
+ class CookbookValidationFailure < OmnifetchError
62
+ status_code(124)
63
+
64
+ # @param [Location] location
65
+ # the location (or any subclass) raising this validation error
66
+ # @param [CachedCookbook] cached_cookbook
67
+ # the cached_cookbook that does not satisfy the constraint
68
+ def initialize(dependency, cached_cookbook)
69
+ @dependency = dependency
70
+ @cached_cookbook = cached_cookbook
71
+ end
72
+
73
+ def to_s
74
+ "The cookbook downloaded for #{@dependency} did not satisfy the constraint."
75
+ end
76
+ end
77
+
78
+ class MismatchedCookbookName < OmnifetchError
79
+ status_code(114)
80
+
81
+ # @param [Dependency] dependency
82
+ # the dependency with the expected name
83
+ # @param [CachedCookbook] cached_cookbook
84
+ # the cached_cookbook with the mismatched name
85
+ def initialize(dependency, cached_cookbook)
86
+ @dependency = dependency
87
+ @cached_cookbook = cached_cookbook
88
+ end
89
+
90
+ def to_s
91
+ out = "In your Berksfile, you have:\n"
92
+ out << "\n"
93
+ out << " cookbook '#{@dependency.name}'\n"
94
+ out << "\n"
95
+ out << "But that cookbook is actually named '#{@cached_cookbook.cookbook_name}'\n"
96
+ out << "\n"
97
+ out << "This can cause potentially unwanted side-effects in the future.\n"
98
+ out << "\n"
99
+ out << "NOTE: If you do not explicitly set the 'name' attribute in the "
100
+ out << "metadata, the name of the directory will be used instead. This "
101
+ out << "is often a cause of confusion for dependency solving."
102
+ out
103
+ end
104
+ end
105
+
106
+ end
@@ -0,0 +1,183 @@
1
+ require 'tmpdir'
2
+ require 'cookbook-omnifetch'
3
+ require 'cookbook-omnifetch/base'
4
+ require 'cookbook-omnifetch/exceptions'
5
+
6
+ module CookbookOmnifetch
7
+ class GitLocation < BaseLocation
8
+ attr_reader :uri
9
+ attr_reader :branch
10
+ attr_reader :tag
11
+ attr_reader :ref
12
+ attr_reader :revision
13
+ attr_reader :rel
14
+
15
+ def initialize(dependency, options = {})
16
+ super
17
+
18
+ @uri = options[:git]
19
+ @branch = options[:branch]
20
+ @tag = options[:tag]
21
+ @ref = options[:ref]
22
+ @revision = options[:revision]
23
+ @rel = options[:rel]
24
+
25
+ # The revision to parse
26
+ @rev_parse = options[:ref] || options[:branch] || options[:tag] || 'master'
27
+ end
28
+
29
+ # @see BaseLoation#installed?
30
+ def installed?
31
+ (!!revision) && install_path.exist?
32
+ end
33
+
34
+ # Install this git cookbook into the cookbook store. This method leverages
35
+ # a cached git copy and a scratch directory to prevent bad cookbooks from
36
+ # making their way into the cookbook store.
37
+ #
38
+ # @see BaseLocation#install
39
+ def install
40
+ scratch_path = Pathname.new(Dir.mktmpdir)
41
+
42
+ if cached?
43
+ Dir.chdir(cache_path) do
44
+ git %|fetch --force --tags #{uri} "refs/heads/*:refs/heads/*"|
45
+ end
46
+ else
47
+ git %|clone #{uri} "#{cache_path}" --bare --no-hardlinks|
48
+ end
49
+
50
+ Dir.chdir(cache_path) do
51
+ @revision ||= git %|rev-parse #{@rev_parse}|
52
+ end
53
+
54
+ # Clone into a scratch directory for validations
55
+ git %|clone --no-checkout "#{cache_path}" "#{scratch_path}"|
56
+
57
+ # Make sure the scratch directory is up-to-date and account for rel paths
58
+ Dir.chdir(scratch_path) do
59
+ git %|fetch --force --tags "#{cache_path}"|
60
+ git %|reset --hard #{@revision}|
61
+
62
+ if rel
63
+ git %|filter-branch --subdirectory-filter "#{rel}" --force|
64
+ end
65
+ end
66
+
67
+ # Validate the scratched path is a valid cookbook
68
+ validate_cached!(scratch_path)
69
+
70
+ # If we got this far, we should copy
71
+ FileUtils.rm_rf(install_path) if install_path.exist?
72
+ FileUtils.cp_r(scratch_path, install_path)
73
+
74
+ # Remove the git history
75
+ FileUtils.rm_rf(File.join(install_path, '.git'))
76
+
77
+ install_path.chmod(0777 & ~File.umask)
78
+ ensure
79
+ # Ensure the scratch directory is cleaned up
80
+ FileUtils.rm_rf(scratch_path) if scratch_path
81
+ end
82
+
83
+ # @see BaseLocation#cached_cookbook
84
+ def cached_cookbook
85
+ if installed?
86
+ @cached_cookbook ||= CookbookOmnifetch.cached_cookbook_class.from_path(install_path)
87
+ else
88
+ nil
89
+ end
90
+ end
91
+
92
+ def ==(other)
93
+ other.is_a?(GitLocation) &&
94
+ other.uri == uri &&
95
+ other.branch == branch &&
96
+ other.tag == tag &&
97
+ other.shortref == shortref &&
98
+ other.rel == rel
99
+ end
100
+
101
+ def to_s
102
+ info = tag || branch || shortref || @rev_parse
103
+
104
+ if rel
105
+ "#{uri} (at #{info}/#{rel})"
106
+ else
107
+ "#{uri} (at #{info})"
108
+ end
109
+ end
110
+
111
+ def to_lock
112
+ out = " git: #{uri}\n"
113
+ out << " revision: #{revision}\n"
114
+ out << " ref: #{shortref}\n" if shortref
115
+ out << " branch: #{branch}\n" if branch
116
+ out << " tag: #{tag}\n" if tag
117
+ out << " rel: #{rel}\n" if rel
118
+ out
119
+ end
120
+
121
+ # The path where this cookbook would live in the store, if it were
122
+ # installed.
123
+ #
124
+ # @return [Pathname, nil]
125
+ def install_path
126
+ CookbookOmnifetch.storage_path
127
+ .join(cache_key)
128
+ end
129
+
130
+ def cache_key
131
+ "#{dependency.name}-#{revision}"
132
+ end
133
+
134
+ protected
135
+
136
+ # The short ref (if one was given).
137
+ #
138
+ # @return [String, nil]
139
+ def shortref
140
+ ref && ref[0...7]
141
+ end
142
+
143
+ private
144
+
145
+ # Perform a git command.
146
+ #
147
+ # @param [String] command
148
+ # the command to run
149
+ # @param [Boolean] error
150
+ # whether to raise error if the command fails
151
+ #
152
+ # @raise [String]
153
+ # the +$stdout+ from the command
154
+ def git(command, error = true)
155
+ unless CookbookOmnifetch.which('git') || CookbookOmnifetch.which('git.exe')
156
+ raise GitNotInstalled.new
157
+ end
158
+
159
+ response = CookbookOmnifetch.shell_out_class.shell_out(%|git #{command}|)
160
+
161
+ if error && !response.success?
162
+ raise GitCommandError.new(command, cache_path, response.stderr)
163
+ end
164
+
165
+ response.stdout.strip
166
+ end
167
+
168
+ # Determine if this git repo has already been downloaded.
169
+ #
170
+ # @return [Boolean]
171
+ def cached?
172
+ cache_path.exist?
173
+ end
174
+
175
+ # The path where this git repository is cached.
176
+ #
177
+ # @return [Pathname]
178
+ def cache_path
179
+ Pathname.new(CookbookOmnifetch.cache_path)
180
+ .join('.cache', 'git', Digest::SHA1.hexdigest(uri))
181
+ end
182
+ end
183
+ end