berkshelf 0.6.0.beta2 → 0.6.0.beta3

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