knife_cookbook_dependencies_over_http 0.0.8
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +25 -0
- data/.rbenv-version +1 -0
- data/Gemfile +3 -0
- data/Guardfile +17 -0
- data/LICENSE +22 -0
- data/README.rdoc +107 -0
- data/Rakefile +76 -0
- data/features/clean.feature +29 -0
- data/features/error_messages.feature +16 -0
- data/features/install.feature +24 -0
- data/features/lockfile.feature +25 -0
- data/features/step_definitions/cli_steps.rb +30 -0
- data/features/support/env.rb +33 -0
- data/features/update.feature +38 -0
- data/features/without.feature +27 -0
- data/knife_cookbook_dependencies_over_http.gemspec +38 -0
- data/lib/chef/knife/cookbook_dependencies_clean.rb +19 -0
- data/lib/chef/knife/cookbook_dependencies_install.rb +22 -0
- data/lib/chef/knife/cookbook_dependencies_update.rb +21 -0
- data/lib/kcd.rb +1 -0
- data/lib/kcd/cookbook.rb +237 -0
- data/lib/kcd/cookbookfile.rb +39 -0
- data/lib/kcd/core_ext/kernel.rb +33 -0
- data/lib/kcd/dsl.rb +13 -0
- data/lib/kcd/error_messages.rb +13 -0
- data/lib/kcd/git.rb +86 -0
- data/lib/kcd/knife_utils.rb +13 -0
- data/lib/kcd/lockfile.rb +40 -0
- data/lib/kcd/metacookbook.rb +18 -0
- data/lib/kcd/shelf.rb +100 -0
- data/lib/kcd/version.rb +3 -0
- data/lib/knife_cookbook_dependencies.rb +53 -0
- data/spec/fixtures/cookbooks/example_cookbook/README.md +12 -0
- data/spec/fixtures/cookbooks/example_cookbook/metadata.rb +6 -0
- data/spec/fixtures/cookbooks/example_cookbook/recipes/default.rb +8 -0
- data/spec/fixtures/lockfile_spec/with_lock/Cookbookfile +1 -0
- data/spec/fixtures/lockfile_spec/without_lock/Cookbookfile.lock +5 -0
- data/spec/lib/kcd/cookbook_spec.rb +135 -0
- data/spec/lib/kcd/cookbookfile_spec.rb +26 -0
- data/spec/lib/kcd/dsl_spec.rb +56 -0
- data/spec/lib/kcd/git_spec.rb +58 -0
- data/spec/lib/kcd/lockfile_spec.rb +54 -0
- data/spec/lib/kcd/shelf_spec.rb +81 -0
- data/spec/spec_helper.rb +78 -0
- data/todo.txt +13 -0
- metadata +301 -0
data/lib/kcd/dsl.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
module KnifeCookbookDependencies
|
2
|
+
module ErrorMessages
|
3
|
+
class << self
|
4
|
+
def missing_cookbook(name)
|
5
|
+
"The cookbook #{name} was not found on the Opscode Community site. Provide a git or path key for #{name} if it is unpublished."
|
6
|
+
end
|
7
|
+
|
8
|
+
def missing_cookbookfile
|
9
|
+
"There is no Cookbookfile in #{Dir.pwd}."
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
data/lib/kcd/git.rb
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
require 'tempfile'
|
2
|
+
|
3
|
+
module KnifeCookbookDependencies
|
4
|
+
class Git
|
5
|
+
class << self
|
6
|
+
def git
|
7
|
+
@git ||= find_git
|
8
|
+
end
|
9
|
+
|
10
|
+
#
|
11
|
+
# This is to defeat aliases/shell functions called 'git' and a number of
|
12
|
+
# other problems.
|
13
|
+
#
|
14
|
+
def find_git
|
15
|
+
git_path = nil
|
16
|
+
ENV["PATH"].split(File::PATH_SEPARATOR).each do |path|
|
17
|
+
potential_path = File.join(path, 'git')
|
18
|
+
if File.executable?(potential_path)
|
19
|
+
git_path = potential_path
|
20
|
+
break
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
unless git_path
|
25
|
+
raise "Could not find git. Please ensure it is in your path."
|
26
|
+
end
|
27
|
+
|
28
|
+
return git_path
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
attr_reader :directory
|
33
|
+
attr_reader :repository
|
34
|
+
|
35
|
+
def initialize(repo)
|
36
|
+
@repository = repo
|
37
|
+
end
|
38
|
+
|
39
|
+
def clone
|
40
|
+
# XXX not sure how resilient this is, maybe a fetch/merge strategy would be better.
|
41
|
+
if @directory
|
42
|
+
Dir.chdir @directory do
|
43
|
+
system(self.class.git, "pull")
|
44
|
+
end
|
45
|
+
else
|
46
|
+
@directory = Dir.mktmpdir
|
47
|
+
system(self.class.git, "clone", @repository, @directory)
|
48
|
+
end
|
49
|
+
|
50
|
+
error_check
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
def checkout(ref)
|
55
|
+
clone
|
56
|
+
|
57
|
+
Dir.chdir @directory do
|
58
|
+
system(self.class.git, "checkout", "-q", ref)
|
59
|
+
end
|
60
|
+
|
61
|
+
error_check
|
62
|
+
end
|
63
|
+
|
64
|
+
def ref
|
65
|
+
return nil unless @directory
|
66
|
+
|
67
|
+
this_ref = nil
|
68
|
+
|
69
|
+
Dir.chdir @directory do
|
70
|
+
this_ref = `#{self.class.git} rev-parse HEAD`.strip
|
71
|
+
end
|
72
|
+
|
73
|
+
return this_ref
|
74
|
+
end
|
75
|
+
|
76
|
+
def clean
|
77
|
+
FileUtils.rm_rf @directory if @directory
|
78
|
+
end
|
79
|
+
|
80
|
+
def error_check
|
81
|
+
if $?.exitstatus != 0
|
82
|
+
raise "Did not succeed executing git; check the output above."
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'stringio'
|
2
|
+
require 'chef/config'
|
3
|
+
|
4
|
+
module KnifeCookbookDependencies
|
5
|
+
module KnifeUtils
|
6
|
+
def self.capture_knife_output(knife_obj)
|
7
|
+
knife_obj.ui = Chef::Knife::UI.new(StringIO.new, StringIO.new, StringIO.new, :format => :json)
|
8
|
+
knife_obj.run
|
9
|
+
knife_obj.ui.stdout.rewind
|
10
|
+
knife_obj.ui.stdout.read
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
data/lib/kcd/lockfile.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
module KnifeCookbookDependencies
|
2
|
+
class Lockfile
|
3
|
+
DEFAULT_FILENAME = "#{KCD::DEFAULT_FILENAME}.lock"
|
4
|
+
|
5
|
+
def initialize(cookbooks)
|
6
|
+
@cookbooks = cookbooks
|
7
|
+
end
|
8
|
+
|
9
|
+
def write(filename = DEFAULT_FILENAME)
|
10
|
+
content = @cookbooks.map do |cookbook|
|
11
|
+
get_cookbook_definition(cookbook)
|
12
|
+
end.join("\n")
|
13
|
+
File.open(DEFAULT_FILENAME, "wb") { |f| f.write content }
|
14
|
+
end
|
15
|
+
|
16
|
+
def get_cookbook_definition(cookbook)
|
17
|
+
definition = "cookbook '#{cookbook.name}'"
|
18
|
+
|
19
|
+
if cookbook.from_git?
|
20
|
+
definition += ", :git => '#{cookbook.git_repo}', :ref => '#{cookbook.git_ref || 'HEAD'}'"
|
21
|
+
elsif cookbook.from_path?
|
22
|
+
definition += ", :path => '#{cookbook.local_path}'"
|
23
|
+
else
|
24
|
+
definition += ", :locked_version => '#{cookbook.locked_version}'"
|
25
|
+
end
|
26
|
+
|
27
|
+
return definition
|
28
|
+
end
|
29
|
+
|
30
|
+
def remove!
|
31
|
+
self.class.remove!
|
32
|
+
end
|
33
|
+
|
34
|
+
class << self
|
35
|
+
def remove!
|
36
|
+
FileUtils.rm_f DEFAULT_FILENAME
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module KnifeCookbookDependencies
|
2
|
+
class MetaCookbook
|
3
|
+
attr_reader :name, :dependencies
|
4
|
+
|
5
|
+
def initialize(name, dependencies)
|
6
|
+
@name = name
|
7
|
+
@dependencies = dependencies
|
8
|
+
end
|
9
|
+
|
10
|
+
def versions
|
11
|
+
@v ||= [DepSelector::Version.new('1.0.0')]
|
12
|
+
end
|
13
|
+
|
14
|
+
def latest_constrained_version
|
15
|
+
versions.first
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
data/lib/kcd/shelf.rb
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
module KnifeCookbookDependencies
|
2
|
+
class Shelf
|
3
|
+
META_COOKBOOK_NAME = 'cookbook_dependencies_shelf'
|
4
|
+
|
5
|
+
attr_accessor :cookbooks, :active_group, :excluded_groups
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@cookbooks = []
|
9
|
+
end
|
10
|
+
|
11
|
+
def shelve_cookbook(*args)
|
12
|
+
@cookbooks << (args.first.is_a?(Cookbook) ? args.first : Cookbook.new(*args))
|
13
|
+
end
|
14
|
+
|
15
|
+
def resolve_dependencies
|
16
|
+
graph = DepSelector::DependencyGraph.new
|
17
|
+
|
18
|
+
post_exclusions = requested_cookbooks
|
19
|
+
cookbooks_to_install = @cookbooks.select {|c| post_exclusions.include?(c.name)}
|
20
|
+
# all cookbooks in the Cookbookfile are dependencies of the shelf
|
21
|
+
shelf = MetaCookbook.new(META_COOKBOOK_NAME, cookbooks_to_install)
|
22
|
+
|
23
|
+
self.class.populate_graph graph, shelf
|
24
|
+
|
25
|
+
|
26
|
+
selector = DepSelector::Selector.new(graph)
|
27
|
+
|
28
|
+
solution = quietly do
|
29
|
+
selector.find_solution([DepSelector::SolutionConstraint.new(graph.package(META_COOKBOOK_NAME))])
|
30
|
+
end
|
31
|
+
|
32
|
+
solution.delete META_COOKBOOK_NAME
|
33
|
+
solution
|
34
|
+
end
|
35
|
+
|
36
|
+
def write_lockfile
|
37
|
+
KCD::Lockfile.new(@cookbooks).write
|
38
|
+
end
|
39
|
+
|
40
|
+
def get_cookbook(name)
|
41
|
+
@cookbooks.select { |c| c.name == name }.first
|
42
|
+
end
|
43
|
+
|
44
|
+
def populate_cookbooks_directory
|
45
|
+
cookbooks_from_path = @cookbooks.select(&:from_path?) | @cookbooks.select(&:from_git?)
|
46
|
+
KCD.ui.info "Fetching cookbooks:"
|
47
|
+
resolve_dependencies.each_pair do |cookbook_name, version|
|
48
|
+
cookbook = cookbooks_from_path.select { |c| c.name == cookbook_name }.first || Cookbook.new(cookbook_name, version.to_s)
|
49
|
+
@cookbooks << cookbook
|
50
|
+
cookbook.download
|
51
|
+
cookbook.unpack
|
52
|
+
cookbook.copy_to_cookbooks_directory
|
53
|
+
cookbook.locked_version = version
|
54
|
+
end
|
55
|
+
@cookbooks = @cookbooks.uniq.reject { |x| x.locked_version.nil? }
|
56
|
+
end
|
57
|
+
|
58
|
+
def exclude(groups)
|
59
|
+
groups = groups.to_s.split(/[,:]/) unless groups.is_a?(Array)
|
60
|
+
@excluded_groups = groups.collect {|c| c.to_sym}
|
61
|
+
end
|
62
|
+
|
63
|
+
def cookbook_groups
|
64
|
+
{}.tap do |groups|
|
65
|
+
@cookbooks.each do |cookbook|
|
66
|
+
cookbook.groups.each do |group|
|
67
|
+
groups[group] ||= []
|
68
|
+
groups[group] << cookbook.name
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def requested_cookbooks
|
75
|
+
return @cookbooks.collect(&:name) unless @excluded_groups
|
76
|
+
[].tap do |r|
|
77
|
+
cookbook_groups.each do |group, cookbooks|
|
78
|
+
r << cookbooks unless @excluded_groups.include?(group.to_sym)
|
79
|
+
end
|
80
|
+
end.flatten.uniq
|
81
|
+
end
|
82
|
+
|
83
|
+
class << self
|
84
|
+
def populate_graph(graph, cookbook)
|
85
|
+
package = graph.package cookbook.name
|
86
|
+
cookbook.versions.each { |v| package.add_version(v) }
|
87
|
+
cookbook.dependencies.each do |dependency|
|
88
|
+
graph = populate_graph(graph, dependency)
|
89
|
+
dep = graph.package(dependency.name)
|
90
|
+
version = package.versions.select { |v| v.version == cookbook.latest_constrained_version }.first
|
91
|
+
dependency.version_constraints.each do |constraint|
|
92
|
+
version.dependencies << DepSelector::Dependency.new(dep, constraint)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
graph
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
data/lib/kcd/version.rb
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
module KnifeCookbookDependencies
|
2
|
+
DEFAULT_FILENAME = 'Cookbookfile'
|
3
|
+
COOKBOOKS_DIRECTORY = 'cookbooks'
|
4
|
+
TMP_DIRECTORY = File.join(ENV['TMPDIR'], 'knife_cookbook_dependencies')
|
5
|
+
FileUtils.mkdir_p TMP_DIRECTORY
|
6
|
+
|
7
|
+
autoload :KnifeUtils, 'kcd/knife_utils'
|
8
|
+
|
9
|
+
class << self
|
10
|
+
attr_accessor :ui
|
11
|
+
|
12
|
+
def root
|
13
|
+
File.join(File.dirname(__FILE__), '..')
|
14
|
+
end
|
15
|
+
|
16
|
+
def shelf
|
17
|
+
@shelf ||= KCD::Shelf.new
|
18
|
+
end
|
19
|
+
|
20
|
+
def clear_shelf!
|
21
|
+
@shelf = nil
|
22
|
+
end
|
23
|
+
|
24
|
+
def ui
|
25
|
+
@ui ||= Chef::Knife::UI.new(STDOUT, STDERR, STDIN, {})
|
26
|
+
end
|
27
|
+
|
28
|
+
def clean
|
29
|
+
clear_shelf!
|
30
|
+
Lockfile.remove!
|
31
|
+
FileUtils.rm_rf COOKBOOKS_DIRECTORY
|
32
|
+
FileUtils.rm_rf TMP_DIRECTORY
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Alias for {KnifeCookbookDependencies}
|
38
|
+
KCD = KnifeCookbookDependencies
|
39
|
+
|
40
|
+
require 'dep_selector'
|
41
|
+
require 'zlib'
|
42
|
+
require 'archive/tar/minitar'
|
43
|
+
|
44
|
+
require 'kcd/version'
|
45
|
+
require 'kcd/shelf'
|
46
|
+
require 'kcd/cookbook'
|
47
|
+
require 'kcd/metacookbook'
|
48
|
+
require 'kcd/dsl'
|
49
|
+
require 'kcd/cookbookfile'
|
50
|
+
require 'kcd/lockfile'
|
51
|
+
require 'kcd/git'
|
52
|
+
require 'kcd/error_messages'
|
53
|
+
require 'kcd/core_ext/kernel'
|
@@ -0,0 +1 @@
|
|
1
|
+
cookbook 'nginx', '= 0.101.0'
|
@@ -0,0 +1,135 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module KnifeCookbookDependencies
|
4
|
+
describe Cookbook do
|
5
|
+
subject { Cookbook.new('ntp') }
|
6
|
+
|
7
|
+
after do
|
8
|
+
subject.clean
|
9
|
+
end
|
10
|
+
|
11
|
+
describe do
|
12
|
+
before do
|
13
|
+
Cookbook.any_instance.stub(:versions).and_return ['0.1.1', '0.9.0', '1.0.0', '1.1.8'].collect {|v| Gem::Version.new(v) }
|
14
|
+
end
|
15
|
+
|
16
|
+
# FIXME: This test is flakey
|
17
|
+
it "should raise an error if the cookbook is unpacked without being downloaded first" do
|
18
|
+
-> { subject.unpack(subject.unpacked_cookbook_path, :clean => true, :download => false) }.should raise_error
|
19
|
+
end
|
20
|
+
|
21
|
+
describe '#unpacked_cookbook_path' do
|
22
|
+
it "should give the path to the directory where the archive should get unpacked" do
|
23
|
+
subject.unpacked_cookbook_path.should == File.join(KCD::TMP_DIRECTORY, 'ntp-1.1.8')
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'should treat cookbooks pulled from a path like a cookbook that has already been unpacked with the path as the unpacked location' do
|
28
|
+
example_cookbook_from_path.unpacked_cookbook_path.should == File.expand_path(File.join(File.dirname(__FILE__), "..", "..", "..", "spec", "fixtures", "cookbooks"))
|
29
|
+
end
|
30
|
+
|
31
|
+
it "should not attempt to download a cookbook being pulled from a path" do
|
32
|
+
Chef::Knife::CookbookSiteDownload.any_instance.should_not_receive(:run)
|
33
|
+
example_cookbook_from_path.download
|
34
|
+
end
|
35
|
+
|
36
|
+
describe "#copy_to" do
|
37
|
+
it "should copy from the unpacked cookbook directory to the target" do
|
38
|
+
example_cookbook_from_path.copy_to_cookbooks_directory
|
39
|
+
File.exists?(File.join(KCD::COOKBOOKS_DIRECTORY, example_cookbook_from_path.name)).should be_true
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
describe "#versions" do
|
45
|
+
it "should return the version in the metadata file as the available versions for a path sourced cookbook" do
|
46
|
+
example_cookbook_from_path.versions.should == [DepSelector::Version.new('0.5.0')]
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
describe '#version_from_metadata' do
|
51
|
+
it "should return the correct version" do
|
52
|
+
subject.version_from_metadata.should == DepSelector::Version.new('1.1.8')
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
describe '#version_constraints_include?' do
|
57
|
+
it "should cycle through all the version constraints to confirm that all of them are satisfied" do
|
58
|
+
subject.add_version_constraint ">= 1.0.0"
|
59
|
+
subject.add_version_constraint "< 2.0.0"
|
60
|
+
subject.version_constraints_include?(DepSelector::Version.new('1.0.0')).should be_true
|
61
|
+
subject.version_constraints_include?(DepSelector::Version.new('1.5.0')).should be_true
|
62
|
+
subject.version_constraints_include?(DepSelector::Version.new('2.0.0')).should be_false
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
describe '#add_version_constraint' do
|
67
|
+
it "should not duplicate version constraints" do
|
68
|
+
subject.add_version_constraint ">= 1.0.0"
|
69
|
+
subject.add_version_constraint ">= 1.0.0"
|
70
|
+
subject.add_version_constraint ">= 1.0.0"
|
71
|
+
subject.add_version_constraint ">= 1.0.0"
|
72
|
+
subject.version_constraints.size.should == 2 # 1 for the
|
73
|
+
# default when
|
74
|
+
# the cookbook
|
75
|
+
# was created in
|
76
|
+
# the subject
|
77
|
+
# instantiation
|
78
|
+
# line
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
describe '#dependencies' do
|
83
|
+
it "should not contain the cookbook itself" do
|
84
|
+
# TODO: Mock
|
85
|
+
Cookbook.new('nginx').dependencies.collect(&:name).include?('nginx').should_not be_true
|
86
|
+
end
|
87
|
+
|
88
|
+
it "should compute the correct dependencies" do
|
89
|
+
cookbook = Cookbook.new('mysql')
|
90
|
+
cookbook.dependencies.should == [Cookbook.new('openssl')]
|
91
|
+
# Second computation is intentional, to make sure it doesn't change the dependency list.
|
92
|
+
cookbook.dependencies.should == [Cookbook.new('openssl')]
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# TODO figure out how to test this. Stubs on classes don't clear after a test.
|
97
|
+
# describe '#unpack' do
|
98
|
+
# it "should not unpack if it is already unpacked" do
|
99
|
+
# Archive::Tar::Minitar.should_receive(:unpack).once
|
100
|
+
# subject.unpack
|
101
|
+
# subject.unpack
|
102
|
+
# end
|
103
|
+
# end
|
104
|
+
|
105
|
+
describe '#groups' do
|
106
|
+
it "should have the default group" do
|
107
|
+
subject.groups.should == [:default]
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
describe '#add_group' do
|
112
|
+
it "should store strings as symbols" do
|
113
|
+
subject.add_group "foo"
|
114
|
+
subject.groups.should == [:default, :foo]
|
115
|
+
end
|
116
|
+
|
117
|
+
it "should not store duplicate groups" do
|
118
|
+
subject.add_group "bar"
|
119
|
+
subject.add_group "bar"
|
120
|
+
subject.add_group :bar
|
121
|
+
subject.groups.should == [:default, :bar]
|
122
|
+
end
|
123
|
+
|
124
|
+
it "should add multiple groups" do
|
125
|
+
subject.add_group "baz", "quux"
|
126
|
+
subject.groups.should == [:default, :baz, :quux]
|
127
|
+
end
|
128
|
+
|
129
|
+
it "should handle multiple groups as an array" do
|
130
|
+
subject.add_group ["baz", "quux"]
|
131
|
+
subject.groups.should == [:default, :baz, :quux]
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|