berkshelf 1.4.6 → 2.0.0.beta

Sign up to get free protection for your applications and to get access to all the features.
Files changed (96) hide show
  1. data/.gitignore +1 -0
  2. data/CHANGELOG.md +1 -5
  3. data/CONTRIBUTING.md +3 -1
  4. data/Gemfile +11 -1
  5. data/README.md +16 -0
  6. data/Thorfile +3 -1
  7. data/berkshelf.gemspec +26 -38
  8. data/features/apply_command.feature +32 -0
  9. data/features/configure_command.feature +31 -0
  10. data/features/contingent_command.feature +5 -5
  11. data/features/default_locations.feature +2 -2
  12. data/features/groups_install.feature +19 -20
  13. data/features/info_command.feature +13 -13
  14. data/features/install_command.feature +86 -83
  15. data/features/json_formatter.feature +60 -23
  16. data/features/list_command.feature +5 -11
  17. data/features/lockfile.feature +286 -6
  18. data/features/open_command.feature +8 -4
  19. data/features/outdated_command.feature +8 -15
  20. data/features/package_command.feature +39 -0
  21. data/features/show_command.feature +8 -9
  22. data/features/step_definitions/chef_server_steps.rb +20 -2
  23. data/features/step_definitions/cli_steps.rb +10 -2
  24. data/features/step_definitions/configure_cli_steps.rb +7 -0
  25. data/features/step_definitions/filesystem_steps.rb +19 -14
  26. data/features/step_definitions/json_steps.rb +22 -5
  27. data/features/step_definitions/utility_steps.rb +13 -1
  28. data/features/support/env.rb +10 -23
  29. data/features/update_command.feature +105 -24
  30. data/features/upload_command.feature +0 -14
  31. data/features/vendor_install.feature +3 -3
  32. data/generator_files/Vagrantfile.erb +11 -11
  33. data/lib/berkshelf.rb +6 -5
  34. data/lib/berkshelf/berksfile.rb +267 -99
  35. data/lib/berkshelf/cli.rb +70 -34
  36. data/lib/berkshelf/cli_commands/test_command.rb +11 -0
  37. data/lib/berkshelf/community_rest.rb +1 -1
  38. data/lib/berkshelf/config.rb +19 -2
  39. data/lib/berkshelf/cookbook_source.rb +41 -12
  40. data/lib/berkshelf/cookbook_store.rb +8 -4
  41. data/lib/berkshelf/errors.rb +61 -29
  42. data/lib/berkshelf/formatters.rb +13 -19
  43. data/lib/berkshelf/formatters/human_readable.rb +8 -0
  44. data/lib/berkshelf/formatters/json.rb +12 -1
  45. data/lib/berkshelf/formatters/null.rb +23 -0
  46. data/lib/berkshelf/init_generator.rb +22 -11
  47. data/lib/berkshelf/location.rb +8 -10
  48. data/lib/berkshelf/locations/chef_api_location.rb +4 -5
  49. data/lib/berkshelf/locations/git_location.rb +14 -12
  50. data/lib/berkshelf/locations/path_location.rb +16 -1
  51. data/lib/berkshelf/locations/site_location.rb +1 -3
  52. data/lib/berkshelf/lockfile.rb +230 -33
  53. data/lib/berkshelf/resolver.rb +2 -1
  54. data/lib/berkshelf/ui.rb +4 -8
  55. data/lib/berkshelf/version.rb +1 -1
  56. data/lib/thor/monkies/shell.rb +2 -5
  57. data/spec/fixtures/cassettes/Berkshelf_Resolver/{ClassMethods/_initialize → _initialize}/adds_the_dependencies_of_the_source_as_sources.yml +0 -0
  58. data/spec/fixtures/cookbooks/example_cookbook/.gitignore +2 -0
  59. data/spec/fixtures/cookbooks/example_cookbook/.kitchen.yml +26 -0
  60. data/spec/fixtures/cookbooks/example_cookbook/Berksfile.lock +5 -0
  61. data/spec/fixtures/lockfiles/default.lock +11 -0
  62. data/spec/{config/knife.rb → knife.rb.sample} +2 -2
  63. data/spec/spec_helper.rb +15 -3
  64. data/spec/support/chef_api.rb +19 -5
  65. data/spec/support/chef_server.rb +4 -3
  66. data/spec/support/knife.rb +18 -0
  67. data/spec/unit/berkshelf/berksfile_spec.rb +332 -235
  68. data/spec/unit/berkshelf/cached_cookbook_spec.rb +40 -42
  69. data/spec/unit/berkshelf/chef/cookbook/chefignore_spec.rb +11 -15
  70. data/spec/unit/berkshelf/community_rest_spec.rb +16 -14
  71. data/spec/unit/berkshelf/config_spec.rb +36 -20
  72. data/spec/unit/berkshelf/cookbook_generator_spec.rb +6 -10
  73. data/spec/unit/berkshelf/cookbook_source_spec.rb +244 -183
  74. data/spec/unit/berkshelf/cookbook_store_spec.rb +72 -76
  75. data/spec/unit/berkshelf/core_ext/file_utils_spec.rb +2 -2
  76. data/spec/unit/berkshelf/downloader_spec.rb +137 -157
  77. data/spec/unit/berkshelf/errors_spec.rb +1 -1
  78. data/spec/unit/berkshelf/formatters/null_spec.rb +17 -0
  79. data/spec/unit/berkshelf/formatters_spec.rb +83 -90
  80. data/spec/unit/berkshelf/git_spec.rb +219 -207
  81. data/spec/unit/berkshelf/init_generator_spec.rb +73 -73
  82. data/spec/unit/berkshelf/location_spec.rb +143 -162
  83. data/spec/unit/berkshelf/locations/chef_api_location_spec.rb +94 -89
  84. data/spec/unit/berkshelf/locations/git_location_spec.rb +75 -59
  85. data/spec/unit/berkshelf/locations/path_location_spec.rb +46 -30
  86. data/spec/unit/berkshelf/locations/site_location_spec.rb +4 -4
  87. data/spec/unit/berkshelf/lockfile_spec.rb +185 -1
  88. data/spec/unit/berkshelf/logger_spec.rb +6 -24
  89. data/spec/unit/berkshelf/mixin/logging_spec.rb +6 -8
  90. data/spec/unit/berkshelf/resolver_spec.rb +36 -38
  91. data/spec/unit/berkshelf/ui_spec.rb +9 -10
  92. data/spec/unit/berkshelf_spec.rb +41 -40
  93. data/spec/unit/chef/config_spec.rb +9 -11
  94. metadata +55 -203
  95. data/spec/config/berkshelf.pem +0 -27
  96. data/spec/config/validator.pem +0 -27
