Skip to content

Commit

Permalink
Merge pull request #4081 from chef/lcg/chef-version
Browse files Browse the repository at this point in the history
RFC-037:  add chef_version and ohai_version metadata
  • Loading branch information
lamont-granquist committed Oct 28, 2015
2 parents cc51b2f + a75ce7c commit 6a306c0
Show file tree
Hide file tree
Showing 10 changed files with 353 additions and 14 deletions.
15 changes: 14 additions & 1 deletion lib/chef/cookbook/cookbook_collection.rb
@@ -1,7 +1,7 @@
#--
# Author:: Tim Hinderliter (<tim@opscode.com>)
# Author:: Christopher Walters (<cw@opscode.com>)
# Copyright:: Copyright (c) 2010 Opscode, Inc.
# Copyright:: Copyright (c) 2010-2015 Chef Software, Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
Expand Down Expand Up @@ -41,5 +41,18 @@ def initialize(cookbook_versions={})
cookbook_versions.each{ |cookbook_name, cookbook_version| self[cookbook_name] = cookbook_version }
end

# Validates that the cookbook metadata allows it to run on this instance.
#
# Currently checks chef_version and ohai_version in the cookbook metadata
# against the running Chef::VERSION and Ohai::VERSION.
#
# @raises [Chef::Exceptions::CookbookChefVersionMismatch] if the Chef::VERSION fails validation
# @raises [Chef::Exceptions::CookbookOhaiVersionMismatch] if the Ohai::VERSION fails validation
def validate!
each do |cookbook_name, cookbook_version|
cookbook_version.metadata.validate_chef_version!
cookbook_version.metadata.validate_ohai_version!
end
end
end
end
124 changes: 115 additions & 9 deletions lib/chef/cookbook/metadata.rb
Expand Up @@ -2,7 +2,7 @@
# Author:: Adam Jacob (<adam@opscode.com>)
# Author:: AJ Christensen (<aj@opscode.com>)
# Author:: Seth Falcon (<seth@opscode.com>)
# Copyright:: Copyright 2008-2010 Opscode, Inc.
# Copyright:: Copyright 2008-2015 Chef Software, Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
Expand Down Expand Up @@ -55,19 +55,23 @@ class Metadata
SOURCE_URL = 'source_url'.freeze
ISSUES_URL = 'issues_url'.freeze
PRIVACY = 'privacy'.freeze
CHEF_VERSIONS = 'chef_versions'.freeze
OHAI_VERSIONS = 'ohai_versions'.freeze

COMPARISON_FIELDS = [ :name, :description, :long_description, :maintainer,
:maintainer_email, :license, :platforms, :dependencies,
:recommendations, :suggestions, :conflicting, :providing,
:replacing, :attributes, :groupings, :recipes, :version,
:source_url, :issues_url, :privacy ]
:source_url, :issues_url, :privacy, :chef_versions, :ohai_versions ]

VERSION_CONSTRAINTS = {:depends => DEPENDENCIES,
:recommends => RECOMMENDATIONS,
:suggests => SUGGESTIONS,
:conflicts => CONFLICTING,
:provides => PROVIDING,
:replaces => REPLACING }
VERSION_CONSTRAINTS = {:depends => DEPENDENCIES,
:recommends => RECOMMENDATIONS,
:suggests => SUGGESTIONS,
:conflicts => CONFLICTING,
:provides => PROVIDING,
:replaces => REPLACING,
:chef_version => CHEF_VERSIONS,
:ohai_version => OHAI_VERSIONS }

include Chef::Mixin::ParamsValidate
include Chef::Mixin::FromFile
Expand All @@ -84,6 +88,11 @@ class Metadata
attr_reader :recipes
attr_reader :version

# @return [Array<Gem::Dependency>] Array of supported Chef versions
attr_reader :chef_versions
# @return [Array<Gem::Dependency>] Array of supported Ohai versions
attr_reader :ohai_versions

# Builds a new Chef::Cookbook::Metadata object.
#
# === Parameters
Expand Down Expand Up @@ -118,6 +127,8 @@ def initialize
@source_url = ''
@issues_url = ''
@privacy = false
@chef_versions = []
@ohai_versions = []

@errors = []
end
Expand Down Expand Up @@ -386,6 +397,28 @@ def replaces(cookbook, *version_args)
@replacing[cookbook]
end

