berkshelf 0.6.0.beta2 → 0.6.0.beta3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. data/.gitignore +1 -2
  2. data/.travis.yml +7 -0
  3. data/Gemfile +1 -1
  4. data/README.md +1 -0
  5. data/Thorfile +7 -0
  6. data/berkshelf.gemspec +4 -1
  7. data/features/config.feature +97 -0
  8. data/features/config_command.feature +10 -0
  9. data/features/cookbook_command.feature +12 -12
  10. data/features/default_locations.feature +5 -0
  11. data/features/install.feature +16 -0
  12. data/features/json_formatter.feature +1 -1
  13. data/features/step_definitions/cli_steps.rb +5 -1
  14. data/features/step_definitions/filesystem_steps.rb +45 -23
  15. data/features/upload_command.feature +4 -4
  16. data/generator_files/Gemfile.erb +2 -2
  17. data/generator_files/Vagrantfile.erb +34 -13
  18. data/generator_files/config.json +22 -0
  19. data/lib/berkshelf/cli.rb +24 -9
  20. data/lib/berkshelf/config.rb +51 -0
  21. data/lib/berkshelf/config_generator.rb +8 -0
  22. data/lib/berkshelf/config_validator.rb +78 -0
  23. data/lib/berkshelf/cookbook_generator.rb +2 -2
  24. data/lib/berkshelf/core_ext/file_utils.rb +36 -0
  25. data/lib/berkshelf/downloader.rb +41 -14
  26. data/lib/berkshelf/errors.rb +20 -0
  27. data/lib/berkshelf/formatters/human_readable.rb +7 -0
  28. data/lib/berkshelf/git.rb +13 -3
  29. data/lib/berkshelf/init_generator.rb +17 -4
  30. data/lib/berkshelf/locations/chef_api_location.rb +1 -1
  31. data/lib/berkshelf/locations/git_location.rb +1 -1
  32. data/lib/berkshelf/locations/site_location.rb +1 -1
  33. data/lib/berkshelf/version.rb +1 -1
  34. data/lib/berkshelf.rb +17 -10
  35. data/spec/support/knife.rb +1 -1
  36. data/spec/support/matchers/file_system_matchers.rb +16 -1
  37. data/spec/unit/berkshelf/config_spec.rb +91 -0
  38. data/spec/unit/berkshelf/config_validator_spec.rb +68 -0
  39. data/spec/unit/berkshelf/cookbook_source_spec.rb +4 -4
  40. data/spec/unit/berkshelf/core_ext/file_utils_spec.rb +23 -0
  41. data/spec/unit/berkshelf/init_generator_spec.rb +34 -32
  42. data/spec/unit/berkshelf/locations/chef_api_location_spec.rb +1 -1
  43. data/spec/unit/berkshelf/lockfile_spec.rb +2 -2
  44. data/spec/unit/berkshelf/resolver_spec.rb +1 -1
  45. data/spec/unit/berkshelf/uploader_spec.rb +1 -1
  46. metadata +68 -5
  47. data/lib/vagrant_init.rb +0 -2
