knife_cookbook_dependencies 0.0.3 → 0.0.5

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