Class: Chef::CookbookVersion

Inherits:
Object
  • Object
show all
Extended by:
Forwardable
Includes:
Comparable
Defined in:
lib/chef/cookbook_version.rb

Overview

Chef::CookbookVersion

CookbookVersion is a model object encapsulating the data about a Chef cookbook. Chef supports maintaining multiple versions of a cookbook on a single server; each version is represented by a distinct instance of this class.

Constant Summary collapse

COOKBOOK_SEGMENTS =
%i{resources providers recipes definitions libraries attributes files templates root_files}.freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(name, *root_paths, chef_server_rest: nil) ⇒ CookbookVersion

Creates a new Chef::CookbookVersion object.

Returns

object<Chef::CookbookVersion>

Duh. :)



94
95
96
97
98
99
100
101
102
103
104
105
# File 'lib/chef/cookbook_version.rb', line 94

def initialize(name, *root_paths, chef_server_rest: nil)
  @name = name
  @root_paths = root_paths
  @frozen = false

  @all_files = []

  @file_vendor = nil
  @cookbook_manifest = Chef::CookbookManifest.new(self)
  @metadata = Chef::Cookbook::Metadata.new
  @chef_server_rest = chef_server_rest
end

Instance Attribute Details

#all_filesObject

Returns the value of attribute all_files.



47
48
49
# File 'lib/chef/cookbook_version.rb', line 47

def all_files
  @all_files
end

#identifierObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

