inspec/lib/fetchers/git.rb
Jerry Aldrich 6ba4aaf630 Add Git SSH and HTTP basic auth support to inspec exec (#3562)
* Add support for `git@` and HTTP basic auth

This adds support for the following:

```
inspec exec git@github.com:private/example_profile
inspec exec https://username:token@github.com/private/example_profile
inspec exec https://username:password@webserver/private/example_profile
```

This also uses the Git fetcher when the URL ends in `.git`. Example:

```
git config credential.helper cache
git ls-remote https://github.com/private/example_profile.git
inspec exec https://github.com/private/example_profile.git
```

Signed-off-by: Jerry Aldrich <jerryaldrichiii@gmail.com>

* Add documentation for `inspec exec` usage

Signed-off-by: Jerry Aldrich <jerryaldrichiii@gmail.com>

* Add tests for Git fetcher and `inspec exec`

Signed-off-by: Jerry Aldrich <jerryaldrichiii@gmail.com>

* Add `opts` to branch of Git fetcher resolve logic

Signed-off-by: Jerry Aldrich <jerryaldrichiii@gmail.com>

* Modify Git example comments

Signed-off-by: Jerry Aldrich <jerryaldrichiii@gmail.com>

* Force `parse_uri` to attempt a parse

Signed-off-by: Jerry Aldrich <jerryaldrichiii@gmail.com>
2018-11-08 15:55:15 -05:00

169 lines
4.9 KiB
Ruby

# encoding: utf-8
require 'tmpdir'
require 'fileutils'
require 'mixlib/shellout'
require 'inspec/log'
module Fetchers
#
# The git fetcher uses the git binary to fetch remote git sources.
# Git-based sources should be specified with the `git:` key in the
# source hash. Additionally, we accept `:branch`, `:ref`, and `:tag`
# keys to allow users to pin to a particular revision.
#
# Parts of this class are derived from:
#
# https://github.com/chef/omnibus/blob/master/lib/omnibus/fetchers/git_fetcher.rb
#
# which is Copyright 2012-2014 Chef Software, Inc. and offered under
# the same Apache 2 software license as inspec.
#
# Many thanks to the omnibus authors!
#
# Note that we haven't replicated all of omnibus' features here. If
# you got to this file during debugging, you may want to look at the
# omnibus source for hints.
#
class Git < Inspec.fetcher(1)
name 'git'
priority 200
def self.resolve(target, opts = {})
if target.is_a?(String)
new(target, opts) if target.start_with?('git@') || target.end_with?('.git')
elsif target.respond_to?(:has_key?) && target.key?(:git)
new(target[:git], opts.merge(target))
end
end
def initialize(remote_url, opts = {})
@branch = opts[:branch]
@tag = opts[:tag]
@ref = opts[:ref]
@remote_url = remote_url
@repo_directory = nil
end
def fetch(dir)
@repo_directory = dir
FileUtils.mkdir_p(dir) unless Dir.exist?(dir)
if cloned?
checkout
else
Dir.mktmpdir do |tmpdir|
checkout(tmpdir)
Inspec::Log.debug("Checkout of #{resolved_ref} successful. Moving checkout to #{dir}")
FileUtils.cp_r(tmpdir + '/.', @repo_directory)
end
end
@repo_directory
end
def cache_key
resolved_ref
end
def archive_path
@repo_directory
end
def resolved_source
{ git: @remote_url, ref: resolved_ref }
end
private
def resolved_ref
@resolved_ref ||= if @ref
@ref
elsif @branch
resolve_ref(@branch)
elsif @tag
resolve_ref(@tag)
else
resolve_ref('master')
end
end
def resolve_ref(ref_name)
command_string = "git ls-remote \"#{@remote_url}\" \"#{ref_name}*\""
cmd = shellout(command_string)
raise "Error running '#{command_string}': #{cmd.stderr}" unless cmd.stderr == ''
ref = parse_ls_remote(cmd.stdout, ref_name)
if !ref
raise "Unable to resolve #{ref_name} to a specific git commit for #{@remote_url}"
end
ref
end
#
# The following comment is a minor modification of the comment in
# the omnibus source for a similar function:
#
# Dereference annotated tags.
#
# The +remote_list+ parameter is assumed to look like this:
#
# a2ed66c01f42514bcab77fd628149eccb4ecee28 refs/tags/rel-0.11.0
# f915286abdbc1907878376cce9222ac0b08b12b8 refs/tags/rel-0.11.0^{}
#
# The SHA with ^{} is the commit pointed to by an annotated
# tag. If ref isn't an annotated tag, there will not be a line
# with trailing ^{}.
#
# @param [String] output
# output from `git ls-remote origin` command
# @param [String] ref_name
# the target git ref_name
#
# @return [String]
#
def parse_ls_remote(output, ref_name)
pairs = output.lines.map { |l| l.chomp.split("\t") }
tagged_commit = pairs.find { |m| m[1].end_with?("#{ref_name}^{}") }
if tagged_commit
tagged_commit.first
else
pairs.find { |m| m[1].end_with?(ref_name.to_s) }&.first
end
end
def cloned?
File.directory?(File.join(@repo_directory, '.git'))
end
def clone(dir = @repo_directory)
git_cmd("clone #{@remote_url} ./", dir) unless cloned?
@repo_directory
end
def checkout(dir = @repo_directory)
clone(dir)
git_cmd("checkout #{resolved_ref}", dir)
@repo_directory
end
def git_cmd(cmd, dir = @repo_directory)
cmd = shellout("git #{cmd}", cwd: dir)
cmd.error!
cmd.status
rescue Errno::ENOENT
raise 'To use git sources, you must have git installed.'
end
def shellout(cmd, opts = {})
Inspec::Log.debug("Running external command: #{cmd} (#{opts})")
cmd = Mixlib::ShellOut.new(cmd, opts)
cmd.run_command
Inspec::Log.debug("External command: completed with exit status: #{cmd.exitstatus}")
Inspec::Log.debug('External command: STDOUT BEGIN')
Inspec::Log.debug(cmd.stdout)
Inspec::Log.debug('External command: STDOUT END')
Inspec::Log.debug('External command: STDERR BEGIN')
Inspec::Log.debug(cmd.stderr)
Inspec::Log.debug('External command: STDERR END')
cmd
end
end
end