cookbook-omnifetch 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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