@@ -69,28 +69,22 @@ module Berkshelf
69
69
  end
70
70
  end
71
71
 
72
- def cleanup_hook
73
- # run after the task is finished
74
- end
75
-
76
- def install(cookbook, version, location)
77
- raise AbstractFunction, "#install must be implemented on #{self.class}"
78
- end
79
-
80
- def use(cookbook, version, path = nil)
81
- raise AbstractFunction, "#install must be implemented on #{self.class}"
82
- end
83
-
84
- def upload(cookbook, version, chef_server_url)
85
- raise AbstractFunction, "#upload must be implemented on #{self.class}"
72
+ class << self
73
+ private
74
+
75
+ def formatter_methods(*args)
76
+ args.each do |meth|
77
+ define_method(meth.to_sym) do |*args|
78
+ raise AbstractFunction, "##{meth} must be implemented on #{self.class}"
79
+ end unless respond_to?(meth.to_sym)
80
+ end
81
+ end
86
82
  end
87
83
 
88
- def msg(message)
89
- raise AbstractFunction, "#msg must be implemented on #{self.class}"
90
- end
84
+ formatter_methods :install, :use, :upload, :msg, :error, :package
91
85
 
92
- def error(message)
93
- raise AbstractFunction, "#error must be implemented on #{self.class}"
86
+ def cleanup_hook
87
+ # run after the task is finished
94
88
  end
