knife_cookbook_dependencies 0.0.3 → 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. data/.gitignore +4 -1
  2. data/README.rdoc +41 -2
  3. data/Rakefile +42 -0
  4. data/features/lib/chef/knife/error_messages.feature +16 -0
  5. data/features/lib/chef/knife/lockfile.feature +25 -0
  6. data/features/lib/chef/knife/without.feature +27 -0
  7. data/features/support/env.rb +30 -0
  8. data/features/support/step_definitions.rb +22 -0
  9. data/knife_cookbook_dependencies.gemspec +6 -2
  10. data/lib/chef/knife/cookbook_dependencies_install.rb +12 -6
  11. data/lib/kcd.rb +1 -0
  12. data/lib/{knife_cookbook_dependencies → kcd}/cookbook.rb +72 -40
  13. data/lib/{knife_cookbook_dependencies → kcd}/cookbookfile.rb +12 -11
  14. data/lib/kcd/dsl.rb +13 -0
  15. data/lib/{knife_cookbook_dependencies → kcd}/error_messages.rb +1 -1
  16. data/lib/{knife_cookbook_dependencies → kcd}/git.rb +0 -0
  17. data/lib/{knife_cookbook_dependencies → kcd}/knife_utils.rb +1 -1
  18. data/lib/{knife_cookbook_dependencies → kcd}/lockfile.rb +8 -5
  19. data/lib/{knife_cookbook_dependencies → kcd}/metacookbook.rb +1 -1
  20. data/lib/{knife_cookbook_dependencies → kcd}/shelf.rb +30 -5
  21. data/lib/{knife_cookbook_dependencies → kcd}/version.rb +1 -1
  22. data/lib/knife_cookbook_dependencies.rb +20 -13
  23. data/spec/acceptance/knife_cookbook_dependencies_spec.rb +1 -11
  24. data/spec/fixtures/lockfile_spec/with_lock/Cookbookfile +1 -0
  25. data/spec/fixtures/lockfile_spec/without_lock/Cookbookfile.lock +5 -0
  26. data/spec/lib/{knife_cookbook_dependencies → kcd}/cookbook_spec.rb +43 -14
  27. data/spec/lib/{knife_cookbook_dependencies → kcd}/cookbookfile_spec.rb +3 -3
  28. data/spec/lib/kcd/dsl_spec.rb +56 -0
  29. data/spec/lib/{knife_cookbook_dependencies → kcd}/git_spec.rb +0 -0
  30. data/spec/lib/kcd/lockfile_spec.rb +54 -0
  31. data/spec/lib/kcd/shelf_spec.rb +81 -0
  32. data/spec/spec_helper.rb +22 -2
  33. data/todo.txt +8 -8
  34. metadata +98 -51
  35. data/lib/knife_cookbook_dependencies/dependency_reader.rb +0 -46
  36. data/lib/knife_cookbook_dependencies/dsl.rb +0 -7
  37. data/spec/lib/knife_cookbook_dependencies/dependency_reader_spec.rb +0 -42
  38. data/spec/lib/knife_cookbook_dependencies/dsl_spec.rb +0 -29
  39. data/spec/lib/knife_cookbook_dependencies/shelf_spec.rb +0 -37
data/.gitignore CHANGED
@@ -19,4 +19,7 @@ cookbooks
19
19
  *~
20
20
  *.tar*
21
21
  \#*