# Metadata DSL to set a valid chef_version. May be declared multiple times
# with the result being 'OR'd such that if any statements match, the version
# is considered supported. Uses Gem::Requirement for its implementation.
#
# @param version_args [Array<String>] Version constraint in String form
# @return [Array<Gem::Dependency>] Current chef_versions array
def chef_version(*version_args)
@chef_versions << Gem::Dependency.new('chef', *version_args) unless version_args.empty?
@chef_versions
end

# Metadata DSL to set a valid ohai_version. May be declared multiple times
# with the result being 'OR'd such that if any statements match, the version
# is considered supported. Uses Gem::Requirement for its implementation.
#
# @param version_args [Array<String>] Version constraint in String form
# @return [Array<Gem::Dependency>] Current ohai_versions array
def ohai_version(*version_args)
@ohai_versions << Gem::Dependency.new('ohai', *version_args) unless version_args.empty?
@ohai_versions
end

# Adds a description for a recipe.
#
# === Parameters
Expand Down Expand Up @@ -481,6 +514,40 @@ def grouping(name, options)
@groupings[name]
end

# Convert an Array of Gem::Dependency objects (chef_version/ohai_version) to an Array.
#
# Gem::Dependencey#to_s is not useful, and there is no #to_json defined on it or its component
# objets, so we have to write our own rendering method.
#
# [ Gem::Dependency.new(">= 12.5"), Gem::Dependency.new(">= 11.18.0", "< 12.0") ]
#
# results in:
#
# [ [ ">= 12.5" ], [ ">= 11.18.0", "< 12.0" ] ]
#
# @param deps [Array<Gem::Dependency>] Multiple Gem-style version constraints
# @return [Array<Array<String>]] Simple object representation of version constraints (for json)
def gem_requirements_to_array(*deps)
deps.map do |dep|
dep.requirement.requirements.map do |op, version|
"#{op} #{version}"
end.sort
end
end

# Convert an Array of Gem::Dependency objects (chef_version/ohai_version) to a hash.
#
# This is the inverse of #gem_requirements_to_array
#
# @param what [String] What version constraint we are constructing ('chef' or 'ohai' presently)
# @param array [Array<Array<String>]] Simple object representation of version constraints (from json)
# @return [Array<Gem::Dependency>] Multiple Gem-style version constraints
def gem_requirements_from_array(what, array)
array.map do |dep|
Gem::Dependency.new(what, *dep)
end
end

def to_hash
{
NAME => self.name,
Expand All @@ -502,7 +569,9 @@ def to_hash
VERSION => self.version,
SOURCE_URL => self.source_url,
ISSUES_URL => self.issues_url,
PRIVACY => self.privacy
PRIVACY => self.privacy,
CHEF_VERSIONS => gem_requirements_to_array(*self.chef_versions),
OHAI_VERSIONS => gem_requirements_to_array(*self.ohai_versions)
}
end

Expand Down Expand Up @@ -537,6 +606,8 @@ def from_hash(o)
@source_url = o[SOURCE_URL] if o.has_key?(SOURCE_URL)
@issues_url = o[ISSUES_URL] if o.has_key?(ISSUES_URL)
@privacy = o[PRIVACY] if o.has_key?(PRIVACY)
@chef_versions = gem_requirements_from_array("chef", o[CHEF_VERSIONS]) if o.has_key?(CHEF_VERSIONS)
@ohai_versions = gem_requirements_from_array("ohai", o[OHAI_VERSIONS]) if o.has_key?(OHAI_VERSIONS)
self
end

Expand Down Expand Up @@ -612,8 +683,43 @@ def privacy(arg=nil)
)
end

# Validates that the Ohai::VERSION of the running chef-client matches one of the
# configured ohai_version statements in this cookbooks metadata.
#
# @raises [Chef::Exceptions::CookbookOhaiVersionMismatch] if the cookbook fails validation
def validate_ohai_version!
unless gem_dep_matches?("ohai", Gem::Version.new(Ohai::VERSION), *ohai_versions)
raise Exceptions::CookbookOhaiVersionMismatch.new(Ohai::VERSION, name, version, *ohai_versions)
end
end

