astratto/kitchen-inspector

View on GitHub
lib/kitchen-inspector/inspector/health_bureau.rb

Summary

Maintainability
B
4 hrs
Test Coverage
#
# Copyright (c) 2013 Stefano Tortarolo <stefano.tortarolo@gmail.com>
#
# MIT License
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#

module KitchenInspector
  module Inspector
    # Main class that, starting from a cookbook, analyzes its dependencies
    # using its "inspectors" and returns a collection of analyzed dependencies
    class HealthBureau
      include Utils

      attr_reader :chef_inspector, :repo_inspector

      def initialize(config)
        configuration = read_config(config)

        begin
          self.instance_eval configuration
        rescue NoMethodError => e
          raise ConfigurationError, "Unsupported configuration: #{e.name}."
        end

        raise ConfigurationError, "Chef Server is not configured properly, " \
                                  "please check your 'chef_server' configuration." unless validate_chef_inspector

        raise ConfigurationError, "Repository Manager is not configured properly, " \
                                  "please check your 'repository_manager' configuration." unless @repo_inspector
      end

      # Inspect your kitchen!
      #
      # If recursive is specified, dependencies' metadata are downloaded and recursively analyzed
      #
      # @param path [String] path to the cookbook to be analyzed
      # @param recursive [Boolean] whether transitive dependencies should be analyzed
      # @return [Array<Dependency>] analyzed dependency and its transitive dependencies
      def investigate(path, recursive=true)
        raise NotACookbookError, 'Path is not a cookbook' unless File.exists?(File.join(path, 'metadata.rb'))

        metadata = Ridley::Chef::Cookbook::Metadata.from_file(File.join(path, 'metadata.rb'))
        metadata.dependencies.collect do |name, version|
          analyze_dependency(Models::Dependency.new(name, version), recursive)
        end.flatten
      end

      # Analyze Chef repo and Repository manager in order to find more information
      # about a given dependency
      #
      # @param dependency [Dependency] dependency to be analyzed
      # @param recursive [Boolean] whether transitive dependencies should be analyzed
      # @return [Array<Dependency>] analyzed dependency and its transitive dependencies
      def analyze_dependency(dependency, recursive)
        chef_info = @chef_inspector.investigate(dependency)

        # Grab information from the Repository Manager
        info_repo = @repo_inspector.investigate(dependency, chef_info[:version_used], recursive)
        deps = info_repo.collect do |dep, repo_info|
          dep_chef_info = @chef_inspector.investigate(dep)
          update_dependency(dep, dep_chef_info, repo_info)
          dep
        end
        deps
      end

      # Update in-place a dependency based on information retrieved from
      # Chef Server and Repository Manager
      #
      # @param dependency [Dependency] dependency to be updated
      # @param chef_info [Hash] information from Chef Server
      # @param repo_info [Hash] information from Repository Manager
      def update_dependency(dependency, chef_info, repo_info)
        dependency.status = :up_to_date

        if !chef_info[:version_used]
          dependency.status = :err_req
          msg = 'No versions found'
          reference_version = @repo_inspector.get_reference_version(nil, repo_info)
          msg << ", using #{reference_version} for recursive analysis" if reference_version

          dependency.remarks << msg
        else
          relaxed_version = satisfy("~> #{chef_info[:version_used]}", chef_info[:versions])
          if relaxed_version != chef_info[:version_used]
            dependency.status = :warn_req
            changelog_url = @repo_inspector.get_changelog(repo_info,
                                          chef_info[:version_used],
                                          relaxed_version)
            dependency.remarks << "#{relaxed_version} is available. #{changelog_url}"
          end
        end

        # Compare Chef and Repository Manager versions
        comparison = compare_repo_chef(chef_info, repo_info)
        chef_info[:status] = comparison[:chef]
        repo_info[:status] = comparison[:repo]
        dependency.remarks.push(*comparison[:remarks]) if comparison[:remarks]

        if repo_info[:not_unique]
          repo_info[:status] = :warn_notunique_repo
          dependency.remarks << "Not unique on #{@repo_inspector.manager.type} (this is #{repo_info[:source_url]})"
        end

        # Check whether latest tag and metadata version in Repository Manager are
        # consistent
        unless @repo_inspector.consistent_version?(repo_info)
          repo_info[:status] = :warn_mismatch_repo
          dependency.remarks << "#{@repo_inspector.manager.type}'s last tag is #{repo_info[:latest_tag]} " \
                                  "but found #{repo_info[:latest_metadata]} in metadata.rb"
        end

        dependency.repomanager = repo_info
        dependency.chef = chef_info
      end

      # Compare Repository Manager and Chef Server
      #
      # @param chef_info [Hash] information from Chef Server
      # @param repo_info [Hash] information from Repository Manager
      # @return [Hash] containing servers statuses and remarks
      def compare_repo_chef(chef_info, repo_info)
        comparison = {:chef => :up_to_date, :repo => :up_to_date,
                  :remarks => []}

        if chef_info[:latest_version] && repo_info[:latest_metadata]
          if chef_info[:latest_version] > repo_info[:latest_metadata]
            comparison[:repo] = :warn_outofdate_repo
            changelog_url = @repo_inspector.get_changelog(repo_info,
                                          repo_info[:latest_metadata].to_s,
                                          chef_info[:latest_version].to_s)
            comparison[:remarks] << "#{@repo_inspector.manager.type} out-of-date! #{changelog_url}"
            return comparison
          elsif chef_info[:latest_version] < repo_info[:latest_metadata]
            comparison[:chef] = :warn_chef
            changelog_url = @repo_inspector.get_changelog(repo_info,
                                          chef_info[:latest_version].to_s,
                                          repo_info[:latest_metadata].to_s)
            comparison[:remarks] << "A new version might appear on Chef server. #{changelog_url}"
            return comparison
          end
        end

        unless repo_info[:latest_metadata]
          comparison[:repo] = :err_repo
          comparison[:remarks] << "#{@repo_inspector.manager.type} doesn't contain any versions."
        end

        unless chef_info[:latest_version]
          comparison[:chef] = :err_chef
          comparison[:remarks] << "Chef Server doesn't contain any versions."
        end

        comparison
      end

      # Initialize the Chef Server configuration
      def chef_server(config)
        @chef_inspector = ChefInspector.new config
      end

      # Initialize the Repository Manager
      def repository_manager(config)
        @repo_inspector = RepositoryInspector.new config
      end

      # Initialize a Chef Inspector using knife.rb settings if not already
      # initialized
      #
      # @return [ChefInspector]
      def validate_chef_inspector
        @chef_inspector ||= begin
          # Fallback to knife.rb if possible
          knife_cfg = "#{Dir.home}/.chef/knife.rb"
          if File.exists?(knife_cfg)
            Chef::Config.from_file knife_cfg
            chef_server({ :username => Chef::Config.node_name,
                          :url => Chef::Config.chef_server_url,
                          :client_pem => Chef::Config.client_key
                        })
          end
        end
      end
    end
  end
end