22
- Cookbookfile*
22
+ ^Cookbookfile*
23
+ .DS_Store
24
+ spec/fixtures/vcr_cassettes/*
25
+ *.sw[op]
data/README.rdoc CHANGED
@@ -1,6 +1,44 @@
1
1
  = Knife Cookbook Dependencies
2
2
 
3
- = Running tests
3
+ A knife plugin to manage cookbook dependencies.
4
+
5
+ == Getting Started
6
+
7
+ To start, just install the plugin
8
+
9
+ $ gem install knife_cookbook_dependencies
10
+
11
+ And add your dependencies to the Cookbookfile in the top-level of your repo
12
+
13
+ cookbook 'memcached'
14
+ cookbook 'ngnix'
15
+
16
+ Install the dependencies to cookbooks/
17
+
18
+ $ knife cookbook dependencies install
19
+
20
+ Put dependencies in a group so they can be ignored at deploy time. This is especially helpful
21
+ when working with chef-solo.
22
+
23
+ group :solo do
24
+ cookbook 'base'
25
+ end
26
+
27
+ If you only have 1 in the group, you can pass it as an option
28
+
29
+ cookbook 'base', :group => 'solo'
30
+
31
+ By default, cookbooks are loaded from the community site. You can load them from a git repository
32
+
33
+ cookbook 'nfs', :git => 'git://github.com/RiotGames/cookbook-nfs.git'
34
+
35
+ Or from a local path
36
+
37
+ cookbook 'myapp', :path => './cookbook'
38
+
39
+ = Contributing
40
+
41
+ == Running tests
4
42
 
5
43
  === Install prerequisites
6
44
 
@@ -27,4 +65,5 @@ Bundler will install all gems and their dependencies required for testing and de
27
65
 
28
66
  * Josiah Kiehl (<josiah@skirmisher.net>)
29
67
  * Jamie Winsor (<jamie@vialstudios.com>)
30
- * Erik Hollensbe (<erik@hollensbe.org>)
68
+ * Erik Hollensbe (<erik@hollensbe.org>)
69
+ * Michael Ivey (<ivey@gweezlebur.com>)
data/Rakefile CHANGED
@@ -27,3 +27,45 @@ end
27
27
 
28
28
  task :check => [:default, "rdoc:check"]
29
29
  task :default => [:clean, :spec]
30
+
31
+ begin
32
+ require 'rspec/core/rake_task'
33
+
34
+ desc "Run specs"
35
+ RSpec::Core::RakeTask.new(:spec) do |r|
36
+ r.rspec_path = "bundle exec rspec"
37
+ end
38
+ rescue LoadError
39
+ desc 'RSpec rake task not available'
40
+ task :spec do
41
+ abort 'RSpec rake task is not available. Be sure to install rspec.'
42
+ end
43
+ end
44
+
45
+ begin
46
+ require 'cucumber'
47
+ require 'cucumber/rake/task'
48
+
49
+ Cucumber::Rake::Task.new(:features) do |t|
50
+ t.cucumber_opts = "--format progress --tags ~@wip --tags ~@live"
51
+ end
52
+
53
+ namespace :features do
54
+ Cucumber::Rake::Task.new(:wip) do |t|
55
+ t.cucumber_opts = "--format progress --tags @wip"
56
+ end
57
+
58
+ Cucumber::Rake::Task.new(:current) do |t|
59
+ t.cucumber_opts = "--format progress --tags @current"
60
+ end
61
+
62
+ Cucumber::Rake::Task.new(:tag) do |t|
63
+ t.cucumber_opts = "--format progress --tags @#{ENV['tag']}"
64
+ end
65
+ end
66
+ rescue LoadError
67
+ desc 'Cucumber rake task not available'
68
+ task :features do
69
+ abort 'Cucumber rake task is not available. Be sure to install cucumber.'
70
+ end
71
+ end
@@ -0,0 +1,16 @@
1
+ Feature: Friendly error messages
2
+ As a CLI user
3
+ I want to have friendly human readable error messages
4
+ So I can identify what went wrong without ambiguity
5
+
6
+ Scenario: running without a Cookbookfile
7
+ When I run `knife cookbook dependencies install`
8
+ Then the output should contain "FATAL: There is no Cookbookfile in "
9
+
10
+ Scenario: when missing a cookbook
11
+ Given I write to "Cookbookfile" with:
12
+ """
13
+ cookbook "doesntexist"
14
+ """
15
+ When I run `knife cookbook dependencies install`
16
+ Then the output should contain "FATAL: The cookbook doesntexist was not found on the Opscode Community site. Provide a git or path key for doesntexist if it is unpublished."
@@ -0,0 +1,25 @@
1
+ Feature: Cookbookfile.lock
2
+ As a user
3
+ I want my versions to be locked even when I don't specify versions in my Cookbookfile
4
+ So when I share my repository, all other developers get the same versions that I did when I installed.
5
+
6
+ @slow_process
7
+ Scenario: Writing the Cookbookfile.lock
8
+ Given I write to "Cookbookfile" with:
9
+ """
10
+ cookbook 'ntp'
11
+ cookbook 'mysql', git: 'https://github.com/opscode-cookbooks/mysql.git', :ref => '190c0c2267785b7b9b303369b8a64ed04364d5f9'
12
+ cookbook 'example_cookbook', path: File.join(KCD.root, 'spec', 'fixtures', 'cookbooks')
13
+ """
14
+ When I run `knife cookbook dependencies install`
15
+ When I sleep
16
+ Then a file named "Cookbookfile.lock" should exist in the current directory
17
+ And the file "Cookbookfile.lock" should contain in the current directory:
18
+ """
19
+ cookbook 'mysql', :git => 'https://github.com/opscode-cookbooks/mysql.git', :ref => '190c0c2267785b7b9b303369b8a64ed04364d5f9'
20
+ cookbook 'example_cookbook', :path => .*
21
+ cookbook 'ntp', :locked_version => '1.1.8'
22
+ cookbook 'openssl', :locked_version => '1.0.0'
23
+ cookbook 'windows', :locked_version => '1.2.12'
24
+ cookbook 'chef_handler', :locked_version => '1.0.6'
25
+ """
@@ -0,0 +1,27 @@
1
+ Feature: --without block
2
+ As a user
3
+ I want to be able to exclude blocks in my Cookbookfile
4
+ So I can have cookbooks organized for use in different situations in a single Cookbookfile
5
+
6
+ @slow_process
7
+ Scenario: Exclude a block
8
+ Given I write to "Cookbookfile" with:
9
+ """
10
+ group :notme do
11
+ cookbook "nginx"
12
+ end
13
+
14
+ cookbook "mysql"
15
+
16
+ group :takeme do
17
+ cookbook "ntp"
18
+ end
19
+ """
20
+ When I run `knife cookbook dependencies install --without notme`
21
+ Then the following directories should exist:
22
+ | cookbooks/mysql |
23
+ | cookbooks/openssl |
24
+ | cookbooks/ntp |
25
+ And the following directories should not exist:
26
+ | cookbooks/nginx |
27
+
@@ -0,0 +1,30 @@
1
+ require 'spork'
2
+
3
+ Spork.prefork do
4
+ require 'rspec'
5
+ require 'pp'
6
+ require 'aruba/cucumber'
7
+ require 'vcr'
8
+
9
+ APP_ROOT = File.expand_path('../../', __FILE__)
10
+
11
+ Dir[File.join(APP_ROOT, "spec/support/**/*.rb")].each {|f| require f}
12
+
13
+ After do
14
+ KCD.clean
15
+ end
16
+
17
+ Around do |scenario, block|
18
+ VCR.use_cassette(scenario.title) do
19
+ block.call
20
+ end
21
+ end
22
+
23
+ Before('@slow_process') do
24
+ @aruba_io_wait_seconds = 5
25
+ end
26
+ end
27
+
28
+ Spork.each_run do
29
+ require 'kcd'
30
+ end
@@ -0,0 +1,22 @@
1
+ Then /^I trace$/ do
2
+ end
3
+
4
+ When /^I sleep$/ do
5
+ sleep 10
6
+ end
7
+
8
+ Then /^a file named "(.*?)" should exist in the current directory$/ do |filename|
9
+ in_current_dir do
10
+ File.exists?(filename).should be_true # not sure why Aruba's
11
+ # #check_file_presence
12
+ # doesn't work here. It
13
+ # looks in the wrong
14
+ # directory.
15
+ end
16
+ end
17
+
18
+ Then /^the file "(.*?)" should contain in the current directory:$/ do |filename, string|
19
+ in_current_dir do
20
+ File.read(filename).should match(Regexp.new(string))
21
+ end
22
+ end
@@ -1,5 +1,6 @@
1
- # -*- encoding: utf-8 -*-
2
- require File.expand_path('../lib/knife_cookbook_dependencies/version', __FILE__)
1
+ # -*- encoding: utf-8; mode: ruby -*-
2
+
3
+ require File.expand_path('../lib/kcd/version', __FILE__)
3
4
 