95
89
 
96
90
  private
@@ -33,6 +33,14 @@ module Berkshelf
33
33
  Berkshelf.ui.info "Uploading #{cookbook} (#{version}) to: '#{chef_api_url}'"
34
34
  end
35
35
 
36
+ # Output a Cookbook package message using {Berkshelf.ui}
37
+ #
38
+ # @param [String] cookbook
39
+ # @param [String] destination
40
+ def package(cookbook, destination)
41
+ Berkshelf.ui.info "Cookbook '#{cookbook}' saved to #{destination}!"
42
+ end
43
+
36
44
  # Output a generic message using {Berkshelf.ui}
37
45
  #
38
46
  # @param [String] message
@@ -7,6 +7,8 @@ module Berkshelf
7
7
  register_formatter :json
8
8
 
9
9
  def initialize
10
+ Berkshelf.ui.mute!
11
+
10
12
  @output = {
11
13
  cookbooks: Array.new,
12
14
  errors: Array.new,
@@ -22,7 +24,7 @@ module Berkshelf
22
24
  output[:cookbooks] << details
23
25
  end
24
26
 
25
- print MultiJson.dump(output)
27
+ print ::JSON.pretty_generate(output)
26
28
  end
27
29
 
28
30
  # Add a Cookbook installation entry to delayed output
@@ -58,6 +60,15 @@ module Berkshelf
58
60
  cookbooks[cookbook][:uploaded_to] = chef_api_url
59
61
  end
60
62
 
63
+ # Add a Cookbook package entry to delayed output
64
+ #
65
+ # @param [String] cookbook
66
+ # @param [String] destination
67
+ def package(cookbook, destination)
68
+ cookbooks[cookbook] ||= {}
69
+ cookbooks[cookbook][:destination] = destination
70
+ end
71
+
61
72
  # Add a generic message entry to delayed output
62
73
  #
63
74
  # @param [String] message
@@ -0,0 +1,23 @@
1
+ module Berkshelf
2
+ module Formatters
3
+ # @author Seth Vargo <sethvargo@gmail.com>
4
+ class Null
5
+ include AbstractFormatter
6
+
7
+ register_formatter :null
8
+
9
+ # The abstract formatter dynamically defines methods that raise an
10
+ # AbstractFunction error. We need to define all of those on our class,
11
+ # otherwise they will be inherited by the Ruby object model.
12
+ AbstractFormatter.instance_methods.each do |meth|
13
+ define_method(meth) do |*args|
14
+ # intentionally do nothing
15
+ end
16
+ end
17
+
18
+ def method_missing(meth, *args, &block)
19
+ # intentionally do nothing
20
+ end
21
+ end
22
+ end
23
+ end
@@ -19,15 +19,18 @@ module Berkshelf
19
19
 
20
20
  class_option :skip_vagrant,
21
21
  type: :boolean,
22
- default: false
22
+ default: false,
23
+ desc: "Skips adding a Vagrantfile and adding supporting gems to the Gemfile"
23
24
 
24
25
  class_option :skip_git,
25
26
  type: :boolean,
26
- default: false
27
+ default: false,
28
+ desc: "Skips adding a .gitignore and running git init in the cookbook directory"
27
29
 
28
30
  class_option :foodcritic,
29
31
  type: :boolean,
30
- default: false
32
+ default: false,
33
+ desc: "Creates a Thorfile with Foodcritic support to lint test your cookbook"
31
34
 
32
35
  class_option :chef_minitest,
33
36
  type: :boolean,
@@ -35,20 +38,22 @@ module Berkshelf
35
38
 
36
39
  class_option :scmversion,
37
40
  type: :boolean,
38
- default: false
41
+ default: false,
42
+ desc: "Creates a Thorfile with SCMVersion support to manage versions for continuous integration"
39
43
 
40
44
  class_option :no_bundler,
41
45
  type: :boolean,
42
- default: false
46
+ default: false,
47
+ desc: "Skips generation of a Gemfile and other Bundler specific support"
43
48
 
44
49
  class_option :cookbook_name,
45
50
  type: :string
46
51
 
47
- class_option :berkshelf_config,
48
- type: :hash,
49
- default: Config.instance
52
+ class_option :skip_test_kitchen,
53
+ type: :boolean,
54
+ default: false,
55
+ desc: "Skip adding a testing environment to your cookbook"
50
56
 
51
- # Generate the cookbook
52
57
  def generate
53
58
  validate_configuration
54
59
  check_option_support
@@ -87,6 +92,10 @@ module Berkshelf
87
92
  template "Gemfile.erb", target.join("Gemfile")
88
93
  end
89
94
 
95
+ unless options[:skip_test_kitchen]
96
+ Kitchen::Generator::Init.new([], options).invoke_all
97
+ end
98
+
90
99
  unless options[:skip_vagrant]
91
100
  template "Vagrantfile.erb", target.join("Vagrantfile")
92
101
  ::Berkshelf::Cli.new([], berksfile: target.join("Berksfile")).invoke(:install)
@@ -95,6 +104,10 @@ module Berkshelf
95
104
 
96
105
  private
97
106
 
107
+ def berkshelf_config
108
+ Berkshelf::Config.instance
109
+ end
110
+
98
111
  # Read the cookbook name from the metadata.rb
99
112
  #
100
113
  # @return [String]
@@ -119,7 +132,6 @@ module Berkshelf
119
132
  end
120
133
  end
121
134
 
122
-
123
135
  # Check for supporting gems for provided options
124
136
  #
125
137
  # @return [Boolean]
@@ -127,7 +139,6 @@ module Berkshelf
127
139
  assert_option_supported(:foodcritic) &&
128
140
  assert_option_supported(:scmversion, 'thor-scmversion') &&
129
141
  assert_default_supported(:no_bundler, 'bundler')
130
- # Vagrant is a dependency of Berkshelf; it will always appear available to the Berkshelf process.
131
142
  end
132
143
 
133
144
  # Warn if the supporting gem for an option is not installed
@@ -145,19 +145,17 @@ module Berkshelf
145
145
  #
146
146
  # @raise [CookbookValidationFailure] if given CachedCookbook does not satisfy the constraint of the location
147
147
  #
148
+ # @todo Change MismatchedCookbookName to raise instead of warn
149
+ #
148
150
  # @return [Boolean]
149
151
  def validate_cached(cached_cookbook)
150
152
  unless version_constraint.satisfies?(cached_cookbook.version)
151
- msg = "Cookbook downloaded for '#{self.name}' from #{self} does not satisfy the version constraint"
152
- msg << " (#{self.version_constraint}). This usually happens if the Chef server contains a cookbook that"
153
- msg << " contains a metadata file with a missing or mis-matched version number."
154
- raise CookbookValidationFailure, msg
153
+ raise CookbookValidationFailure.new(self, cached_cookbook)
155
154
  end
156
155
 
157
- # JW TODO: Safe to uncomment when when Opscode makes the 'name' a required attribute in Cookbook metadata
158
- # unless self.name == cached_cookbook.cookbook_name
159
- # raise AmbiguousCookbookName, "Expected a cookbook at #{self} to be named '#{self.name}'. Did you set the 'name' attribute in your Cookbooks metadata? If you didn't, the name of the directory will be used as the name of your Cookbook (awful, right?)."
160
- # end
156
+ unless self.name == cached_cookbook.cookbook_name
157
+ Berkshelf.ui.warn(MismatchedCookbookName.new(self, cached_cookbook).to_s)
158
+ end
161
159
 
162
160
  true
163
161
  end
@@ -168,8 +166,8 @@ module Berkshelf
168
166
  }
