cookbook-omnifetch 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +22 -0
- data/.rspec +1 -0
- data/Gemfile +3 -0
- data/LICENSE +201 -0
- data/README.md +45 -0
- data/Rakefile +2 -0
- data/cookbook-omnifetch.gemspec +39 -0
- data/lib/cookbook-omnifetch.rb +137 -0
- data/lib/cookbook-omnifetch/artifactserver.rb +122 -0
- data/lib/cookbook-omnifetch/base.rb +77 -0
- data/lib/cookbook-omnifetch/exceptions.rb +106 -0
- data/lib/cookbook-omnifetch/git.rb +183 -0
- data/lib/cookbook-omnifetch/github.rb +8 -0
- data/lib/cookbook-omnifetch/integration.rb +46 -0
- data/lib/cookbook-omnifetch/path.rb +86 -0
- data/lib/cookbook-omnifetch/version.rb +3 -0
- data/spec/fixtures/cookbooks/example_cookbook-0.5.0/README.md +12 -0
- data/spec/fixtures/cookbooks/example_cookbook-0.5.0/metadata.rb +3 -0
- data/spec/fixtures/cookbooks/example_cookbook-0.5.0/recipes/default.rb +8 -0
- data/spec/fixtures/cookbooks/example_cookbook/.gitignore +2 -0
- data/spec/fixtures/cookbooks/example_cookbook/.kitchen.yml +26 -0
- data/spec/fixtures/cookbooks/example_cookbook/Berksfile +1 -0
- data/spec/fixtures/cookbooks/example_cookbook/Berksfile.lock +3 -0
- data/spec/fixtures/cookbooks/example_cookbook/README.md +12 -0
- data/spec/fixtures/cookbooks/example_cookbook/metadata.rb +3 -0
- data/spec/fixtures/cookbooks/example_cookbook/recipes/default.rb +8 -0
- data/spec/spec_helper.rb +44 -0
- data/spec/unit/artifactserver_spec.rb +51 -0
- data/spec/unit/base_spec.rb +81 -0
- data/spec/unit/git_spec.rb +258 -0
- data/spec/unit/path_spec.rb +108 -0
- metadata +144 -0
@@ -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
|