# # Copyright:: Chef Software Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # require_relative "base" require_relative "../ui" require_relative "../pager" require_relative "../policyfile/differ" require_relative "../policyfile/comparison_base" require_relative "../policyfile/storage_config" require_relative "../configurable" require_relative "../dist" require "chef/server_api" module ChefCLI module Command class Diff < Base include Configurable include Policyfile::StorageConfigDelegation banner(<<~BANNER) Usage: #{ChefCLI::Dist::EXEC} diff [POLICYFILE] [--head | --git GIT_REF | POLICY_GROUP | POLICY_GROUP...POLICY_GROUP ] `#{ChefCLI::Dist::EXEC} diff` displays an itemized diff comparing two revisions of a Policyfile lock. When the `--git` option is given, `#{ChefCLI::Dist::EXEC} diff` either compares a given git reference against the current lockfile revision on disk or compares between two git references. Examples: * `#{ChefCLI::Dist::EXEC} diff --git HEAD`: compares the current lock with the latest commit on the current branch. * `#{ChefCLI::Dist::EXEC} diff --git master`: compares the current lock with the latest commit to master. * `#{ChefCLI::Dist::EXEC} diff --git v1.0.0`: compares the current lock with the revision as of the `v1.0.0` tag. * `#{ChefCLI::Dist::EXEC} diff --git master...dev-branch`: compares the Policyfile lock on master with the revision on the `dev-branch` branch. * `#{ChefCLI::Dist::EXEC} diff --git v1.0.0...master`: compares the Policyfile lock at the `v1.0.0` tag with the lastest revision on the master branch. `#{ChefCLI::Dist::EXEC} diff --head` is a shortcut for `#{ChefCLI::Dist::EXEC} diff --git HEAD`. When no git-specific flag is given, `#{ChefCLI::Dist::EXEC} diff` either compares the current lockfile revision on disk to one on the #{ChefCLI::Dist::SERVER_PRODUCT} or compares two lockfiles on the #{ChefCLI::Dist::SERVER_PRODUCT}. Lockfiles on the #{ChefCLI::Dist::SERVER_PRODUCT} are specified by Policy Group. Examples: * `#{ChefCLI::Dist::EXEC} diff staging`: compares the current lock with the one currently assigned to the `staging` Policy Group. * `#{ChefCLI::Dist::EXEC} diff production...staging`: compares the lock currently assigned to the `production` Policy Group to the lock currently assigned to the `staging` Policy Group. Options: BANNER option :git, short: "-g GIT_REF", long: "--git GIT_REF", description: "Compare local lock against GIT_REF, or between two git commits." option :head, long: "--head", description: "Compare local lock against last git commit.", boolean: true option :pager, long: "--[no-]pager", description: "Enable/disable paged diff ouput (default: enabled).", default: true, boolean: true option :config_file, short: "-c CONFIG_FILE", long: "--config CONFIG_FILE", description: "Path to configuration file." option :debug, short: "-D", long: "--debug", description: "Enable stacktraces and other debug output.", default: false attr_accessor :ui attr_reader :old_base attr_reader :new_base attr_reader :storage_config def initialize(*args) super @ui = UI.new @old_base = nil @new_base = nil @policyfile_relative_path = nil @storage_config = nil @http_client = nil @old_lock = nil @new_lock = nil end def debug? !!config[:debug] end def run(params = []) return 1 unless apply_params!(params) print_diff 0 rescue PolicyfileServiceError => e handle_error(e) 1 end def handle_error(error) ui.err("Error: #{error.message}") if error.respond_to?(:reason) ui.err("Reason: #{error.reason}") ui.err("") ui.err(error.extended_error_info) if debug? ui.err(error.cause.backtrace.join("\n")) if debug? end end def print_diff # eagerly evaluate locks so we hit any errors before we've entered # pagerland. Also, git commands behave weirdly when run while the pager # is active, doing this eagerly also avoids that issue materialize_locks Pager.new(enable_pager: config[:pager]).with_pager do |pager| differ = differ(pager.ui) differ.run_report end end def differ(ui = self.ui) Policyfile::Differ.new(old_name: old_base.name, old_lock: old_lock, new_name: new_base.name, new_lock: new_lock, ui: ui) end def http_client @http_client ||= Chef::ServerAPI.new(chef_config.chef_server_url, signing_key_filename: chef_config.client_key, client_name: chef_config.node_name) end def old_lock materialize_locks unless @old_lock @old_lock end def new_lock materialize_locks unless @new_lock @new_lock end def policy_name local_lock["name"] end def local_lock @local_lock ||= local_lock_comparison_base.lock end # ComparisonBase for the local lockfile. This is used to get the # policy_name which is needed to query the server for the lockfile of a # particular policy_group. def local_lock_comparison_base Policyfile::ComparisonBase::Local.new(policyfile_lock_relpath) end def policyfile_lock_relpath storage_config.policyfile_lock_filename end def apply_params!(params) remaining_args = parse_options(params) if no_comparison_specified?(remaining_args) ui.err("No comparison specified") ui.err("") ui.err(opt_parser) false elsif conflicting_args_and_opts_given?(remaining_args) ui.err("Conflicting arguments and options: git and Policy Group comparisons cannot be mixed") ui.err("") ui.err(opt_parser) false elsif conflicting_git_options_given? ui.err("Conflicting git options: --head and --git are exclusive") ui.err("") ui.err(opt_parser) false elsif config[:head] set_policyfile_path_from_args(remaining_args) @old_base = Policyfile::ComparisonBase::Git.new("HEAD", policyfile_lock_relpath) @new_base = Policyfile::ComparisonBase::Local.new(policyfile_lock_relpath) true elsif config[:git] set_policyfile_path_from_args(remaining_args) parse_git_comparison(config[:git]) else set_policyfile_path_from_args(remaining_args) parse_server_comparison(remaining_args) end end def parse_server_comparison(args) comparison_string = args.last if comparison_string.include?("...") old_pgroup, new_pgroup, *extra = comparison_string.split("...") @old_base, @new_base = [old_pgroup, new_pgroup].map do |g| Policyfile::ComparisonBase::PolicyGroup.new(g, policy_name, http_client) end unless extra.empty? ui.err("Unable to parse policy group comparison `#{comparison_string}`. Only 2 references can be specified.") return false end else @old_base = Policyfile::ComparisonBase::PolicyGroup.new(comparison_string, policy_name, http_client) @new_base = Policyfile::ComparisonBase::Local.new(policyfile_lock_relpath) end true end def parse_git_comparison(git_ref) if git_ref.include?("...") old_ref, new_ref, *extra = git_ref.split("...") @old_base, @new_base = [old_ref, new_ref].map do |r| Policyfile::ComparisonBase::Git.new(r, policyfile_lock_relpath) end unless extra.empty? ui.err("Unable to parse git comparison `#{git_ref}`. Only 2 references can be specified.") return false end else @old_base = Policyfile::ComparisonBase::Git.new(git_ref, policyfile_lock_relpath) @new_base = Policyfile::ComparisonBase::Local.new(policyfile_lock_relpath) end true end def no_comparison_specified?(args) !policy_group_comparison?(args) && !config[:head] && !config[:git] end def conflicting_args_and_opts_given?(args) (config[:git] || config[:head]) && policy_group_comparison?(args) end def conflicting_git_options_given? config[:git] && config[:head] end def comparing_policy_groups? !(config[:git] || config[:head]) end # Try to detect if the only argument given is a policyfile path. This is # necessary because we support an optional argument with the path to the # ruby policyfile. It would be easier if we used an option like `-f`, but # that would be inconsistent with other commands (`chef install`, `chef # push`, etc.). def policy_group_comparison?(args) return false if args.empty? return true if args.size > 1 !(args.first =~ /\.rb\Z/) end def set_policyfile_path_from_args(args) policyfile_relative_path = if !comparing_policy_groups? args.first || "Policyfile.rb" elsif args.size == 1 "Policyfile.rb" else args.first end @storage_config = Policyfile::StorageConfig.new.use_policyfile(policyfile_relative_path) end def materialize_locks @old_lock = old_base.lock @new_lock = new_base.lock end end end end