# Validates that the Chef::VERSION of the running chef-client matches one of the
# configured chef_version statements in this cookbooks metadata.
#
# @raises [Chef::Exceptions::CookbookChefVersionMismatch] if the cookbook fails validation
def validate_chef_version!
unless gem_dep_matches?("chef", Gem::Version.new(Chef::VERSION), *chef_versions)
raise Exceptions::CookbookChefVersionMismatch.new(Chef::VERSION, name, version, *chef_versions)
end
end

private

# Helper to match a gem style version (ohai_version/chef_version) against a set of
# Gem::Dependency version constraints. If none are present, it always matches. if
# multiple are present, one must match. Returns false if none matches.
#
# @param what [String] the name of the constraint (e.g. 'chef' or 'ohai')
# @param version [String] the version to compare against the constraints
# @param deps [Array<Gem::Dependency>] Multiple Gem-style version constraints
# @return [Boolean] true if no constraints or a match, false if no match
def gem_dep_matches?(what, version, *deps)
# always match if we have no chef_version at all
return true unless deps.length > 0
# match if we match any of the chef_version lines
deps.any? { |dep| dep.match?(what, version) }
end

def run_validation
if name.nil?
@errors = ["The `name' attribute is required in cookbook metadata"]
Expand Down
16 changes: 15 additions & 1 deletion lib/chef/exceptions.rb
Expand Up @@ -2,7 +2,7 @@
# Author:: Adam Jacob (<adam@opscode.com>)
# Author:: Seth Falcon (<seth@opscode.com>)
# Author:: Kyle Goodwin (<kgoodwin@primerevenue.com>)
# Copyright:: Copyright 2008-2010 Opscode, Inc.
# Copyright:: Copyright 2008-2015 Chef Software, Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
Expand Down Expand Up @@ -482,6 +482,20 @@ def initialize
end
end

class CookbookChefVersionMismatch < RuntimeError
def initialize(chef_version, cookbook_name, cookbook_version, *constraints)
constraint_str = constraints.map { |c| c.requirement.as_list.to_s }.join(', ')
super "Cookbook '#{cookbook_name}' version '#{cookbook_version}' depends on chef version #{constraint_str}, but the running chef version is #{chef_version}"
end
end

class CookbookOhaiVersionMismatch < RuntimeError
def initialize(ohai_version, cookbook_name, cookbook_version, *constraints)
constraint_str = constraints.map { |c| c.requirement.as_list.to_s }.join(', ')
super "Cookbook '#{cookbook_name}' version '#{cookbook_version}' depends on ohai version #{constraint_str}, but the running ohai version is #{ohai_version}"
end
end

class MultipleDscResourcesFound < RuntimeError
attr_reader :resources_found
def initialize(resources_found)
Expand Down
2 changes: 2 additions & 0 deletions lib/chef/policy_builder/expand_node_object.rb
Expand Up @@ -74,11 +74,13 @@ def setup_run_context(specific_recipes=nil)
cl = Chef::CookbookLoader.new(Chef::Config[:cookbook_path])
cl.load_cookbooks
cookbook_collection = Chef::CookbookCollection.new(cl)
cookbook_collection.validate!
run_context = Chef::RunContext.new(node, cookbook_collection, @events)
else
Chef::Cookbook::FileVendor.fetch_from_remote(api_service)
cookbook_hash = sync_cookbooks
cookbook_collection = Chef::CookbookCollection.new(cookbook_hash)
cookbook_collection.validate!
run_context = Chef::RunContext.new(node, cookbook_collection, @events)
end

Expand Down
1 change: 1 addition & 0 deletions lib/chef/policy_builder/policyfile.rb
Expand Up @@ -153,6 +153,7 @@ def setup_run_context(specific_recipes=nil)
Chef::Cookbook::FileVendor.fetch_from_remote(http_api)
sync_cookbooks
cookbook_collection = Chef::CookbookCollection.new(cookbooks_to_sync)
cookbook_collection.validate!
run_context = Chef::RunContext.new(node, cookbook_collection, events)

setup_chef_class(run_context)
Expand Down
20 changes: 20 additions & 0 deletions spec/integration/client/client_spec.rb
Expand Up @@ -320,6 +320,26 @@ class ::Blah
end
end

