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.
Files changed (46) hide show
  1. data/.gitignore +25 -0
  2. data/.rbenv-version +1 -0
  3. data/Gemfile +3 -0
  4. data/Guardfile +17 -0
  5. data/LICENSE +22 -0
  6. data/README.rdoc +107 -0
  7. data/Rakefile +76 -0
  8. data/features/clean.feature +29 -0
  9. data/features/error_messages.feature +16 -0
  10. data/features/install.feature +24 -0
  11. data/features/lockfile.feature +25 -0
  12. data/features/step_definitions/cli_steps.rb +30 -0
  13. data/features/support/env.rb +33 -0
  14. data/features/update.feature +38 -0
  15. data/features/without.feature +27 -0
  16. data/knife_cookbook_dependencies_over_http.gemspec +38 -0
  17. data/lib/chef/knife/cookbook_dependencies_clean.rb +19 -0
  18. data/lib/chef/knife/cookbook_dependencies_install.rb +22 -0
  19. data/lib/chef/knife/cookbook_dependencies_update.rb +21 -0
  20. data/lib/kcd.rb +1 -0
  21. data/lib/kcd/cookbook.rb +237 -0
  22. data/lib/kcd/cookbookfile.rb +39 -0
  23. data/lib/kcd/core_ext/kernel.rb +33 -0
  24. data/lib/kcd/dsl.rb +13 -0
  25. data/lib/kcd/error_messages.rb +13 -0
  26. data/lib/kcd/git.rb +86 -0
  27. data/lib/kcd/knife_utils.rb +13 -0
  28. data/lib/kcd/lockfile.rb +40 -0
  29. data/lib/kcd/metacookbook.rb +18 -0
  30. data/lib/kcd/shelf.rb +100 -0
  31. data/lib/kcd/version.rb +3 -0
  32. data/lib/knife_cookbook_dependencies.rb +53 -0
  33. data/spec/fixtures/cookbooks/example_cookbook/README.md +12 -0
  34. data/spec/fixtures/cookbooks/example_cookbook/metadata.rb +6 -0
  35. data/spec/fixtures/cookbooks/example_cookbook/recipes/default.rb +8 -0
  36. data/spec/fixtures/lockfile_spec/with_lock/Cookbookfile +1 -0
  37. data/spec/fixtures/lockfile_spec/without_lock/Cookbookfile.lock +5 -0
  38. data/spec/lib/kcd/cookbook_spec.rb +135 -0
  39. data/spec/lib/kcd/cookbookfile_spec.rb +26 -0
  40. data/spec/lib/kcd/dsl_spec.rb +56 -0
  41. data/spec/lib/kcd/git_spec.rb +58 -0
  42. data/spec/lib/kcd/lockfile_spec.rb +54 -0
  43. data/spec/lib/kcd/shelf_spec.rb +81 -0
  44. data/spec/spec_helper.rb +78 -0
  45. data/todo.txt +13 -0
  46. metadata +301 -0
data/lib/kcd/dsl.rb ADDED
@@ -0,0 +1,13 @@
1
+ module KnifeCookbookDependencies
2
+ module DSL
3
+ def cookbook(*args)
4
+ KCD.shelf.shelve_cookbook(*args)
5
+ end
6
+
7
+ def group *args
8
+ KCD.shelf.active_group = args
9
+ yield
10
+ KCD.shelf.active_group = nil
11
+ end
12
+ end
13
+ end
@@ -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
@@ -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
@@ -0,0 +1,3 @@
1
+ module KnifeCookbookDependencies
2
+ VERSION = "0.0.8"
3
+ end
@@ -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,12 @@
1
+ Description
2
+ ===========
3
+
4
+ Requirements
5
+ ============
6
+
7
+ Attributes
8
+ ==========
9
+
10
+ Usage
11
+ =====
12
+
@@ -0,0 +1,6 @@
1
+ maintainer "Josiah Kiehl"
2
+ maintainer_email "josiah@skirmisher.net"
3
+ license "DO WHAT YOU WANT CAUSE A PIRATE IS FREE"
4
+ description "Installs/Configures example_cookbook"
5
+ long_description IO.read(File.join(File.dirname(__FILE__), 'README.md'))
6
+ version "0.5.0"
@@ -0,0 +1,8 @@
1
+ #
2
+ # Cookbook Name:: example_cookbook
3
+ # Recipe:: default
4
+ #
5
+ # Copyright 2012, YOUR_COMPANY_NAME
6
+ #
7
+ # All rights reserved - Do Not Redistribute
8
+ #
@@ -0,0 +1 @@
1
+ cookbook 'nginx', '= 0.101.0'
@@ -0,0 +1,5 @@
1
+ cookbook 'nginx', :locked_version => '0.101.0'
2
+ cookbook 'build-essential', :locked_version => '1.0.0'
3
+ cookbook 'runit', :locked_version => '0.15.0'
4
+ cookbook 'bluepill', :locked_version => '1.0.4'
5
+ cookbook 'ohai', :locked_version => '1.0.2'
@@ -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