@@ -0,0 +1,51 @@
1
+ module Berkshelf
2
+ # @author Justin Campbell <justin@justincampbell.me>
3
+ class Config < Hashie::Mash
4
+ DEFAULT_PATH = "~/.berkshelf/config.json"
5
+
6
+ include ActiveModel::Validations
7
+ validates_with ConfigValidator
8
+
9
+ class << self
10
+ # @return [String, nil]
11
+ # the contents of the file
12
+ def file
13
+ File.read path if File.exists? path
14
+ end
15
+
16
+ # @param [#to_s] json
17
+ #
18
+ # @return [Config]
19
+ def from_json(json)
20
+ hash = JSON.parse(json).to_hash
21
+
22
+ new.tap do |config|
23
+ hash.each do |key, value|
24
+ config[key] = value
25
+ end
26
+ end
27
+ end
28
+
29
+ # @return [Config]
30
+ def instance
31
+ @instance ||= if file
32
+ from_json file
33
+ else
34
+ new
35
+ end
36
+ end
37
+
38
+ # @return [String]
39
+ def path
40
+ File.expand_path DEFAULT_PATH
41
+ end
42
+ end
43
+
44
+ # @param [String, Symbol] key
45
+ #
46
+ # @return [Config, Object]
47
+ def [](key)
48
+ super or self.class.new
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,8 @@
1
+ module Berkshelf
2
+ # @author Justin Campbell <justin@justincampbell.me>
3
+ class ConfigGenerator < BaseGenerator
4
+ def generate
5
+ template "config.json", Config.path
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,78 @@
1
+ module Berkshelf
2
+ # @author Justin Campbell <justin@justincampbell.me>
3
+ class ConfigValidator < ActiveModel::Validator
4
+ DEFAULT_STRUCTURE = {
5
+ vagrant: {
6
+ chef: {
7
+ chef_server_url: String,
8
+ validation_client_name: String,
9
+ validation_key_path: String
10
+ },
11
+ vm: {
12
+ box: String,
13
+ box_url: String,
14
+ forward_port: Hash,
15
+ host_name: String,
16
+ network: {
17
+ bridged: Object,
18
+ hostonly: String
19
+ },
20
+ provision: String
21
+ }
22
+ }
23
+ }
24
+
25
+ # Recursively validate the structure of a hash with another hash. If
26
+ # invalid, the actual_hash will have errors added to it.
27
+ #
28
+ # @param [Hash] actual_hash
29
+ # The hash to validate
30
+ #
31
+ # @param [Hash] expected_hash
32
+ # The expected structure of actual_hash
33
+ #
34
+ # @param [Config] config
35
+ # The config object to add errors to. This is only used recursively.
36
+ #
37
+ # @return [Boolean]
38
+ def assert_in_structure(actual_hash, expected_hash, config = nil)
39
+ config ||= actual_hash
40
+
41
+ actual_hash.keys.each do |key|
42
+ unless expected_hash.keys.include? key.to_sym
43
+ config.errors.add key, "is not a valid key"
44
+ return
45
+ end
46
+
47
+ actual = actual_hash[key]
48
+ expected = expected_hash[key.to_sym]
49
+
50
+ if actual.is_a?(Hash) && expected.is_a?(Hash)
51
+ return unless assert_in_structure actual, expected, config
52
+ else
53
+ unless actual.is_a? expected
54
+ config.errors.add key, "should be an instance of #{expected}"
55
+ return
56
+ end
57
+ end
58
+ end
59
+
60
+ true
61
+ end
62
+
63
+ # @see DEFAULT_STRUCTURE
64
+ # @return [Hash]
65
+ def structure
66
+ @structure ||= DEFAULT_STRUCTURE
67
+ end
68
+
69
+ # @param [Config] config
70
+ # The config to validate
71
+ #
72
+ # @return [Boolean]
73
+ def validate(config)
74
+ assert_in_structure config, structure
75
+ end
76
+ end
77
+ end
78
+
@@ -9,11 +9,11 @@ module Berkshelf
9
9
  type: :string,
10
10
  required: true
11
11
 
12
- class_option :vagrant,
12
+ class_option :skip_vagrant,
13
13
  type: :boolean,
14
14
  default: false
15
15
 
16
- class_option :git,
16
+ class_option :skip_git,
17
17
  type: :boolean,
18
18
  default: false
19
19
 
@@ -0,0 +1,36 @@
1
+ require 'fileutils'
2
+
3
+ module FileUtils
4
+ class << self
5
+ alias_method :old_mv, :mv
6
+
7
+ # Override mv to ensure {safe_mv} is run if we are on the Windows platform.
8
+ #
9
+ # @see {FileUtils::mv}
10
+ # @see {safe_mv}
11
+ def mv(src, dest, options = {})
12
+ if windows?
13
+ safe_mv(src, dest, options)
14
+ else
15
+ old_mv(src, dest, options)
16
+ end
17
+ end
18
+
19
+ # If we encounter Errno::EACCES, which seems to happen occasionally on Windows,
20
+ # try to copy and delete the file instead of moving it.
21
+ #
22
+ # @see https://github.com/RiotGames/berkshelf/issues/140
23
+ # @see http://www.ruby-forum.com/topic/1044813
24
+ #
25
+ # @param [String] src
26
+ # @param [String] dest
27
+ # @param [Hash] options
28
+ # @see {FileUtils::mv}
29
+ def safe_mv(src, dest, options = {})
30
+ FileUtils.mv(src, dest, options)
31
+ rescue Errno::EACCES
32
+ FileUtils.cp_r(src, dest)
33
+ FileUtils.rm_rf(src)
34
+ end
35
+ end
36
+ end
@@ -28,7 +28,7 @@ module Berkshelf
28
28
  # to download cookbook sources which do not have an explicit location. An array of default locations will