169
167
  end
170
168
 
171
- def to_json
172
- MultiJson.dump(self.to_hash, pretty: true)
169
+ def to_json(options = {})
170
+ JSON.pretty_generate(to_hash, options)
173
171
  end
174
172
 
175
173
  private
@@ -4,7 +4,7 @@ module Berkshelf
4
4
  class << self
5
5
  # @return [Proc]
6
6
  def finalizer
7
- proc { conn.terminate if conn.alive? }
7
+ proc { conn.terminate if defined?(conn) && conn.alive? }
8
8
  end
9
9
 
10
10
  # @param [String] node_name
@@ -150,7 +150,7 @@ module Berkshelf
150
150
  # @return [Berkshelf::CachedCookbook]
151
151
  def download(destination)
152
152
  berks_path = File.join(destination, "#{name}-#{target_cookbook.version}")
153
-
153
+
154
154
  temp_path = target_cookbook.download
155
155
  FileUtils.mv(temp_path, berks_path)
156
156
 
@@ -174,14 +174,13 @@ module Berkshelf
174
174
  else
175
175
  conn.cookbook.latest_version(name)
176
176
  end
177
- rescue Ridley::Errors::HTTPNotFound,
178
- Ridley::Errors::ResourceNotFound
177
+ rescue Ridley::Errors::HTTPNotFound
179
178
  @target_cookbook = nil