4
5
  Gem::Specification.new do |s|
5
6
  # TODO FIXME need to modify all of these
@@ -20,6 +21,9 @@ Gem::Specification.new do |s|
20
21
  s.add_runtime_dependency 'chef', '~> 0.10.0'
21
22
  s.add_runtime_dependency 'minitar'
22
23
 
24
+ s.add_development_dependency 'cucumber'
25
+ s.add_development_dependency 'vcr'
26
+ s.add_development_dependency 'webmock'
23
27
  s.add_development_dependency 'aruba'
24
28
  s.add_development_dependency 'rake', '~> 0.9.0'
25
29
  s.add_development_dependency 'rdoc', '~> 3.0'
@@ -1,17 +1,23 @@
1
1
  require 'chef/knife'
2
- require 'knife_cookbook_dependencies'
2
+ require 'kcd'
3
3
 
4
4
  module KnifeCookbookDependencies
5
5
  class CookbookDependenciesInstall < Chef::Knife
6
- banner "knife cookbook dependencies install"
6
+ banner "knife cookbook dependencies install (options)"
7
+
8
+ option :without,
9
+ :short => "-W WITHOUT",
10
+ :long => "--without WITHOUT",
11
+ :description => "Exclude cookbooks that are in these groups"
7
12
 
8
13
  def run