The ‘identifier` field is used for cookbook_artifacts, which are organized on the chef server according to their content. If the policy_mode option to CookbookManifest is set to true it will include this field in the manifest Hash and in the upload URL.

This field may be removed or have different behavior in the future, don’t use it in 3rd party code.



65
66
67
# File 'lib/chef/cookbook_version.rb', line 65

def identifier
  @identifier
end

#metadataObject

A Chef::Cookbook::Metadata object. It has a setter that fixes up the metadata to add descriptions of the recipes contained in this CookbookVersion.



55
56
57
# File 'lib/chef/cookbook_version.rb', line 55

def 
  @metadata
end

#nameObject

Returns the value of attribute name.



50
51
52
# File 'lib/chef/cookbook_version.rb', line 50

def name
  @name
end

#root_pathsObject

Returns the value of attribute root_paths.



49
50
51
# File 'lib/chef/cookbook_version.rb', line 49

def root_paths
  @root_paths
end

Class Method Details

.available_versions(cookbook_name) ⇒ Object

Given a cookbook_name, get a list of all versions that exist on the server.

Returns

[String]

Array of cookbook versions, which are strings like ‘x.y.z’

nil

if the cookbook doesn’t exist. an error will also be logged.



528
529
530
531
532
533
534
535
536
537
538
539
# File 'lib/chef/cookbook_version.rb', line 528

def self.available_versions(cookbook_name)
  chef_server_rest.get("cookbooks/#{cookbook_name}")[cookbook_name]["versions"].map do |cb|
    cb["version"]
  end
rescue Net::HTTPClientException => e
  if /^404/.match?(e.to_s)
    Chef::Log.error("Cannot find a cookbook named #{cookbook_name}")
    nil
  else
    raise
  end
end

.cacheObject



86
87
88
# File 'lib/chef/cookbook_version.rb', line 86

def self.cache
  Chef::FileCache
end

.checksum_cookbook_file(filepath) ⇒ Object

This is the one and only method that knows how cookbook files’ checksums are generated.



79
80
81
82
83
84
# File 'lib/chef/cookbook_version.rb', line 79

def self.checksum_cookbook_file(filepath)
  Chef::Digester.generate_md5_checksum_for_file(filepath)
rescue Errno::ENOENT
  Chef::Log.trace("File #{filepath} does not exist, so there is no checksum to generate")
  nil
end

.chef_server_restObject



494
495
496
# File 'lib/chef/cookbook_version.rb', line 494

def self.chef_server_rest
  Chef::ServerAPI.new(Chef::Config[:chef_server_url], { version_class: Chef::CookbookManifestVersions })
end

.from_cb_artifact_data(o) ⇒ Object



464
465
466
# File 'lib/chef/cookbook_version.rb', line 464

def self.from_cb_artifact_data(o)
  from_hash(o)
end

.from_hash(o) ⇒ Object



449
450
451
452
453
454
455
456
457
458
459
460
461
462
# File 'lib/chef/cookbook_version.rb', line 449

def self.from_hash(o)
  cookbook_version = new(o["cookbook_name"] || o["name"])

  # We want the Chef::Cookbook::Metadata object to always be inflated
  cookbook_version.manifest = o
  cookbook_version. = Chef::Cookbook::Metadata.from_hash(o["metadata"])
  cookbook_version.identifier = o["identifier"] if o.key?("identifier")

  # We don't need the following step when we decide to stop supporting deprecated operators in the metadata (e.g. <<, >>)
  cookbook_version.manifest["metadata"] = Chef::JSONCompat.from_json(Chef::JSONCompat.to_json(cookbook_version.))

  cookbook_version.freeze_version if o["frozen?"]
  cookbook_version
end

.listObject Also known as: latest_cookbooks

The API returns only a single version of each cookbook in the result from the cookbooks method



509
510
511
# File 'lib/chef/cookbook_version.rb', line 509

def self.list
  chef_server_rest.get("cookbooks")
end

.list_all_versionsObject



518
519
520
# File 'lib/chef/cookbook_version.rb', line 518

def self.list_all_versions
  chef_server_rest.get("cookbooks?num_versions=all")
end

.load(name, version = "_latest") ⇒ Object



503
504
505
506
# File 'lib/chef/cookbook_version.rb', line 503

def self.load(name, version = "_latest")
  version = "_latest" if version == "latest"
  from_hash(chef_server_rest.get("cookbooks/#{name}/#{version}"))
end

Instance Method Details

#<=>(other) ⇒ Object



541
542
543
544
545
546
547
548
# File 'lib/chef/cookbook_version.rb', line 541

def <=>(other)
  raise Chef::Exceptions::CookbookVersionNameMismatch if name != other.name

  # FIXME: can we change the interface to the Metadata class such
  # that metadata.version returns a Chef::Version instance instead
  # of a string?
  Chef::Version.new(version) <=> Chef::Version.new(other.version)
end

#attribute_filenames_by_short_filenameObject



131
132
133
134
135
136
137
138
# File 'lib/chef/cookbook_version.rb', line 131

def attribute_filenames_by_short_filename
  @attribute_filenames_by_short_filename ||= begin
    name_map = filenames_by_name(files_for("attributes"))
    root_alias = cookbook_manifest.root_files.find { |record| record[:name] == "root_files/attributes.rb" }
    name_map["default"] = root_alias[:full_path] if root_alias
    name_map
  end
end

#checksumsObject

Returns a hash of checksums to either nil or the on disk path (which is done by generate_manifest).



182
183
184
# File 'lib/chef/cookbook_version.rb', line 182

def checksums
  cookbook_manifest.checksums
end

#chef_server_restObject

REST API



490
491
492
# File 'lib/chef/cookbook_version.rb', line 490

def chef_server_rest
  @chef_server_rest ||= chef_server_rest
end

#compile_metadata(path = root_dir) ⇒ Object



554
555
556
557
558
559
560
561
562
563
564
565
# File 'lib/chef/cookbook_version.rb', line 554

def (path = root_dir)
  json_file = "#{path}/metadata.json"
  rb_file = "#{path}/metadata.rb"
  return nil if File.exist?(json_file)

  md = Chef::Cookbook::Metadata.new
  md.from_file(rb_file)
  f = File.open(json_file, "w")
  f.write(Chef::JSONCompat.to_json_pretty(md))
  f.close
  f.path
end

#cookbook_manifestObject



550
551
552
# File 'lib/chef/cookbook_version.rb', line 550

def cookbook_manifest
  @cookbook_manifest ||= CookbookManifest.new(self)
end

#destroyObject



498
499
500
501
# File 'lib/chef/cookbook_version.rb', line 498

def destroy
  chef_server_rest.delete("cookbooks/#{name}/#{version}")
  self
end

#displayObject



439
440
441
442
443
444
445
446
447
# File 'lib/chef/cookbook_version.rb', line 439

def display
  output = Mash.new
  output["cookbook_name"] = name
  output["name"] = full_name
  output["frozen?"] = frozen_version?
  output["metadata"] = .to_h
  output["version"] = version
  output.merge(cookbook_manifest.by_parent_directory)
end

#freeze_versionObject



118
119
120
# File 'lib/chef/cookbook_version.rb', line 118

def freeze_version
  @frozen = true
end

#frozen_version?Boolean

Indicates if this version is frozen or not. Freezing a cookbook version indicates that a new cookbook with the same name and version number should

Returns:

  • (Boolean)


114
115
116
# File 'lib/chef/cookbook_version.rb', line 114

def frozen_version?
  @frozen
end

#full_nameObject



127
128
129
# File 'lib/chef/cookbook_version.rb', line 127

def full_name
  "#{name}-#{version}"
end

#fully_qualified_recipe_namesObject

Return recipe names in the form of cookbook_name::recipe_name



191
192
193
194
195
196
197
198
# File 'lib/chef/cookbook_version.rb', line 191

def fully_qualified_recipe_names
  files_for("recipes").inject([]) do |memo, recipe|
    rname = recipe[:name].split("/")[1]
    rname = File.basename(rname, ".rb")
    memo << "#{name}::#{rname}"
    memo
  end
end

#has_cookbook_file_for_node?(node, cookbook_filename) ⇒ Boolean

Query whether a cookbook_file file cookbook_filename is available. File specificity for the given node is obeyed in the lookup.

Returns:

  • (Boolean)


249
250
251
# File 'lib/chef/cookbook_version.rb', line 249

def has_cookbook_file_for_node?(node, cookbook_filename)
  !!find_preferred_manifest_record(node, :files, cookbook_filename)
end

#has_metadata_file?Boolean

Returns:

  • (Boolean)


482
483
484
# File 'lib/chef/cookbook_version.rb', line 482

def 
  all_files.include?() || all_files.include?()
end

#has_template_for_node?(node, template_filename) ⇒ Boolean

Query whether a template file template_filename is available. File specificity for the given node is obeyed in the lookup.

Returns:

  • (Boolean)


243
244
245
# File 'lib/chef/cookbook_version.rb', line 243

def has_template_for_node?(node, template_filename)
  !!find_preferred_manifest_record(node, :templates, template_filename)
end

#load_recipe(recipe_name, run_context) ⇒ Object

called from DSL



201
202
203
204
205
206
207
208
209
# File 'lib/chef/cookbook_version.rb', line 201

def load_recipe(recipe_name, run_context)
  if recipe_filenames_by_name.key?(recipe_name)
    load_ruby_recipe(recipe_name, run_context)
  elsif recipe_yml_filenames_by_name.key?(recipe_name)
    load_yml_recipe(recipe_name, run_context)
  else
    raise Chef::Exceptions::RecipeNotFound, "could not find recipe #{recipe_name} for cookbook #{name}"
  end
end

#load_ruby_recipe(recipe_name, run_context) ⇒ Object



224
225
226
227
228
229
230
231
232
233
234
235
# File 'lib/chef/cookbook_version.rb', line 224

def load_ruby_recipe(recipe_name, run_context)
  Chef::Log.trace("Found recipe #{recipe_name} in cookbook #{name}")
  recipe = Chef::Recipe.new(name, recipe_name, run_context)
  recipe_filename = recipe_filenames_by_name[recipe_name]

  unless recipe_filename
    raise Chef::Exceptions::RecipeNotFound, "could not find #{recipe_name} files for cookbook #{name}"
  end

  recipe.from_file(recipe_filename)
  recipe
end

#load_yml_recipe(recipe_name, run_context) ⇒ Object



211
212
213
214
215
216
217
218
219
220
221
222
# File 'lib/chef/cookbook_version.rb', line 211

def load_yml_recipe(recipe_name, run_context)
  Chef::Log.trace("Found recipe #{recipe_name} in cookbook #{name}")
  recipe = Chef::Recipe.new(name, recipe_name, run_context)
  recipe_filename = recipe_yml_filenames_by_name[recipe_name]

  unless recipe_filename
    raise Chef::Exceptions::RecipeNotFound, "could not find #{recipe_name} files for cookbook #{name}"
  end

  recipe.from_yaml_file(recipe_filename)
  recipe
end

#manifestObject



172
173
174
# File 'lib/chef/cookbook_version.rb', line 172

def manifest
  cookbook_manifest.manifest
end

#manifest=(new_manifest) ⇒ Object



176
177
178
# File 'lib/chef/cookbook_version.rb', line 176

def manifest=(new_manifest)
  cookbook_manifest.update_from(new_manifest)
end

#manifest_records_by_pathObject



186
187
188
# File 'lib/chef/cookbook_version.rb', line 186

def manifest_records_by_path
  cookbook_manifest.manifest_records_by_path
end

#metadata_json_fileObject



468
469
470
# File 'lib/chef/cookbook_version.rb', line 468

def 
  File.join(root_paths[0], "metadata.json")
end

#metadata_rb_fileObject



472
473
474
# File 'lib/chef/cookbook_version.rb', line 472

def 
  File.join(root_paths[0], "metadata.rb")
end

#preferred_filename_on_disk_location(node, segment, filename, current_filepath = nil) ⇒ Object



305
306
307
308
309
310
311
312
# File 'lib/chef/cookbook_version.rb', line 305

def preferred_filename_on_disk_location(node, segment, filename, current_filepath = nil)
  manifest_record = preferred_manifest_record(node, segment, filename)
  if current_filepath && (manifest_record["checksum"] == self.class.checksum_cookbook_file(current_filepath))
    nil
  else
    file_vendor.get_filename(manifest_record["path"])
  end
end

#preferred_manifest_record(node, segment, filename) ⇒ Object

Determine the most specific manifest record for the given segment/filename, given information in the node. Throws FileNotFound if there is no such segment and filename in the manifest.

A manifest record is a Mash that follows the following form:

:name => "example.rb",
:path => "files/default/example.rb",
:specificity => "default",
:checksum => "1234"



265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
# File 'lib/chef/cookbook_version.rb', line 265

def preferred_manifest_record(node, segment, filename)
  found_pref = find_preferred_manifest_record(node, segment, filename)
  if found_pref
    manifest_records_by_path[found_pref]
  else
    if %i{files templates}.include?(segment)
      error_message = "Cookbook '#{name}' (#{version}) does not contain a file at any of these locations:\n"
      error_locations = if filename.is_a?(Array)
                          filename.map { |name| "  #{File.join(segment.to_s, name)}" }
                        else
                          [
                            "  #{segment}/host-#{node[:fqdn]}/#{filename}",
                            "  #{segment}/#{node[:platform]}-#{node[:platform_version]}/#{filename}",
                            "  #{segment}/#{node[:platform]}/#{filename}",
                            "  #{segment}/default/#{filename}",
                            "  #{segment}/#{filename}",
                          ]
                        end
      error_message << error_locations.join("\n")
      existing_files = segment_filenames(segment)
      # Strip the root_dir prefix off all files for readability
      pretty_existing_files = existing_files.map do |path|
        if root_dir
          path[root_dir.length + 1..-1]
        else
          path
        end
      end
      # Show the files that the cookbook does have. If the user made a typo,
      # hopefully they'll see it here.
      unless pretty_existing_files.empty?
        error_message << "\n\nThis cookbook _does_ contain: ['#{pretty_existing_files.join("','")}']"
      end
      raise Chef::Exceptions::FileNotFound, error_message
    else
      raise Chef::Exceptions::FileNotFound, "cookbook #{name} does not contain file #{segment}/#{filename}"
    end
  end
end

#preferred_manifest_records_for_directory(node, segment, dirname) ⇒ Object

Determine the manifest records from the most specific directory for the given node. See #preferred_manifest_record for a description of entries of the returned Array.



354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
# File 'lib/chef/cookbook_version.rb', line 354

def preferred_manifest_records_for_directory(node, segment, dirname)
  preferences = preferences_for_path(node, segment, dirname)
  records_by_pref = {}
  preferences.each { |pref| records_by_pref[pref] = [] }

  files_for(segment).each do |manifest_record|
    manifest_record_path = manifest_record[:path]

    # extract the preference part from the path.
    if manifest_record_path =~ %r{(#{Regexp.escape(segment.to_s)}/[^/]+/#{Regexp.escape(dirname)})/.+$}
      # Note the specificity_dirname includes the segment and
      # dirname argument as above, which is what
      # preferences_for_path returns. It could be
      # "files/ubuntu-9.10/dirname", for example.
      specificity_dirname = $1

      # Record the specificity_dirname only if it's in the list of
      # valid preferences
      if records_by_pref[specificity_dirname]
        records_by_pref[specificity_dirname] << manifest_record
      end
    end
  end

  best_pref = preferences.find { |pref| !records_by_pref[pref].empty? }

  raise Chef::Exceptions::FileNotFound, "cookbook #{name} (#{version}) has no directory #{segment}/default/#{dirname}" unless best_pref

  records_by_pref[best_pref]
end

#recipe_filenames_by_nameObject



155
156
157
158
159
160
161
162
163
164
165
# File 'lib/chef/cookbook_version.rb', line 155

def recipe_filenames_by_name
  @recipe_filenames_by_name ||= begin
    name_map = filenames_by_name(files_for("recipes"))
    root_alias = cookbook_manifest.root_files.find { |record| record[:name] == "root_files/recipe.rb" }
    if root_alias
      Chef::Log.error("Cookbook #{name} contains both recipe.rb and and recipes/default.rb, ignoring recipes/default.rb") if name_map["default"]
      name_map["default"] = root_alias[:full_path]
    end
    name_map
  end
end

#recipe_yml_filenames_by_nameObject



140
141
142
143
144
145
146
147
148
149
150
151
152
153
# File 'lib/chef/cookbook_version.rb', line 140

def recipe_yml_filenames_by_name
  @recipe_yml_filenames_by_name ||= begin
    name_map = yml_filenames_by_name(files_for("recipes"))
    root_alias = cookbook_manifest.root_files.find { |record|
      record[:name] == "root_files/recipe.yml" ||
        record[:name] == "root_files/recipe.yaml"
    }
    if root_alias
      Chef::Log.error("Cookbook #{name} contains both recipe.yml and recipes/default.yml, ignoring recipes/default.yml") if name_map["default"]
      name_map["default"] = root_alias[:full_path]
    end
    name_map
  end
end

#relative_filenames_in_preferred_directory(node, segment, dirname) ⇒ Object



314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
# File 'lib/chef/cookbook_version.rb', line 314

def relative_filenames_in_preferred_directory(node, segment, dirname)
  preferences = preferences_for_path(node, segment, dirname)
  filenames_by_pref = {}
  preferences.each { |pref| filenames_by_pref[pref] = [] }

  files_for(segment).each do |manifest_record|
    manifest_record_path = manifest_record[:path]

    # find the NON SPECIFIC filenames, but prefer them by filespecificity.
    # For example, if we have a file:
    # 'files/default/somedir/somefile.conf' we only keep
    # 'somedir/somefile.conf'. If there is also
    # 'files/$hostspecific/somedir/otherfiles' that matches the requested
    # hostname specificity, that directory will win, as it is more specific.
    #
    # This is clearly ugly b/c the use case is for remote directory, where
    # we're just going to make cookbook_files out of these and make the
    # cookbook find them by filespecificity again. but it's the shortest
    # path to "success" for now.
    if manifest_record_path =~ %r{(#{Regexp.escape(segment.to_s)}/[^/]*/?#{Regexp.escape(dirname)})/.+$}
      specificity_dirname = $1
      non_specific_path = manifest_record_path[%r{#{Regexp.escape(segment.to_s)}/[^/]*/?#{Regexp.escape(dirname)}/(.+)$}, 1]
      # Record the specificity_dirname only if it's in the list of
      # valid preferences
      if filenames_by_pref[specificity_dirname]
        filenames_by_pref[specificity_dirname] << non_specific_path
      end
    end
  end

  best_pref = preferences.find { |pref| !filenames_by_pref[pref].empty? }

  raise Chef::Exceptions::FileNotFound, "cookbook #{name} has no directory #{segment}/default/#{dirname}" unless best_pref

  filenames_by_pref[best_pref]
end

#reload_metadata!Object



476
477
478
479
480
# File 'lib/chef/cookbook_version.rb', line 476

def reload_metadata!
  if File.exist?()
    .from_json(IO.read())
  end
end

#root_dirObject

The first root path is the primary cookbook dir, from which metadata is loaded



68
69
70
# File 'lib/chef/cookbook_version.rb', line 68

def root_dir
  root_paths[0]
end

#segment_filenames(segment) ⇒ Object



237
238
239
# File 'lib/chef/cookbook_version.rb', line 237

def segment_filenames(segment)
  files_for(segment).map { |f| f["full_path"] || File.join(root_dir, f["path"]) }
end

#versionObject



107
108
109
# File 'lib/chef/cookbook_version.rb', line 107

def version
  .version
end

#version=(new_version) ⇒ Object



122
123
124
125
# File 'lib/chef/cookbook_version.rb', line 122

def version=(new_version)
  cookbook_manifest.reset!
  .version(new_version)
end