180
179
  end
181
180
 
182
181
  if @target_cookbook.nil?
183
182
  msg = "Cookbook '#{name}' found at #{self}"
184
- msg << " that would satisfy constraint (#{version_constraint}" if version_constraint
183
+ msg << " that would satisfy constraint (#{version_constraint})" if version_constraint
185
184
  raise CookbookNotFound, msg
186
185
  end
187
186
 
@@ -20,10 +20,9 @@ module Berkshelf
20
20
  attr_accessor :uri
21
21
  attr_accessor :branch
22
22
  attr_accessor :rel
23
- attr_accessor :branch_name
23
+ attr_accessor :ref
24
24
  attr_reader :options
25
25
 
26
- alias_method :ref, :branch
27
26
  alias_method :tag, :branch
28
27
 
29
28
  # @param [#to_s] name
@@ -44,9 +43,9 @@ module Berkshelf
44
43
  @name = name
45
44
  @version_constraint = version_constraint
46
45
  @uri = options[:git]
47
- @branch = options[:branch] || options[:ref] || options[:tag] || "master"
46
+ @branch = options[:branch] || options[:tag] || 'master'
47
+ @ref = options[:ref]
48
48
  @rel = options[:rel]
49
- @branch_name = @branch.gsub("-", "_").gsub("/", "__") # In case the remote is specified
50
49
 
51
50
  Git.validate_uri!(@uri)
52
51
  end
@@ -55,22 +54,24 @@ module Berkshelf
55
54
  #
56
55
  # @return [Berkshelf::CachedCookbook]
57
56
  def download(destination)
58
- return local_revision(destination) if cached?(destination)
59
-
60
- ::Berkshelf::Git.checkout(clone, branch) if branch
61
- unless branch
62
- self.branch = ::Berkshelf::Git.rev_parse(clone)
57
+ if cached?(destination)
58
+ @ref = Berkshelf::Git.rev_parse(revision_path(destination))
59
+ return local_revision(destination)
63
60
  end
64
61
 
62
+ Berkshelf::Git.checkout(clone, ref || branch) if ref || branch
63
+ @ref = Berkshelf::Git.rev_parse(clone)
64
+
65
65
  tmp_path = rel ? File.join(clone, rel) : clone
66
66
  unless File.chef_cookbook?(tmp_path)
67
67
  msg = "Cookbook '#{name}' not found at git: #{uri}"
68
68
  msg << " with branch '#{branch}'" if branch
69
+ msg << " with ref '#{ref}'" if ref
69
70
  msg << " at path '#{rel}'" if rel
70
71
  raise CookbookNotFound, msg
71
72
  end
72
73
 
73
- cb_path = File.join(destination, "#{name}-#{branch_name}")
74
+ cb_path = File.join(destination, "#{name}-#{ref}")
74
75
  FileUtils.rm_rf(cb_path)
75
76
  FileUtils.mv(tmp_path, cb_path)
76
77
 
@@ -91,6 +92,7 @@ module Berkshelf
91
92
  def to_s
92
93
  s = "#{self.class.location_key}: '#{uri}'"
93
94
  s << " with branch: '#{branch}'" if branch