29
29
  # be used if no locations are explicitly added by the {#add_location} function.
30
30
  def locations
31
- @locations.empty? ? DEFAULT_LOCATIONS : @locations
31
+ @locations.any? ? @locations : DEFAULT_LOCATIONS
32
32
  end
33
33
 
34
34
  # Create a location hash and add it to the end of the array of locations.
@@ -43,9 +43,10 @@ module Berkshelf
43
43
  # @return [Hash]
44
44
  def add_location(type, value, options = {})
45
45
  if has_location?(type, value)
46
- raise DuplicateLocationDefined, "A default '#{type}' location with the value '#{value}' is already defined"
46
+ raise DuplicateLocationDefined,
47
+ "A default '#{type}' location with the value '#{value}' is already defined"
47
48
  end
48
-
49
+
49
50
  @locations.push(type: type, value: value, options: options)
50
51
  end
51
52
 
@@ -54,21 +55,48 @@ module Berkshelf
54
55
  #
55
56
  # @return [Boolean]
56
57
  def has_location?(type, value)
57
- !@locations.select { |loc| loc[:type] == type && loc[:value] == value }.empty?
58
+ @locations.select { |loc| loc[:type] == type && loc[:value] == value }.any?
58
59
  end
59
60
 
60
- # Downloads the given CookbookSource. If the given source does not contain a value for {CookbookSource#location}
61
- # the default locations of this downloader will be used to attempt to retrieve the source.
61
+ # Downloads the given CookbookSource.
62
62
  #
63
63
  # @param [CookbookSource] source
64
64
  # the source to download
65
65
  #
66
66
  # @return [Array]
67
- # an array containing the downloaded CachedCookbook and the Location used to download the cookbook
67
+ # an array containing the downloaded CachedCookbook and the Location used
68
+ # to download the cookbook
68
69
  def download(source)
69
70
  cached_cookbook, location = if source.location
70
- [ source.location.download(storage_path), source.location ]
71
+ begin
72
+ [source.location.download(storage_path), source.location]
73
+ rescue
74
+ Berkshelf.formatter.error "Failed to download #{source.name} from #{source.location}"
75
+
76
+ raise
77
+ end
71
78
  else
79
+ search_locations(source)
80
+ end
81
+
82
+ source.cached_cookbook = cached_cookbook
83
+
84
+ [cached_cookbook, location]
85
+ end
86
+
87
+ private
88
+
89
+ # Searches locations for a CookbookSource. If the source does not contain a
90
+ # value for {CookbookSource#location}, the default locations of this
91
+ # downloader will be used to attempt to retrieve the source.
92
+ #
93
+ # @param [CookbookSource] source
94
+ # the source to download
95
+ #
96
+ # @return [Array]
97
+ # an array containing the downloaded CachedCookbook and the Location used
98
+ # to download the cookbook
99
+ def search_locations(source)
72
100
  cached_cookbook = nil
73
101
  location = nil
74
102
 
@@ -94,13 +122,12 @@ module Berkshelf
94
122
  [ cached_cookbook, location ]
95
123
  end
96
124
 
97
- source.cached_cookbook = cached_cookbook
98
-
99
- [ cached_cookbook, location ]
100
- end
101
-
102
- private
103
125
 
126
+ # Validates that a source is an instance of CookbookSource
127
+ #
128
+ # @param [CookbookSource] source
129
+ #
130
+ # @return [Boolean]
104
131
  def validate_source(source)
105
132
  source.is_a?(Berkshelf::CookbookSource)
106
133
  end
@@ -80,4 +80,24 @@ module Berkshelf
80
80
  end
81
81
 
