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.
- 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
|