95
+ s << " at ref: '#{ref}'" if ref
94
96
  s
95
97
  end
96
98
 
@@ -122,8 +124,8 @@ module Berkshelf
122
124
  end
123
125
 
124
126
  def revision_path(destination)
125
- return unless branch
126
- File.join(destination, "#{name}-#{branch_name}")
127
+ return unless ref
128
+ File.join(destination, "#{name}-#{ref}")
127
129
  end
128
130
  end
129
131
  end
@@ -57,8 +57,23 @@ module Berkshelf
57
57
  super.merge(value: self.path)
58
58
  end
59
59
 
60
+ # The string representation of this PathLocation. If the path
61
+ # is the default cookbook store, just leave it out, because
62
+ # it's probably just cached.
63
+ #
64
+ # @example
65
+ # loc.to_s #=> artifact (1.4.0)
66
+ #
67
+ # @example
68
+ # loc.to_s #=> artifact (1.4.0) at path: '/Users/Seth/Dev/artifact'
69
+ #
70
+ # @return [String]
60
71
  def to_s
61
- "#{self.class.location_key}: '#{path}'"
72
+ if path.to_s.include?(Berkshelf.berkshelf_path.to_s)
73
+ "#{self.class.location_key}"
74
+ else
75
+ "#{self.class.location_key}: '#{path}'"
76
+ end
62
77
  end
63
78
  end
64
79
  end
@@ -22,9 +22,7 @@ module Berkshelf
22
22
  @name = name
23
23
  @version_constraint = version_constraint
24
24
 
25
- api_uri = if options[:site].nil?
26
- SHORTNAMES[:opscode]
27
- elsif SHORTNAMES.has_key?(options[:site])
25
+ api_uri = if options[:site].nil? || SHORTNAMES.has_key?(options[:site])
28
26
  SHORTNAMES[options[:site]]
29
27
  elsif options[:site].kind_of?(Symbol)
30
28
  raise InvalidSiteShortnameError.new(options[:site])
@@ -1,54 +1,251 @@
1
1
  module Berkshelf
2
+ # The object representation of the Berkshelf lockfile. The lockfile is useful
3
+ # when working in teams where the same cookbook versions are desired across
4
+ # multiple workstations.
5
+ #
6
+ # @author Seth Vargo <sethvargo@gmail.com>
2
7
  class Lockfile
3
- class << self
4
- def remove!
5
- FileUtils.rm_f DEFAULT_FILENAME
6
- end
8
+ # @return [Pathname]
9
+ # the path to this Lockfile
10
+ attr_reader :filepath
7
11
 
8
- # @param [Array<CookbookSource>] sources
9
- def update!(sources)
10
- contents = File.readlines(DEFAULT_FILENAME)
11
- contents.delete_if do |line|
12
- line =~ /cookbook '(#{sources.map(&:name).join('|')})'/
13
- end
12
+ # @return [Berkshelf::Berksfile]
13
+ # the Berksfile for this Lockfile
14
+ attr_reader :berksfile
14
15
 
15
- contents += sources.map { |source| definition(source) }
16
- File.open(DEFAULT_FILENAME, 'wb') { |f| f.write(contents.join("\n").squeeze("\n")) }
17
- end
16
+ # @return [String]
17
+ # the last known SHA of the Berksfile
18
+ attr_accessor :sha
18
19
 
19
- # @param [CookbookSource] source
20
- #
21
- # @return [String]
22
- def definition(source)
23
- definition = "cookbook '#{source.name}'"
20
+ # Create a new lockfile instance associated with the given Berksfile. If a
21
+ # Lockfile exists, it is automatically loaded. Otherwise, an empty instance is
22
+ # created and ready for use.
23
+ #
24
+ # @param berksfile [Berkshelf::Berksfile]
25
+ # the Berksfile associated with this Lockfile
26
+ def initialize(berksfile)
27
+ @berksfile = berksfile
28
+ @filepath = File.expand_path("#{berksfile.filepath}.lock")
29
+ @sources = {}
30
+
31
+ load! if File.exists?(@filepath)
32
+ end
24
33
 