when_the_repository "has a cookbook that should fail chef_version checks" do
before do
file 'cookbooks/x/recipes/default.rb', ''
file 'cookbooks/x/metadata.rb', <<EOM
name 'x'
version '0.0.1'
chef_version '~> 999.99'
EOM
file 'config/client.rb', <<EOM
local_mode true
cookbook_path "#{path_to('cookbooks')}"
EOM
end
it "should fail the chef client run" do
command = shell_out("#{chef_client} -c \"#{path_to('config/client.rb')}\" -o 'x::default' --no-fork", :cwd => chef_dir)
expect(command.exitstatus).to eql(1)
expect(command.stdout).to match(/Chef::Exceptions::CookbookChefVersionMismatch/)
end
end

when_the_repository "has a cookbook that generates deprecation warnings" do
before do
file 'cookbooks/x/recipes/default.rb', <<-EOM
Expand Down
29 changes: 28 additions & 1 deletion spec/integration/knife/upload_spec.rb
@@ -1,6 +1,6 @@
#
# Author:: John Keiser (<jkeiser@opscode.com>)
# Copyright:: Copyright (c) 2013 Opscode, Inc.
# Copyright:: Copyright (c) 2013-2015 Chef Software, Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
Expand Down Expand Up @@ -699,6 +699,19 @@
end
end
end
when_the_chef_server "is empty" do
when_the_repository 'has a cookbook with an invalid chef_version constraint in it' do
before do
file 'cookbooks/x/metadata.rb', cb_metadata('x', '1.0.0', "\nchef_version '~> 999.0'")
end
it 'knife upload succeeds' do
knife('upload /cookbooks/x').should_succeed <<EOM
Created /cookbooks/x
EOM
knife('diff --name-status /cookbooks').should_succeed ''
end
end
end
end # without versioned cookbooks

with_versioned_cookbooks do
Expand Down Expand Up @@ -1219,6 +1232,20 @@
end
end
end

when_the_chef_server "is empty" do
when_the_repository 'has a cookbook with an invalid chef_version constraint in it' do
before do
file 'cookbooks/x-1.0.0/metadata.rb', cb_metadata('x', '1.0.0', "\nchef_version '~> 999.0'")
end
it 'knife upload succeeds' do
knife('upload /cookbooks/x-1.0.0').should_succeed <<EOM
Created /cookbooks/x-1.0.0
EOM
knife('diff --name-status /cookbooks').should_succeed ''
end
end
end
end # with versioned cookbooks

when_the_chef_server 'has a user' do
Expand Down
34 changes: 34 additions & 0 deletions spec/integration/solo/solo_spec.rb
Expand Up @@ -70,6 +70,40 @@
end
end

when_the_repository "has a cookbook with an incompatible chef_version" do
before do
file 'cookbooks/x/metadata.rb', cb_metadata('x', '1.0.0', "\nchef_version '~> 999.0'")
file 'cookbooks/x/recipes/default.rb', 'puts "ITWORKS"'
file 'config/solo.rb', <<EOM
cookbook_path "#{path_to('cookbooks')}"
file_cache_path "#{path_to('config/cache')}"
EOM
end

it "should exit with an error" do
result = shell_out("#{chef_solo} -c \"#{path_to('config/solo.rb')}\" -o 'x::default' -l debug", :cwd => chef_dir)
expect(result.exitstatus).to eq(1)
expect(result.stdout).to include("Chef::Exceptions::CookbookChefVersionMismatch")
end
end

when_the_repository "has a cookbook with an incompatible ohai_version" do
before do
file 'cookbooks/x/metadata.rb', cb_metadata('x', '1.0.0', "\nohai_version '~> 999.0'")
file 'cookbooks/x/recipes/default.rb', 'puts "ITWORKS"'
file 'config/solo.rb', <<EOM
cookbook_path "#{path_to('cookbooks')}"
file_cache_path "#{path_to('config/cache')}"
EOM
end

it "should exit with an error" do
result = shell_out("#{chef_solo} -c \"#{path_to('config/solo.rb')}\" -o 'x::default' -l debug", :cwd => chef_dir)
expect(result.exitstatus).to eq(1)
expect(result.stdout).to include("Chef::Exceptions::CookbookOhaiVersionMismatch")
end
end


when_the_repository "has a cookbook with a recipe with sleep" do
before do
Expand Down

0 comments on commit 6a306c0

Please sign in to comment.