9
14
  ui.info 'Reading Cookbookfile'
10
- ::KnifeCookbookDependencies.ui = ui
11
- ::KnifeCookbookDependencies::Cookbookfile.process_install
15
+ ::KCD.ui = ui
16
+ ::KCD::Cookbookfile.process_install(config[:without])
12
17
  end
13
18
  end
14
19
 
15
- class CookbookDepsInstall < CookbookDependenciesInstall; end
16
-
20
+ class CookbookDepsInstall < CookbookDependenciesInstall
21
+ banner "knife cookbook deps install (options)"
22
+ end
17
23
  end
data/lib/kcd.rb ADDED
@@ -0,0 +1 @@
1
+ require 'knife_cookbook_dependencies'
@@ -1,17 +1,17 @@
1
- require 'knife_cookbook_dependencies/knife_utils'
2
- require 'knife_cookbook_dependencies/git'
3
1
  require 'chef/knife/cookbook_site_download'
4
2
  require 'chef/knife/cookbook_site_show'
3
+ require 'chef/cookbook/metadata'
5
4
 
6
5
  module KnifeCookbookDependencies
7
6
  class Cookbook
8
- attr_reader :name, :version_constraints
7
+ attr_reader :name, :version_constraints, :groups
9
8
  attr_accessor :locked_version
10
9
 
11
10
  DOWNLOAD_LOCATION = ENV["TMPDIR"] || '/tmp'
12
11
 
13
- def initialize *args
12
+ def initialize(*args)
14
13
  @options = args.last.is_a?(Hash) ? args.pop : {}
14
+ @groups = []
15
15
 
16
16
  if from_git? and from_path?
17
17
  raise "Invalid: path and git options provided to #{args[0]}. They are mutually exclusive."
@@ -21,14 +21,17 @@ module KnifeCookbookDependencies
21
21
  @name, constraint_string = args
22
22
 
23
23
  add_version_constraint(if from_path?
24
- "= #{version_from_metadata_file.to_s}"
24
+ "= #{version_from_metadata.to_s}"
25
25
  else
26
26
  constraint_string
27
27
  end)
28
28
  @locked_version = DepSelector::Version.new(@options[:locked_version]) if @options[:locked_version]
29
+ add_group(KnifeCookbookDependencies.shelf.active_group) if KnifeCookbookDependencies.shelf.active_group
30
+ add_group(@options[:group]) if @options[:group]
31
+ add_group(:default) if @groups.empty?
29
32
  end
30
33
 
31
- def add_version_constraint constraint_string
34
+ def add_version_constraint(constraint_string)
32
35
  @version_constraints ||= []