82
82
  class AmbiguousCookbookName < BerkshelfError; status_code(114); end
83
+
84
+ class InvalidConfiguration < BerkshelfError
85
+ status_code(115)
86
+
87
+ def initialize(errors)
88
+ @errors = errors
89
+ end
90
+
91
+ def to_s
92
+ strings = ["Invalid configuration:"]
93
+
94
+ @errors.messages.each do |key, errors|
95
+ errors.each do |error|
96
+ strings << " #{key} #{error}"
97
+ end
98
+ end
99
+
100
+ strings.join "\n"
101
+ end
102
+ end
83
103
  end
@@ -46,6 +46,13 @@ module Berkshelf
46
46
  def error(message)
47
47
  Berkshelf.ui.error message
48
48
  end
49
+
50
+ # Output a deprecation warning
51
+ #
52
+ # @param [String] message
53
+ def deprecation(message)
54
+ Berkshelf.ui.info "DEPRECATED: #{message}"
55
+ end
49
56
  end
50
57
  end
51
58
  end
data/lib/berkshelf/git.rb CHANGED
@@ -4,8 +4,10 @@ require 'mixlib/shellout'
4
4
  module Berkshelf
5
5
  # @author Jamie Winsor <jamie@vialstudios.com>
6
6
  class Git
7
- GIT_REGEXP = URI.regexp(%w{ https git })
8
- SSH_REGEXP = /(.+)@(.+):(.+)\.git/
7
+ GIT_REGEXP = URI.regexp(%w{ https git }).freeze
8
+ SSH_REGEXP = /(.+)@(.+):(.+)\.git/.freeze
9
+ HAS_QUOTE_RE = %r{\"}.freeze
10
+ HAS_SPACE_RE = %r{\s}.freeze
9
11
 
10
12
  class << self
11
13
  # @overload git(commands)
@@ -16,7 +18,9 @@ module Berkshelf
16
18
  # @return [String]
17
19
  # the output of the execution of the Git command
18
20
  def git(*command)
19
- cmd = Mixlib::ShellOut.new(git_cmd, *command)
21
+ command.unshift(git_cmd)
22
+ command_str = command.map { |p| quote_cmd_arg(p) }.join(' ')
23
+ cmd = Mixlib::ShellOut.new(command_str)
20
24
  cmd.run_command
21
25
 
22
26
  unless cmd.exitstatus == 0
@@ -134,6 +138,12 @@ module Berkshelf
134
138
  def git_cmd
135
139
  @git_cmd ||= find_git
136
140
  end
141
+
142
+ def quote_cmd_arg(arg)
143
+ return arg if HAS_QUOTE_RE.match(arg)
144
+ return arg unless HAS_SPACE_RE.match(arg)
145
+ "\"#{arg}\""
146
+ end
137
147
  end
138
148
  end
139
149
  end
@@ -20,11 +20,11 @@ module Berkshelf
20
20
  type: :boolean,
21
21
  default: false
22
22
 
23
- class_option :vagrant,
23
+ class_option :skip_vagrant,
24
24
  type: :boolean,
25
25
  default: false
26
26
 
27
- class_option :git,
27
+ class_option :skip_git,
28
28
  type: :boolean,
29
29
  default: false
30
30
 
@@ -43,15 +43,22 @@ module Berkshelf
43
43
  class_option :cookbook_name,
44
44
  type: :string
45
45
 
46
+ class_option :berkshelf_config,
47
+ type: :hash,
48
+ default: Config.instance
49
+
46
50
  def generate
51
+ validate_configuration
52
+
47
53
  template "Berksfile.erb", target.join("Berksfile")
48
54
 
49
55
  if options[:chefignore]
50
56
  copy_file "chefignore", target.join("chefignore")
51
57
  end
52
58
 
53
- if options[:git] || options[:scmversion]
59
+ unless options[:skip_git]
54
60
  template "gitignore.erb", target.join(".gitignore")
61
+
55
62
  unless File.exists?(target.join(".git"))
56
63
  inside target do
57
64
  run "git init", capture: true
@@ -71,7 +78,7 @@ module Berkshelf
71
78
  template "Gemfile.erb", target.join("Gemfile")
72
79
  end
73
80
 
74
- if options[:vagrant]
81
+ unless options[:skip_vagrant]
75
82
  template "Vagrantfile.erb", target.join("Vagrantfile")
76
83
  ::Berkshelf::Cli.new([], berksfile: target.join("Berksfile")).invoke(:install)
77
84
  end
@@ -89,5 +96,11 @@ module Berkshelf
89
96
  File.basename(target)
90
97
  end
91
98
  end
99
+
100
+ def validate_configuration
101
+ unless Config.instance.valid?
102
+ raise InvalidConfiguration.new Config.instance.errors
103
+ end
104
+ end
92
105
  end
93
106
  end
@@ -130,7 +130,7 @@ module Berkshelf
130
130
  scratch = download_files(cookbook.manifest)
131
131
 
132
132
  cb_path = File.join(destination, "#{name}-#{version}")
133
- FileUtils.mv(scratch, cb_path, force: true)
133
+ FileUtils.mv(scratch, cb_path)
134
134
 
135
135
  cached = CachedCookbook.from_store_path(cb_path)
136
136
  validate_cached(cached)
@@ -52,7 +52,7 @@ module Berkshelf
52
52
 
53
53
  cb_path = File.join(destination, "#{self.name}-#{Git.rev_parse(tmp_clone)}")
54
54
  FileUtils.rm_rf(cb_path)
55
- FileUtils.mv(tmp_clone, cb_path, force: true)
55
+ FileUtils.mv(tmp_clone, cb_path)
56
56
 
57
57
  cached = CachedCookbook.from_store_path(cb_path)
58
58
  validate_cached(cached)
@@ -53,7 +53,7 @@ module Berkshelf
53
53
  cb_path = File.join(destination, "#{name}-#{version}")
54
54
 
55
55
  self.class.unpack(downloaded_tf.path, dir)
56
- FileUtils.mv(File.join(dir, name), cb_path, force: true)
56
+ FileUtils.mv(File.join(dir, name), cb_path)
57
57
 
58
58
  cached = CachedCookbook.from_store_path(cb_path)
59
59
  validate_cached(cached)
@@ -1,3 +1,3 @@
1
1
  module Berkshelf
2
- VERSION = "0.6.0.beta2"
2
+ VERSION = "0.6.0.beta3"
3
3
  end
data/lib/berkshelf.rb CHANGED
@@ -1,17 +1,21 @@
1
+ require 'chef/cookbook/metadata'
2
+ require 'chef/cookbook_version'
3
+ require 'chef/knife'
4
+ require 'chef/platform'
5
+
6
+ require 'chozo/core_ext'
7
+ require 'active_support/core_ext'
8
+ require 'active_model'
9
+ require 'archive/tar/minitar'
1
10
  require 'forwardable'
2
- require 'uri'
11
+ require 'hashie'
3
12
  require 'pathname'
4
- require 'tmpdir'
5
- require 'zlib'
6
- require 'archive/tar/minitar'
7
- require 'solve'
8
13
  require 'ridley'
14
+ require 'solve'
9
15
  require 'thor'
10
- require 'chef/knife'
11
- require 'chef/platform'
12
- require 'chef/cookbook/metadata'
13
- require 'chef/cookbook_version'
14
- require 'active_support/core_ext'
16
+ require 'tmpdir'
17
+ require 'uri'
18
+ require 'zlib'
15
19
 
16
20
  require 'berkshelf/version'
17
21
  require 'berkshelf/core_ext'
@@ -39,6 +43,9 @@ module Berkshelf
39
43
  autoload :Downloader, 'berkshelf/downloader'
40
44
  autoload :Uploader, 'berkshelf/uploader'
41
45
  autoload :Resolver, 'berkshelf/resolver'
46
+ autoload :Config, 'berkshelf/config'
47
+ autoload :ConfigGenerator, 'berkshelf/config_generator'
48
+ autoload :ConfigValidator, 'berkshelf/config_validator'
42
49
 
43
50
  require 'berkshelf/location'
44
51
 
@@ -9,7 +9,7 @@ module Berkshelf
9
9
  Chef::Config.from_file(path)
10
10
  ENV["CHEF_CONFIG"] = path
11
11
  else
12
- raise "Cannot continue; '#{path}' must exist and have testing credentials."
12
+ raise "Cannot continue; '#{path}' must exist and have testing credentials." unless ENV['CI']
13
13
  end
14
14
  end
15
15
  end
@@ -6,6 +6,7 @@ module Berkshelf
6
6
  class File
7
7
  def initialize(name, &block)
8
8
  @contents = []
9
+ @negative_contents = []
9
10
  @name = name
10
11
 
11
12
  if block_given?
@@ -17,6 +18,10 @@ module Berkshelf
17
18
  @contents << text
18
19
  end
19
20
 
21
+ def does_not_contain(text)
22
+ @negative_contents << text
23
+ end
24
+
20
25
  def matches?(root)
21
26
  unless root.join(@name).exist?
22
27
  throw :failure, root.join(@name)
@@ -29,6 +34,12 @@ module Berkshelf
29
34
  throw :failure, [root.join(@name), string, contents]
30
35
  end
31
36
  end
37
+
38
+ @negative_contents.each do |string|
39
+ if contents.include?(string)
40
+ throw :failure, [:not, root.join(@name), string, contents]
41
+ end
42
+ end
32
43
  end
33
44
  end
34
45
 
@@ -82,7 +93,11 @@ module Berkshelf
82
93
  class Root < Directory
83
94
  def failure_message
84
95
  if @failure.is_a?(Array) && @failure[0] == :not
85
- "Structure should not have had #{@failure[1]}, but it did"
96
+ if @failure[2]
97
+ "File #{@failure[1]} should not have contained \"#{@failure[2]}\""
98
+ else
99
+ "Structure should not have had #{@failure[1]}, but it did"
100
+ end
86
101
  elsif @failure.is_a?(Array)
87
102
  "Structure should have #{@failure[0]} with #{@failure[1]}. It had:\n#{@failure[2]}"
88
103
  else
@@ -0,0 +1,91 @@
1
+ require 'spec_helper'
2
+
3
+ describe Berkshelf::Config do
4
+ subject { config }
5
+
6
+ let(:config) { klass.new }
7
+ let(:klass) { described_class }
8
+
9
+ it { should be_valid }
10
+
11
+ its(:present?) { should be_false }
12
+
13
+ it "set and gets hash keys" do
14
+ config[:a] = 1
15
+ config[:a].should == 1
16
+ end
17
+
18
+ it "does not raise an error for nested hash keys that have not been set" do
19
+ config[:d][:e]
20
+ end
21
+
22
+ it "has indifferent access" do
23
+ config[:a] = 1
24
+ config['b'] = 2
25
+
26
+ config['a'].should == 1
27
+ config[:b].should == 2
28
+ end
29
+
30
+ describe ".file" do
31
+ subject { klass.file }
32
+
33
+ context "when the file does not exist" do
34
+ before :each do
35
+ File.stub exists?: false
36
+ end
37
+
38
+ it { should be_nil }
39
+ end
40
+ end
41
+
42
+ describe ".from_json" do
43
+ subject(:config) { klass.from_json json }
44
+
45
+ let(:json) {
46
+ <<-JSON
47
+ {
48
+ "a": 1,
49
+ "b": {
50
+ "c": 2
51
+ }
52
+ }
53
+ JSON
54
+ }
55
+
56
+ it "has data" do
57
+ config[:a].should == 1
58
+ end
59
+
60
+ it "has nested data" do
61
+ config[:b][:c].should == 2
62
+ end
63
+
64
+ it "does not raise an error for nested hash keys that have not been set" do
65
+ config[:d][:e]
66
+ end
67
+
68
+ it "has indifferent access" do
69
+ config['a'].should == 1
70
+ config[:a].should == 1
71
+ end
72
+
73
+ context "with an invalid configuration" do
74
+ let(:json) { '{ "wat": 1 }' }
75
+
76
+ it { should_not be_valid }
77
+ end
78
+ end
79
+
80
+ describe ".instance" do
81
+ subject { klass.instance }
82
+
83
+ it { should be_a klass }
84
+ end
85
+
86
+ describe ".path" do
87
+ subject { klass.path }
88
+
89
+ it { should be_a String }
90
+ end
91
+ end