25
- if source.location.is_a?(GitLocation)
26
- definition += ", :git => '#{source.location.uri}', :ref => '#{source.location.branch || 'HEAD'}'"
27
- elsif source.location.is_a?(PathLocation)
28
- definition += ", :path => '#{source.location.path}'"
34
+ # Load the lockfile from file system.
35
+ def load!
36
+ contents = File.read(filepath)
37
+
38
+ begin
39
+ hash = JSON.parse(contents, symbolize_names: true)
40
+ rescue JSON::ParserError
41
+ if contents =~ /^cookbook ["'](.+)["']/
42
+ Berkshelf.ui.warn "You are using the old lockfile format. Attempting to convert..."
43
+ hash = LockfileLegacy.parse(contents)
29
44
  else
30
- definition += ", :locked_version => '#{source.locked_version}'"
45
+ raise
31
46
  end
47
+ end
48
+
49
+ @sha = hash[:sha]
32
50
 
33
- definition
51
+ hash[:sources].each do |name, options|
52
+ add(CookbookSource.new(berksfile, name.to_s, options))
34
53
  end
35
54
  end
36
55
 
37
- DEFAULT_FILENAME = "#{Berkshelf::DEFAULT_FILENAME}.lock".freeze
56
+ # Set the sha value to nil to mark that the lockfile is not out of
57
+ # sync with the Berksfile.
58
+ def reset_sha!
59
+ @sha = nil
60
+ end
61
+
62
+ # The list of sources constrained in this lockfile.
63
+ #
64
+ # @return [Array<Berkshelf::CookbookSource>]
65
+ # the list of sources in this lockfile
66
+ def sources
67
+ @sources.values
68
+ end
69
+
70
+ # Find the given source in this lockfile. This method accepts a source
71
+ # attribute which may either be the name of a cookbook (String) or an
72
+ # actual cookbook source.
73
+ #
74
+ # @param [String, Berkshelf::CookbookSource] source
75
+ # the cookbook source/name to find
76
+ # @return [CookbookSource, nil]
77
+ # the cookbook source from this lockfile or nil if one was not found
78
+ def find(source)
79
+ @sources[cookbook_name(source).to_s]
80
+ end
81
+
82
+ # Determine if this lockfile contains the given source.
83
+ #
84
+ # @param [String, Berkshelf::CookbookSource] source
85
+ # the cookbook source/name to determine existence of
86
+ # @return [Boolean]
87
+ # true if the source exists, false otherwise
88
+ def has_source?(source)
89
+ !find(source).nil?
90
+ end
91
+
92
+ # Replace the current list of sources with `sources`. This method does
93
+ # not write out the lockfile - it only changes the state of the object.
94
+ #
95
+ # @param [Array<Berkshelf::CookbookSource>] sources
96
+ # the list of sources to update
97
+ # @option options [String] :sha
98
+ # the sha of the Berksfile updating the sources
99
+ def update(sources, options = {})
100
+ reset_sources!
101
+ @sha = options[:sha]
102
+
103
+ sources.each { |source| append(source) }
104
+ save
105
+ end
106
+
107
+ # Add the given source to the `sources` list, if it doesn't already exist.
108
+ #
109
+ # @param [Berkshelf::CookbookSource] source
110
+ # the source to append to the sources list
111
+ def add(source)
112
+ @sources[cookbook_name(source)] = source
113
+ end
114
+ alias_method :append, :add
115
+
116
+ # Remove the given source from this lockfile. This method accepts a source
117
+ # attribute which may either be the name of a cookbook (String) or an
118
+ # actual cookbook source.
119
+ #
120
+ # @param [String, Berkshelf::CookbookSource] source
121
+ # the cookbook source/name to remove
122
+ #
123
+ # @raise [Berkshelf::CookbookNotFound]
124
+ # if the provided source does not exist
125
+ def remove(source)
126
+ unless has_source?(source)
127
+ raise Berkshelf::CookbookNotFound, "'#{cookbook_name(source)}' does not exist in this lockfile!"
128
+ end
129
+
130
+ @sources.delete(cookbook_name(source))
131
+ end
132
+ alias_method :unlock, :remove
38
133
 
39
- attr_reader :sources
134
+ # @return [String]
135
+ # the string representation of the lockfile
136
+ def to_s
137
+ "#<Berkshelf::Lockfile #{Pathname.new(filepath).basename}>"
138
+ end
40
139
 
41
- def initialize(sources)
42
- @sources = Array(sources)
140
+ # @return [String]
141
+ # the detailed string representation of the lockfile
142
+ def inspect
143
+ "#<Berkshelf::Lockfile #{Pathname.new(filepath).basename}, sources: #{sources.inspect}>"
43
144
  end
44
145
 
45
- def write(filename = DEFAULT_FILENAME)
46
- content = sources.map { |source| self.class.definition(source) }.join("\n")
47
- File.open(filename, "wb") { |f| f.write content }
146
+ # Write the current lockfile to a hash
147
+ #
148
+ # @return [Hash]
149
+ # the hash representation of this lockfile
150
+ # * :sha [String] the last-known sha for the berksfile
151
+ # * :sources [Array<Berkshelf::CookbookSource>] the list of sources
152
+ def to_hash
153
+ {
154
+ sha: sha,
155
+ sources: @sources
156
+ }
48
157
  end
49
158
 
50
- def remove!
51
- self.class.remove!
159
+ # The JSON representation of this lockfile
160
+ #
161
+ # Relies on {#to_hash} to generate the json
162
+ #
163
+ # @return [String]
164
+ # the JSON representation of this lockfile
165
+ def to_json(options = {})
166
+ JSON.pretty_generate(to_hash, options)
52
167
  end
168
+
169
+ private
170
+
171
+ # Save the contents of the lockfile to disk.
172
+ def save
173
+ File.open(filepath, 'w') do |file|
174
+ file.write to_json + "\n"
175
+ end
176
+ end
177
+
178
+ # Clear the sources array
179
+ def reset_sources!
180
+ @sources = {}
181
+ end
182
+
183
+ # Return the name of this cookbook (because it's the key in our
184
+ # table).
185
+ #
186
+ # @param [Berkshelf::CookbookSource, #to_s] source
187
+ # the source to find the name from
188
+ #
189
+ # @return [String]
190
+ # the name of the cookbook
191
+ def cookbook_name(source)
192
+ source.is_a?(CookbookSource) ? source.name : source.to_s
193
+ end
194
+
195
+ # Legacy support for old lockfiles
196
+ #
197
+ # @author Seth Vargo <sethvargo@gmail.com>
198
+ # @todo Remove this class in the next major release.
199
+ class LockfileLegacy
200
+ class << self
201
+ # Read the old lockfile content and instance eval in context.
202
+ #
203
+ # @param [String] content
204
+ # the string content read from a legacy lockfile
205
+ def parse(content)
206
+ sources = {}.tap do |hash|
207
+ content.split("\n").each do |line|
208
+ next if line.empty?
209
+
210
+ source = self.new(line)
211
+ hash[source.name] = source.options
212
+ end
213
+ end
214
+
215
+ {
216
+ sha: nil,
217
+ sources: sources
218
+ }
219
+ end
220
+ end
221
+
222
+ # @return [Hash]
223
+ # the hash of options
224
+ attr_reader :options
225
+
226
+ # @return [String]
227
+ # the name of this cookbook
228
+ attr_reader :name
229
+
230
+ # Create a new legacy lockfile for processing
231
+ #
232
+ # @param [String] content
233
+ # the content to parse out and convert to a hash
234
+ def initialize(content)
235
+ instance_eval(content).to_hash
236
+ end
237
+
238
+ # Method defined in legacy lockfiles (since we are using
239
+ # instance_eval).
240
+ #
241
+ # @param [String] name
242
+ # the name of this cookbook
243
+ # @option options [String] :locked_version
244
+ # the locked version of this cookbook
245
+ def cookbook(name, options = {})
246
+ @name = name
247
+ @options = options
248
+ end
249
+ end
53
250
  end
54
251
  end