33
36
  @version_constraints << DepSelector::VersionConstraint.new(constraint_string) unless @version_constraints.collect(&:to_s).include? constraint_string
34
37
  end
@@ -36,22 +39,21 @@ module KnifeCookbookDependencies
36
39
  def download(show_output = false)
37
40
  return if @downloaded
38
41
  return if !from_git? and downloaded_archive_exists?
42
+ return if from_path? and !from_git?
39
43
 
40
44
  if from_git?
41
- @git ||= KnifeCookbookDependencies::Git.new(@options[:git])
45
+ @git ||= KCD::Git.new(@options[:git])
42
46
  @git.clone
43
47
  @git.checkout(@options[:ref]) if @options[:ref]
44
48
  @options[:path] ||= @git.directory
45
- elsif from_path?
46
- return
47
49
  else
48
50
  csd = Chef::Knife::CookbookSiteDownload.new([name, latest_constrained_version.to_s, "--file", download_filename])
49
51
  rescue_404 do
50
- output = KnifeCookbookDependencies::KnifeUtils.capture_knife_output(csd)
52
+ output = KCD::KnifeUtils.capture_knife_output(csd)
51
53
  end
52
54
 
53
55
  if show_output
54
- puts output
56
+ output.split(/\r?\n/).each { |x| KCD.ui.info(x) }
55
57
  end
56
58
  end
57
59
 
@@ -59,69 +61,74 @@ module KnifeCookbookDependencies
59
61
  end
60
62
 
61
63
  def copy_to_cookbooks_directory
62
- FileUtils.mkdir_p KnifeCookbookDependencies::COOKBOOKS_DIRECTORY
64
+ FileUtils.mkdir_p KCD::COOKBOOKS_DIRECTORY
63
65
 
64
- target = File.join(KnifeCookbookDependencies::COOKBOOKS_DIRECTORY, @name)
66
+ target = File.join(KCD::COOKBOOKS_DIRECTORY, @name)
65
67
  FileUtils.rm_rf target
66
68
  FileUtils.cp_r full_path, target
67
69
  FileUtils.rm_rf File.join(target, '.git') if from_git?
68
70
  end
69
71
 
70
72
  # TODO: Clean up download repetition functionality here, in #download and the associated test.
71
- def unpack(location = unpacked_cookbook_path, do_clean = false, do_download = true)
73
+ def unpack(location = unpacked_cookbook_path, options={})
72
74
  return true if from_path?
73
- self.clean(File.join(location, @name)) if do_clean
74
- download if do_download
75
- fname = download_filename
76
- if File.directory? location
77
- true # noop
78
- elsif downloaded_archive_exists?
79
- Archive::Tar::Minitar.unpack(Zlib::GzipReader.new(File.open(fname)), location)
80
- true
81
- else
82
- # TODO: Raise friendly error message class
75
+
76
+ clean if options[:clean]
77
+ download if options[:download]
78
+
79
+ unless downloaded_archive_exists? or File.directory?(location)
80
+ # TODO raise friendly error
83
81
  raise "Archive hasn't been downloaded yet"
84
82
  end
83
+
84
+ if downloaded_archive_exists?
85
+ Archive::Tar::Minitar.unpack(Zlib::GzipReader.new(File.open(download_filename)), location)
86
+ end
87
+
88
+ return true
85
89
  end
86
90
 
87
91
  def dependencies
88
92
  download
89
93
  unpack
90
- @dependencies ||= DependencyReader.new(self).read
94
+
95
+ unless @dependencies
96
+ @dependencies = []
97
+ metadata.dependencies.each { |name, constraint| depends(name, constraint) }
98
+ end
99
+
100
+ @dependencies
91
101
  end
92
102
 
93
103
  def latest_constrained_version
94
104
  return @locked_version if @locked_version
95
- return version_from_metadata_file if from_path? or from_git?
105
+ return version_from_metadata if from_path? or from_git?
96
106
 
97
107
  versions.reverse.each do |v|
98
108
  return v if version_constraints_include? v
99
109
  end
100
- KnifeCookbookDependencies.ui.fatal "No version available to fit the following constraints for #{@name}: #{version_constraints.inspect}\nAvailable versions: #{versions.inspect}"
110
+ KCD.ui.fatal "No version available to fit the following constraints for #{@name}: #{version_constraints.inspect}\nAvailable versions: #{versions.inspect}"
101
111
  exit 1
102
112
  end
103
113
 
104
- def version_constraints_include? version
114
+ def version_constraints_include?(version)
105
115
  @version_constraints.inject(true) { |check, constraint| check and constraint.include? version }
106
116
  end
107
117
 
108
118
  def versions
109
119
  return [latest_constrained_version] if @locked_version
110
- return [version_from_metadata_file] if from_path? or from_git?
120
+ return [version_from_metadata] if from_path? or from_git?
111
121
  cookbook_data['versions'].collect { |v| DepSelector::Version.new(v.split(/\//).last.gsub(/_/, '.')) }.sort
112
122
  end
113
123
 
114
- def version_from_metadata_file
115
- # TODO: make a generic metadata file reader to replace
116
- # dependencyreader and incorporate pulling the version as
117
- # well... knife probably has something like this I can use/steal
118
- DepSelector::Version.new(metadata_file.match(/version\s+[\"\']([0-9\.]*)[\"\']/)[1])
124
+ def version_from_metadata
125
+ DepSelector::Version.new(metadata.version)
119
126
  end
120
127
 
121
128
  def cookbook_data
122
129
  css = Chef::Knife::CookbookSiteShow.new([@name])
123
130
  rescue_404 do
124
- @cookbook_data ||= JSON.parse(KnifeCookbookDependencies::KnifeUtils.capture_knife_output(css))
131
+ @cookbook_data ||= JSON.parse(KCD::KnifeUtils.capture_knife_output(css))
125
132
  end
126
133
  end
127
134
 
@@ -146,18 +153,25 @@ module KnifeCookbookDependencies
146
153
  File.join(full_path, "metadata.rb")
147
154
  end
148
155
 
149
- def metadata_file
156
+ def metadata
150
157
  download
151
158
  unpack
152
- File.open(metadata_filename).read
159
+
160
+ cookbook_metadata = Chef::Cookbook::Metadata.new
161
+ cookbook_metadata.from_file(metadata_filename)
162
+ cookbook_metadata
163
+ end
164
+
165
+ def local_path
166
+ @options[:path]
153
167
  end
154
168
 
155
169
  def from_path?
156
- !!@options[:path]
170
+ !!local_path
157
171
  end
158
172
 
159
173
  def from_git?
160
- !!@options[:git]
174
+ !!git_repo
161
175
  end
162
176
 
163
177
  def git_repo
@@ -168,6 +182,14 @@ module KnifeCookbookDependencies
168
182
  (from_git? && @git) ? @git.ref : nil
169
183
  end
170
184
 
185
+ def add_group(*groups)
186
+ groups = groups.first if groups.first.is_a?(Array)
187
+ groups.each do |group|
188
+ group = group.to_sym
189
+ @groups << group unless @groups.include?(group)
190
+ end
191
+ end
192
+
171
193
  def downloaded_archive_exists?
172
194
  download_filename && File.exists?(download_filename)
173
195
  end
@@ -189,9 +211,19 @@ module KnifeCookbookDependencies
189
211
  begin
190
212
  yield
191
213
  rescue Net::HTTPServerException => e
192
- KnifeCookbookDependencies.ui.fatal ErrorMessages.missing_cookbook(@name) if e.message.match(/404/)
214
+ KCD.ui.fatal ErrorMessages.missing_cookbook(@name) if e.message.match(/404/)
193
215
  exit 100
194
216
  end
195
217
  end
218
+
219
+ private
220
+ def depends(name, constraint = nil)
221
+ dependency_cookbook = KCD.shelf.get_cookbook(name) || @dependencies.find { |c| c.name == name }
222
+ if dependency_cookbook
223
+ dependency_cookbook.add_version_constraint constraint
224
+ else
225
+ @dependencies << Cookbook.new(name, constraint)
226
+ end
227
+ end
196
228
  end
197
229
  end