diff --git a/lib/puppet/provider/vcsrepo.rb b/lib/puppet/provider/vcsrepo.rb index 9b0a080..9793264 100644 --- a/lib/puppet/provider/vcsrepo.rb +++ b/lib/puppet/provider/vcsrepo.rb @@ -1,60 +1,60 @@ # frozen_string_literal: true require 'tmpdir' require 'digest/md5' require 'fileutils' # Abstract class Puppet::Provider::Vcsrepo < Puppet::Provider def check_force return unless path_exists? && !path_empty? raise Puppet::Error, 'Path %s exists and is not the desired repository.' % @resource.value(:path) unless @resource.value(:force) notice 'Removing %s to replace with desired repository.' % @resource.value(:path) destroy end private def set_ownership owner = @resource.value(:owner) || nil group = @resource.value(:group) || nil excludes = @resource.value(:excludes) || nil if excludes.nil? || excludes.empty? FileUtils.chown_R(owner, group, @resource.value(:path)) else FileUtils.chown(owner, group, files) end end def files excludes = @resource.value(:excludes) path = @resource.value(:path) Dir["#{path}/**/*"].reject { |f| excludes.any? { |p| f.start_with?("#{path}/#{p}") } } end def path_exists? File.directory?(@resource.value(:path)) end def path_empty? # Path is empty if the only entries are '.' and '..' d = Dir.new(@resource.value(:path)) d.read # should return '.' d.read # should return '..' d.read.nil? end - # Note: We don't rely on Dir.chdir's behavior of automatically returning the + # NOTE: We don't rely on Dir.chdir's behavior of automatically returning the # value of the last statement -- for easier stubbing. def at_path #:nodoc: value = nil Dir.chdir(@resource.value(:path)) do value = yield end value end def tempdir @tempdir ||= File.join(Dir.tmpdir, 'vcsrepo-' + Digest::MD5.hexdigest(@resource.value(:path))) end end diff --git a/lib/puppet/provider/vcsrepo/bzr.rb b/lib/puppet/provider/vcsrepo/bzr.rb index ee218e4..86d5086 100644 --- a/lib/puppet/provider/vcsrepo/bzr.rb +++ b/lib/puppet/provider/vcsrepo/bzr.rb @@ -1,110 +1,110 @@ # frozen_string_literal: true require File.join(File.dirname(__FILE__), '..', 'vcsrepo') Puppet::Type.type(:vcsrepo).provide(:bzr, parent: Puppet::Provider::Vcsrepo) do desc 'Supports Bazaar repositories' commands bzr: 'bzr' has_features :reference_tracking def create check_force if !@resource.value(:source) create_repository(@resource.value(:path)) else clone_repository(@resource.value(:revision)) end end def working_copy_exists? return false unless File.directory?(@resource.value(:path)) begin bzr('status', @resource.value(:path)) - return true + true rescue Puppet::ExecutionFailure - return false + false end end def exists? working_copy_exists? end def destroy FileUtils.rm_rf(@resource.value(:path)) end def revision at_path do current_revid = bzr('version-info')[%r{^revision-id:\s+(\S+)}, 1] desired = @resource.value(:revision) begin desired_revid = bzr('revision-info', desired).strip.split(%r{\s+}).last rescue Puppet::ExecutionFailure # Possible revid available during update (but definitely not current) desired_revid = nil end if current_revid == desired_revid desired else current_revid end end end def revision=(desired) at_path do begin bzr('update', '-r', desired) rescue Puppet::ExecutionFailure bzr('update', '-r', desired, ':parent') end end update_owner end def source at_path do bzr('info')[%r{^\s+parent branch:\s+(\S+?)$}m, 1] end end def source=(_desired) create # recreate end def latest at_path do bzr('version-info', ':parent')[%r{^revision-id:\s+(\S+)}, 1] end end def latest? at_path do return revision == latest end end private def create_repository(path) bzr('init', path) update_owner end def clone_repository(revision) args = ['branch'] if revision args.push('-r', revision) end args.push(@resource.value(:source), @resource.value(:path)) bzr(*args) update_owner end def update_owner set_ownership if @resource.value(:owner) || @resource.value(:group) end end diff --git a/lib/puppet/provider/vcsrepo/cvs.rb b/lib/puppet/provider/vcsrepo/cvs.rb index 62c9e36..3b0064f 100644 --- a/lib/puppet/provider/vcsrepo/cvs.rb +++ b/lib/puppet/provider/vcsrepo/cvs.rb @@ -1,157 +1,157 @@ # frozen_string_literal: true require File.join(File.dirname(__FILE__), '..', 'vcsrepo') Puppet::Type.type(:vcsrepo).provide(:cvs, parent: Puppet::Provider::Vcsrepo) do desc 'Supports CVS repositories/workspaces' commands cvs: 'cvs' has_features :gzip_compression, :reference_tracking, :modules, :cvs_rsh, :user def create check_force if !@resource.value(:source) create_repository(@resource.value(:path)) else checkout_repository end update_owner end def exist? working_copy_exists? end def working_copy_exists? if @resource.value(:source) directory = File.join(@resource.value(:path), 'CVS') return false unless File.directory?(directory) begin at_path { runcvs('-nq', 'status', '-l') } - return true + true rescue Puppet::ExecutionFailure - return false + false end else directory = File.join(@resource.value(:path), 'CVSROOT') return false unless File.directory?(directory) config = File.join(@resource.value(:path), 'CVSROOT', 'config,v') return false unless File.exist?(config) true end end def destroy FileUtils.rm_rf(@resource.value(:path)) end def latest? Puppet.debug "Checking for updates because 'ensure => latest'" at_path do # We cannot use -P to prune empty dirs, otherwise # CVS would report those as "missing", regardless # if they have contents or updates. is_current = (runcvs('-nq', 'update', '-d').strip == '') unless is_current then Puppet.debug "There are updates available on the checkout's current branch/tag." end return is_current end end def latest # CVS does not have a conecpt like commit-IDs or change # sets, so we can only have the current branch name (or the # requested one, if that differs) as the "latest" revision. should = @resource.value(:revision) current = revision (should != current) ? should : current end def revision unless @rev if File.exist?(tag_file) contents = File.read(tag_file).strip - # Note: Doesn't differentiate between N and T entries + # NOTE: Doesn't differentiate between N and T entries @rev = contents[1..-1] else @rev = 'HEAD' end Puppet.debug "Checkout is on branch/tag '#{@rev}'" end @rev end def revision=(desired) at_path do runcvs('update', '-dr', desired, '.') update_owner @rev = desired end end def source File.read(File.join(@resource.value(:path), 'CVS', 'Root')).chomp end def source=(_desired) create # recreate end def module File.read(File.join(@resource.value(:path), 'CVS', 'Repository')).chomp end def module=(_desired) create # recreate end private def tag_file File.join(@resource.value(:path), 'CVS', 'Tag') end def checkout_repository dirname, basename = File.split(@resource.value(:path)) Dir.chdir(dirname) do args = ['-d', @resource.value(:source)] if @resource.value(:compression) args.push('-z', @resource.value(:compression)) end args.push('checkout') if @resource.value(:revision) args.push('-r', @resource.value(:revision)) end args.push('-d', basename, module_name) runcvs(*args) end end # If no module is provided, use '.', the root of the repo def module_name @resource.value(:module) || '.' end def create_repository(path) runcvs('-d', path, 'init') end def update_owner set_ownership if @resource.value(:owner) || @resource.value(:group) end def runcvs(*args) if @resource.value(:cvs_rsh) Puppet.debug 'Using CVS_RSH = ' + @resource.value(:cvs_rsh) e = { CVS_RSH: @resource.value(:cvs_rsh) } else e = {} end if @resource.value(:user) && @resource.value(:user) != Facter['id'].value Puppet.debug 'Running as user ' + @resource.value(:user) Puppet::Util::Execution.execute([:cvs, *args], uid: @resource.value(:user), custom_environment: e, combine: true, failonfail: true) else Puppet::Util::Execution.execute([:cvs, *args], custom_environment: e, combine: true, failonfail: true) end end end diff --git a/lib/puppet/provider/vcsrepo/git.rb b/lib/puppet/provider/vcsrepo/git.rb index f647382..764edc4 100644 --- a/lib/puppet/provider/vcsrepo/git.rb +++ b/lib/puppet/provider/vcsrepo/git.rb @@ -1,610 +1,610 @@ # frozen_string_literal: true require File.join(File.dirname(__FILE__), '..', 'vcsrepo') Puppet::Type.type(:vcsrepo).provide(:git, parent: Puppet::Provider::Vcsrepo) do desc 'Supports Git repositories' has_command(:git, 'git') do environment('HOME' => ENV['HOME']) end has_features :bare_repositories, :reference_tracking, :ssh_identity, :multiple_remotes, :user, :depth, :branch, :submodules def create check_force if @resource.value(:revision) && ensure_bare_or_mirror? raise("Cannot set a revision (#{@resource.value(:revision)}) on a bare repository") end if !@resource.value(:source) if @resource.value(:ensure) == :mirror raise('Cannot init repository with mirror option, try bare instead') end init_repository else clone_repository(default_url, @resource.value(:path)) update_remotes(@resource.value(:source)) set_mirror if @resource.value(:ensure) == :mirror && @resource.value(:source).is_a?(Hash) if @resource.value(:revision) checkout end if !ensure_bare_or_mirror? && @resource.value(:submodules) == :true update_submodules end end update_owner_and_excludes end def destroy FileUtils.rm_rf(@resource.value(:path)) end # Checks to see if the current revision is equal to the revision on the # remote (whether on a branch, tag, or reference) # # @return [Boolean] Returns true if the repo is on the latest revision def latest? revision == latest_revision end # Just gives the `should` value that we should be setting the repo to if # latest? returns false # # @return [String] Returns the target sha/tag/branch def latest if !@resource.value(:revision) && (branch = on_branch?) branch else @resource.value(:revision) end end # Get the current revision of the repo (tag/branch/sha) # # @return [String] Returns the branch/tag if the current sha matches the # remote; otherwise returns the current sha. def revision # HEAD is the default, but lets just be explicit here. get_revision('HEAD') end # Is passed the desired reference, whether a tag, rev, or branch. Should # handle transitions from a rev/branch/tag to a rev/branch/tag. Detached # heads should be treated like bare revisions. # # @param [String] desired The desired revision to which the repo should be # set. def revision=(desired) # just checkout tags and shas; fetch has already happened so they should be updated. checkout(desired) # branches require more work. if local_branch_revision?(desired) # reset instead of pull to avoid merge conflicts. assuming remote is # updated and authoritative. # TODO might be worthwhile to have an allow_local_changes param to decide # whether to reset or pull when we're ensuring latest. if @resource.value(:source) at_path { git_with_identity('reset', '--hard', "#{@resource.value(:remote)}/#{desired}") } else at_path { git_with_identity('reset', '--hard', desired.to_s) } end end # TODO: Would this ever reach here if it is bare? if !ensure_bare_or_mirror? && @resource.value(:submodules) == :true update_submodules end update_owner_and_excludes end def bare_exists? bare_git_config_exists? && !working_copy_exists? end def ensure_bare_or_mirror? [:bare, :mirror].include? @resource.value(:ensure) end # If :source is set to a hash (for supporting multiple remotes), # we search for the URL for :remote. If it doesn't exist, # we throw an error. If :source is just a string, we use that # value for the default URL. def default_url return @resource.value(:source) unless @resource.value(:source).is_a?(Hash) return @resource.value(:source)[@resource.value(:remote)] if @resource.value(:source).key?(@resource.value(:remote)) raise("You must specify the URL for remote '#{@resource.value(:remote)}' in the :source hash") end def working_copy_exists? # NOTE: a change in the `default_url` will tell the type that this repo # doesn't exist (i.e. it triggers a "not the same repository" error). # Thus, changing the `source` property from a string to a string (which # changes the origin url), or if the @resource.value(:remote)'s url is # changed, the provider will require force. return false unless File.directory?(File.join(@resource.value(:path), '.git')) at_path do if @resource.value(:source) begin return git('config', '--get', "remote.#{@resource.value(:remote)}.url").chomp == default_url rescue Puppet::ExecutionFailure return false end else begin git('status') return true rescue Puppet::ExecutionFailure return false end end end end def exists? working_copy_exists? || bare_exists? end def remove_remote(remote) at_path do git_with_identity('remote', 'remove', remote) end end def update_remote_url(remote_name, remote_url) current = git_with_identity('config', '-l') return if remote_url.nil? # Check if remote exists at all, regardless of URL. # If remote doesn't exist, add it if !current.include? "remote.#{remote_name}.url" git_with_identity('remote', 'add', remote_name, remote_url) - return true + true # If remote exists, but URL doesn't match, update URL elsif !current.include? "remote.#{remote_name}.url=#{remote_url}" git_with_identity('remote', 'set-url', remote_name, remote_url) - return true + true else - return false + false end end def source at_path do remotes = git('remote').split("\n") return git('config', '--get', "remote.#{remotes[0]}.url").chomp if remotes.size == 1 Hash[remotes.map do |remote| [remote, git('config', '--get', "remote.#{remote}.url").chomp] end] end end def source=(desired) # NOTE: a change in the `default_url` will tell the type that this repo # doesn't exist (i.e. it triggers a "not the same repository" error). # Thus, a change from a string to a string (which changes the origin url), # or if the @resource.value(:remote)'s url is changed, the provider will # require force, without ever reaching this block. The recreation is # duplicated here in case something changes in the `working_copy_exists?` # logic. current = source if current.is_a?(Hash) current.each_key do |remote| remove_remote(remote) if desired.is_a?(Hash) && !desired.key?(remote) remove_remote(remote) if desired.is_a?(String) && remote != @resource.value(:remote) end end if current.is_a?(String) && desired.is_a?(String) create # recreate else update_remotes(desired) end end def update_remotes(remotes) do_update = false # If supplied source is a hash of remote name and remote url pairs, then # we loop around the hash. Otherwise, we assume single url specified # in source property if remotes.is_a?(Hash) remotes.keys.sort.each do |remote_name| remote_url = remotes[remote_name] at_path { do_update |= update_remote_url(remote_name, remote_url) } end else at_path { do_update |= update_remote_url(@resource.value(:remote), remotes) } end # If at least one remote was added or updated, then we must # call the 'git remote update' command at_path { git_with_identity('remote', 'update') } if do_update == true end def update_references at_path do git_with_identity('fetch', @resource.value(:remote)) git_with_identity('fetch', '--tags', @resource.value(:remote)) update_owner_and_excludes end end # Convert working copy to bare # # Moves: # /.git # to: # / # and sets core.bare=true, and calls `set_mirror` if appropriate def convert_working_copy_to_bare return unless working_copy_exists? && !bare_exists? notice 'Converting working copy repository to bare repository' FileUtils.mv(File.join(@resource.value(:path), '.git'), tempdir) FileUtils.rm_rf(@resource.value(:path)) FileUtils.mv(tempdir, @resource.value(:path)) at_path do git('config', '--local', '--bool', 'core.bare', 'true') return unless @resource.value(:ensure) == :mirror raise('Cannot have empty repository that is also a mirror.') unless @resource.value(:source) set_mirror end end # Convert bare to working copy # # Moves: # / # to: # /.git # and sets core.bare=false, and calls `set_no_mirror` if appropriate def convert_bare_to_working_copy notice 'Converting bare repository to working copy repository' FileUtils.mv(@resource.value(:path), tempdir) FileUtils.mkdir(@resource.value(:path)) FileUtils.mv(tempdir, File.join(@resource.value(:path), '.git')) if commits? at_path do git('config', '--local', '--bool', 'core.bare', 'false') reset('HEAD') git_with_identity('checkout', '--force') update_owner_and_excludes end end set_no_mirror if mirror? end def mirror? at_path do begin git('config', '--get-regexp', 'remote\..*\.mirror') return true rescue Puppet::ExecutionFailure return false end end end def set_mirror at_path do if @resource.value(:source).is_a?(String) git('config', "remote.#{@resource.value(:remote)}.mirror", 'true') else @resource.value(:source).each_key do |remote| git('config', "remote.#{remote}.mirror", 'true') end end end end def set_no_mirror at_path do if @resource.value(:source).is_a?(String) begin git('config', '--unset', "remote.#{@resource.value(:remote)}.mirror") rescue Puppet::ExecutionFailure next end else @resource.value(:source).each_key do |remote| begin git('config', '--unset', "remote.#{remote}.mirror") rescue Puppet::ExecutionFailure next end end end end end private # @!visibility private def bare_git_config_exists? return false unless File.exist?(File.join(@resource.value(:path), 'config')) begin at_path { git('config', '--list', '--file', 'config') } - return true + true rescue Puppet::ExecutionFailure - return false + false end end # @!visibility private def clone_repository(source, path) args = ['clone'] if @resource.value(:depth) && @resource.value(:depth).to_i > 0 args.push('--depth', @resource.value(:depth).to_s) if @resource.value(:revision) && !@resource.value(:branch) args.push('--branch', @resource.value(:revision).to_s) end end if @resource.value(:branch) args.push('--branch', @resource.value(:branch).to_s) end case @resource.value(:ensure) when :bare then args << '--bare' when :mirror then args << '--mirror' end if @resource.value(:remote) != 'origin' args.push('--origin', @resource.value(:remote)) end if !working_copy_exists? args.push(source, path) Dir.chdir('/') do git_with_identity(*args) end else notice 'Repo has already been cloned' end end # @!visibility private def init_repository if @resource.value(:ensure) == :bare && working_copy_exists? convert_working_copy_to_bare elsif @resource.value(:ensure) == :present && bare_exists? convert_bare_to_working_copy else # normal init FileUtils.mkdir(@resource.value(:path)) FileUtils.chown(@resource.value(:user), nil, @resource.value(:path)) if @resource.value(:user) args = ['init'] if @resource.value(:ensure) == :bare args << '--bare' end at_path do git_with_identity(*args) end end end # @!visibility private def commits? at_path do begin commits = git_with_identity('rev-list', '--all', '--count').to_i rescue Puppet::ExecutionFailure commits = 0 end return commits > 0 end end # Will checkout a rev/branch/tag using the locally cached versions. Does not # handle upstream branch changes # @!visibility private def checkout(revision = @resource.value(:revision)) keep_local_changes = @resource.value(:keep_local_changes) stash if keep_local_changes == :true if !local_branch_revision?(revision) && remote_branch_revision?(revision) # non-locally existant branches (perhaps switching to a branch that has never been checked out) at_path { git_with_identity('checkout', '--force', '-b', revision, '--track', "#{@resource.value(:remote)}/#{revision}") } else # tags, locally existant branches (perhaps outdated), and shas at_path { git_with_identity('checkout', '--force', revision) } end unstash if keep_local_changes == :true end # @!visibility private def reset(desired) at_path do git_with_identity('reset', '--hard', desired) end end # @!visibility private def update_submodules at_path do git_with_identity('submodule', 'update', '--init', '--recursive') end end # Determins if the branch exists at the upstream but has not yet been locally committed # @!visibility private def remote_branch_revision?(revision = @resource.value(:revision)) # git < 1.6 returns '#{@resource.value(:remote)}/#{revision}' # git 1.6+ returns 'remotes/#{@resource.value(:remote)}/#{revision}' branch = at_path { branches.grep %r{(remotes/)?#{@resource.value(:remote)}/#{revision}$} } branch unless branch.empty? end # Determins if the branch is already cached locally # @!visibility private def local_branch_revision?(revision = @resource.value(:revision)) at_path { branches.include?(revision) } end # @!visibility private def tag_revision?(revision = @resource.value(:revision)) at_path { tags.include?(revision) } end # @!visibility private def branches at_path { git_with_identity('branch', '--no-color', '-a') }.tr('*', ' ').split(%r{\n}).map { |line| line.strip } end # git < 2.4 returns 'detached from' # git 2.4+ returns 'HEAD detached at' # @!visibility private def on_branch? at_path do matches = git_with_identity('branch', '--no-color', '-a').match %r{\*\s+(.*)} - matches[1] unless matches[1] =~ %r{(\(detached from|\(HEAD detached at|\(no branch)} + matches[1] unless %r{(\(detached from|\(HEAD detached at|\(no branch)}.match?(matches[1]) end end # @!visibility private def tags at_path { git_with_identity('tag', '-l') }.split(%r{\n}).map { |line| line.strip } end # @!visibility private def set_excludes # Excludes may be an Array or a String. at_path do open('.git/info/exclude', 'w') do |f| if @resource.value(:excludes).respond_to?(:each) @resource.value(:excludes).each { |ex| f.puts ex } else f.puts @resource.value(:excludes) end end end end # @!visibility private def stash at_path { git_with_identity('stash', 'save') } end # @!visibility private def unstash at_path { git_with_identity('stash', 'pop') } end # Finds the latest revision or sha of the current branch if on a branch, or # of HEAD otherwise. # @note Calls create which can forcibly destroy and re-clone the repo if # force => true # @see get_revision # # @!visibility private # @return [String] Returns the output of get_revision def latest_revision # TODO: Why is create called here anyway? create if @resource.value(:force) && working_copy_exists? create unless working_copy_exists? branch = on_branch? return get_revision("#{@resource.value(:remote)}/#{branch}") if branch get_revision end # Returns the current revision given if the revision is a tag or branch and # matches the current sha. If the current sha does not match the sha of a tag # or branch, then it will just return the sha (ie, is not in sync) # # @!visibility private # # @param [String] rev The revision of which to check if it is current # @return [String] Returns the tag/branch of the current repo if it's up to # date; otherwise returns the sha of the requested revision. def get_revision(rev = 'HEAD') unless @resource.value(:source) status = at_path { git_with_identity('status') } is_it_new = status =~ %r{Initial commit|No commits yet} if is_it_new status =~ %r{On branch (.*)} branch = Regexp.last_match(1) return branch end end current = at_path { git_with_identity('rev-parse', rev).strip } if @resource.value(:revision) == current # if already pointed at desired revision, it must be a SHA, so just return it return current end if @resource.value(:source) update_references end if @resource.value(:revision) canonical = if tag_revision? # git-rev-parse will give you the hash of the tag object itself rather # than the commit it points to by default. Using tag^0 will return the # actual commit. at_path { git_with_identity('rev-parse', "#{@resource.value(:revision)}^0").strip } elsif local_branch_revision? at_path { git_with_identity('rev-parse', @resource.value(:revision)).strip } elsif remote_branch_revision? at_path { git_with_identity('rev-parse', "#{@resource.value(:remote)}/#{@resource.value(:revision)}").strip } else # look for a sha (could match invalid shas) at_path { git_with_identity('rev-parse', '--revs-only', @resource.value(:revision)).strip } end raise("#{@resource.value(:revision)} is not a local or remote ref") if canonical.nil? || canonical.empty? current = @resource.value(:revision) if current == canonical end current end # @!visibility private def update_owner_and_excludes if @resource.value(:owner) || @resource.value(:group) set_ownership end set_excludes if @resource.value(:excludes) end def git_version git('--version').match(%r{[0-9]+\.[0-9]+\.[0-9]+(\.[0-9]+)?})[0] end # @!visibility private def git_with_identity(*args) if @resource.value(:trust_server_cert) == :true git_ver = git_version git_ver_err = "Can't set sslVerify to false, the -c parameter is not supported in Git #{git_ver}. Please install Git 1.7.2 or higher." return raise(git_ver_err) unless Gem::Version.new(git_ver) >= Gem::Version.new('1.7.2') args.unshift('-c', 'http.sslVerify=false') end if @resource.value(:identity) Tempfile.open('git-helper', Puppet[:statedir]) do |f| f.puts '#!/bin/sh' f.puts 'SSH_AUTH_SOCKET=' f.puts 'export SSH_AUTH_SOCKET' f.puts 'exec ssh -oStrictHostKeyChecking=no -oPasswordAuthentication=no -oKbdInteractiveAuthentication=no ' \ "-oChallengeResponseAuthentication=no -oConnectTimeout=120 -i #{@resource.value(:identity)} $*" f.close FileUtils.chmod(0o755, f.path) env_git_ssh_save = ENV['GIT_SSH'] env_git_ssh_command_save = ENV['GIT_SSH_COMMAND'] ENV['GIT_SSH'] = f.path ENV['GIT_SSH_COMMAND'] = nil # Unset GIT_SSH_COMMAND environment variable ret = git(*args) ENV['GIT_SSH'] = env_git_ssh_save ENV['GIT_SSH_COMMAND'] = env_git_ssh_command_save return ret end elsif @resource.value(:user) && @resource.value(:user) != Facter['id'].value env = Etc.getpwnam(@resource.value(:user)) Puppet::Util::Execution.execute("git #{args.join(' ')}", uid: @resource.value(:user), failonfail: true, custom_environment: { 'HOME' => env['dir'] }, combine: true) else git(*args) end end end diff --git a/lib/puppet/provider/vcsrepo/hg.rb b/lib/puppet/provider/vcsrepo/hg.rb index 837746c..1b20773 100644 --- a/lib/puppet/provider/vcsrepo/hg.rb +++ b/lib/puppet/provider/vcsrepo/hg.rb @@ -1,155 +1,155 @@ # frozen_string_literal: true require File.join(File.dirname(__FILE__), '..', 'vcsrepo') Puppet::Type.type(:vcsrepo).provide(:hg, parent: Puppet::Provider::Vcsrepo) do desc 'Supports Mercurial repositories' commands hg: 'hg' has_features :reference_tracking, :ssh_identity, :user, :basic_auth def create check_force if !@resource.value(:source) create_repository(@resource.value(:path)) else clone_repository(@resource.value(:revision)) end update_owner end def working_copy_exists? return false unless File.directory?(@resource.value(:path)) begin hg_wrapper('status', @resource.value(:path)) - return true + true rescue Puppet::ExecutionFailure - return false + false end end def exists? working_copy_exists? end def destroy FileUtils.rm_rf(@resource.value(:path)) end def latest? at_path do return revision == latest end end def latest at_path do begin hg_wrapper('incoming', '--branch', '.', '--newest-first', '--limit', '1', remote: true)[%r{^changeset:\s+(?:-?\d+):(\S+)}m, 1] rescue Puppet::ExecutionFailure # If there are no new changesets, return the current nodeid revision end end end def revision at_path do current = hg_wrapper('parents')[%r{^changeset:\s+(?:-?\d+):(\S+)}m, 1] desired = @resource.value(:revision) if desired # Return the tag name if it maps to the current nodeid mapped = hg_wrapper('tags')[%r{^#{Regexp.quote(desired)}\s+\d+:(\S+)}m, 1] if current == mapped desired else current end else current end end end def revision=(desired) at_path do begin hg_wrapper('pull', remote: true) rescue StandardError next end begin hg_wrapper('merge') rescue Puppet::ExecutionFailure next # If there's nothing to merge, just skip end hg_wrapper('update', '--clean', '-r', desired) end update_owner end def source at_path do hg_wrapper('paths')[%r{^default = (.*)}, 1] end end def source=(_desired) create # recreate end private def create_repository(path) hg_wrapper('init', path) end def clone_repository(revision) args = ['clone'] if revision args.push('-u', revision) end args.push(@resource.value(:source), @resource.value(:path)) args.push(remote: true) hg_wrapper(*args) end def update_owner set_ownership if @resource.value(:owner) || @resource.value(:group) end def sensitive? (@resource.parameters.key?(:basic_auth_password) && @resource.parameters[:basic_auth_password].sensitive) ? true : false # Check if there is a sensitive parameter end def hg_wrapper(*args) options = { remote: false } if !args.empty? && args[-1].is_a?(Hash) options.merge!(args.pop) end if @resource.value(:basic_auth_username) && @resource.value(:basic_auth_password) args += [ '--config', "auth.x.prefix=#{@resource.value(:source)}", '--config', "auth.x.username=#{@resource.value(:basic_auth_username)}", '--config', "auth.x.password=#{sensitive? ? @resource.value(:basic_auth_password).unwrap : @resource.value(:basic_auth_password)}", '--config', 'auth.x.schemes=http https' ] end if options[:remote] && @resource.value(:identity) args += ['--ssh', "ssh -oStrictHostKeyChecking=no -oPasswordAuthentication=no -oKbdInteractiveAuthentication=no -oChallengeResponseAuthentication=no -i #{@resource.value(:identity)}"] end args.map! { |a| (a =~ %r{\s}) ? "'#{a}'" : a } # Adds quotes to arguments with whitespaces. if @resource.value(:user) && @resource.value(:user) != Facter['id'].value Puppet::Util::Execution.execute("hg #{args.join(' ')}", uid: @resource.value(:user), failonfail: true, combine: true, sensitive: sensitive?) else Puppet::Util::Execution.execute("hg #{args.join(' ')}", sensitive: sensitive?) end end end diff --git a/lib/puppet/provider/vcsrepo/p4.rb b/lib/puppet/provider/vcsrepo/p4.rb index 7230c1f..8b213a1 100644 --- a/lib/puppet/provider/vcsrepo/p4.rb +++ b/lib/puppet/provider/vcsrepo/p4.rb @@ -1,286 +1,286 @@ # frozen_string_literal: true require File.join(File.dirname(__FILE__), '..', 'vcsrepo') Puppet::Type.type(:vcsrepo).provide(:p4, parent: Puppet::Provider::Vcsrepo) do desc 'Supports Perforce depots' has_features :filesystem_types, :reference_tracking, :p4config def create check_force # create or update client create_client(client_name) # if source provided, sync client source = @resource.value(:source) if source revision = @resource.value(:revision) sync_client(source, revision) end update_owner end def working_copy_exists? # Check if the server is there, or raise error p4(['info'], marshal: false) # Check if workspace is setup args = ['where'] args.push(@resource.value(:path) + '/...') hash = p4(args, raise: false) (hash['code'] != 'error') end def exists? working_copy_exists? end def destroy args = ['client'] args.push('-d', '-f') args.push(client_name) p4(args) FileUtils.rm_rf(@resource.value(:path)) end def latest? rev = revision if rev (rev >= latest) else true end end def latest args = ['changes'] args.push('-m1', @resource.value(:source)) hash = p4(args) hash['change'].to_i end def revision args = ['cstat'] args.push(@resource.value(:source)) hash = p4(args, marshal: false) hash = marshal_cstat(hash) revision = 0 if hash && hash['code'] != 'error' hash['data'].each do |c| if c['status'] == 'have' change = c['change'].to_i revision = change if change > revision end end end revision end def revision=(desired) sync_client(@resource.value(:source), desired) update_owner end def source args = ['where'] args.push(@resource.value(:path) + '/...') hash = p4(args, raise: false) hash['depotFile'] end def source=(_desired) create # recreate end private def update_owner set_ownership if @resource.value(:owner) || @resource.value(:group) end # Sync the client workspace files to head or specified revision. # Params: # +source+:: Depot path to sync # +revision+:: Perforce change list to sync to (optional) def sync_client(source, revision) Puppet.debug "Syncing: #{source}" args = ['sync'] if revision args.push(source + "@#{revision}") else args.push(source) end p4(args) end # Returns the name of the Perforce client workspace def client_name p4config = @resource.value(:p4config) # default (generated) client name path = @resource.value(:path) host = Facter.value('hostname') default = 'puppet-' + Digest::MD5.hexdigest(path + host) # check config for client name set_client = nil if p4config && File.file?(p4config) open(p4config) do |f| m = f.grep(%r{^P4CLIENT=}).pop p = %r{^P4CLIENT=(.*)$} set_client = p.match(m)[1] if m end end set_client || ENV['P4CLIENT'] || default end # Create (or update) a client workspace spec. # If a client name is not provided then a hash based on the path is used. # Params: # +client+:: Name of client workspace # +path+:: The Root location of the Perforce client workspace def create_client(client) Puppet.debug "Creating client: #{client}" # fetch client spec hash = parse_client(client) hash['Root'] = @resource.value(:path) hash['Description'] = 'Generated by Puppet VCSrepo' # check is source is a Stream source = @resource.value(:source) if source parts = source.split(%r{/}) if parts && parts.length >= 4 source = '//' + parts[2] + '/' + parts[3] streams = p4(['streams', source], raise: false) if streams['code'] == 'stat' hash['Stream'] = streams['Stream'] notice 'Streams' + streams['Stream'].inspect end end end # save client spec save_client(hash) end # Fetches a client workspace spec from Perforce and returns a hash map representation. # Params: # +client+:: name of the client workspace def parse_client(client) args = ['client'] args.push('-o', client) hash = p4(args) hash end # Saves the client workspace spec from the given hash # Params: # +hash+:: hash map of client spec def save_client(hash) spec = '' view = "\nView:\n" hash.keys.sort.each do |k| v = hash[k] next if k == 'code' - if k.to_s =~ %r{View} + if %r{View}.match?(k.to_s) view += "\t#{v}\n" else spec += "#{k}: #{v}\n" end end spec += view args = ['client'] args.push('-i') p4(args, input: spec, marshal: false) end # Sets Perforce Configuration environment. # P4CLIENT generated, but overwitten if defined in config. def config p4config = @resource.value(:p4config) cfg = {} cfg.store 'P4CONFIG', p4config if p4config cfg.store 'P4CLIENT', client_name cfg end def p4(args, options = {}) # Merge custom options with defaults opts = { raise: true, # Raise errors marshal: true, # Marshal output }.merge(options) cmd = ['p4'] cmd.push '-R' if opts[:marshal] cmd.push args cmd_str = cmd.respond_to?(:join) ? cmd.join(' ') : cmd Puppet.debug "environment: #{config}" Puppet.debug "command: #{cmd_str}" hash = {} Open3.popen3(config, cmd_str) do |i, o, e, t| # Send input stream if provided if opts[:input] Puppet.debug "input:\n" + opts[:input] i.write opts[:input] i.close end if opts[:marshal] hash = Marshal.dump(o) else hash['data'] = o.read end # Raise errors, Perforce or Exec if opts[:raise] && !e.eof && t.value != 0 raise Puppet::Error, "\nP4: #{e.read}" end if opts[:raise] && hash['code'] == 'error' && t.value != 0 raise Puppet::Error, "\nP4: #{hash['data']}" end end Puppet.debug "hash: #{hash}\n" hash end # helper method as cstat does not Marshal def marshal_cstat(hash) data = hash['data'] code = 'error' list = [] change = {} data.each_line do |l| p = %r{^\.\.\. (.*) (.*)$} m = p.match(l) next unless m change[m[1]] = m[2] next unless m[1] == 'status' code = 'stat' list.push change change = {} end hash = {} hash.store 'code', code hash.store 'data', list hash end end diff --git a/lib/puppet/provider/vcsrepo/svn.rb b/lib/puppet/provider/vcsrepo/svn.rb index 879eec5..420491d 100644 --- a/lib/puppet/provider/vcsrepo/svn.rb +++ b/lib/puppet/provider/vcsrepo/svn.rb @@ -1,310 +1,310 @@ # frozen_string_literal: true require File.join(File.dirname(__FILE__), '..', 'vcsrepo') Puppet::Type.type(:vcsrepo).provide(:svn, parent: Puppet::Provider::Vcsrepo) do desc 'Supports Subversion repositories' commands svn: 'svn', svnadmin: 'svnadmin', svnlook: 'svnlook' has_features :filesystem_types, :reference_tracking, :basic_auth, :configuration, :conflict, :depth, :include_paths def create check_force if !@resource.value(:source) if @resource.value(:includes) raise Puppet::Error, 'Specifying include paths on a nonexistent repo.' end create_repository(@resource.value(:path)) else if @resource.value(:basic_auth_username) && !@resource.value(:basic_auth_password) raise("You must specify the HTTP basic authentication password for user '#{@resource.value(:basic_auth_username)}'") end if !@resource.value(:basic_auth_username) && @resource.value(:basic_auth_password) raise('You must specify the HTTP basic authentication username') end if @resource.value(:basic_auth_username) && @resource.value(:basic_auth_password) - if @resource.value(:basic_auth_password).to_s =~ %r{[\u007B-\u00BF\u02B0-\u037F\u2000-\u2BFF]} + if %r{[\u007B-\u00BF\u02B0-\u037F\u2000-\u2BFF]}.match?(@resource.value(:basic_auth_password).to_s) raise('The password can not contain non-ASCII characters') end end checkout_repository(@resource.value(:source), @resource.value(:path), @resource.value(:revision), @resource.value(:depth)) end if @resource.value(:includes) validate_version update_includes(@resource.value(:includes)) end update_owner end def working_copy_exists? return false unless File.directory?(@resource.value(:path)) if @resource.value(:source) begin svn_wrapper('info', @resource.value(:path)) - return true + true rescue Puppet::ExecutionFailure => detail - if detail.message =~ %r{This client is too old} + if %r{This client is too old}.match?(detail.message) raise Puppet::Error, detail.message end - return false + false end else begin svnlook('uuid', @resource.value(:path)) - return true + true rescue Puppet::ExecutionFailure - return false + false end end end def exists? working_copy_exists? end def destroy FileUtils.rm_rf(@resource.value(:path)) end def latest? at_path do (revision >= latest) && (@resource.value(:source) == source) end end def buildargs args = ['--non-interactive'] if @resource.value(:basic_auth_username) && @resource.value(:basic_auth_password) args.push('--username', @resource.value(:basic_auth_username)) args.push('--password', sensitive? ? @resource.value(:basic_auth_password).unwrap : @resource.value(:basic_auth_password)) args.push('--no-auth-cache') end if @resource.value(:configuration) args.push('--config-dir', @resource.value(:configuration)) end if @resource.value(:trust_server_cert) != :false args.push('--trust-server-cert') end args end def latest args = buildargs.push('info', '-r', 'HEAD') at_path do svn_wrapper(*args)[%r{^Revision:\s+(\d+)}m, 1] end end def source args = buildargs.push('info') at_path do svn_wrapper(*args)[%r{^URL:\s+(\S+)}m, 1] end end def source=(desired) args = buildargs.push('switch') if @resource.value(:force) args.push('--force') end if @resource.value(:revision) args.push('-r', @resource.value(:revision)) end if @resource.value(:conflict) args.push('--accept', @resource.value(:conflict)) end args.push(desired) at_path do svn_wrapper(*args) end update_owner end def revision args = buildargs.push('info') at_path do svn_wrapper(*args)[%r{^Revision:\s+(\d+)}m, 1] end end def revision=(desired) args = if @resource.value(:source) buildargs.push('switch', '-r', desired, @resource.value(:source)) else buildargs.push('update', '-r', desired) end if @resource.value(:force) args.push('--force') end if @resource.value(:conflict) args.push('--accept', @resource.value(:conflict)) end at_path do svn_wrapper(*args) end update_owner end def includes return nil if Gem::Version.new(return_svn_client_version) < Gem::Version.new('1.6.0') get_includes('.') end def includes=(desired) validate_version exists = includes old_paths = exists - desired new_paths = desired - exists # Remove paths that are no longer specified old_paths.each { |path| delete_include(path) } update_includes(new_paths) end private def svn_wrapper(*args) Puppet::Util::Execution.execute("svn #{args.join(' ')}", sensitive: sensitive?) end def sensitive? (@resource.parameters.key?(:basic_auth_password) && @resource.parameters[:basic_auth_password].sensitive) ? true : false # Check if there is a sensitive parameter end def get_includes(directory) at_path do args = buildargs.push('info', directory) if svn_wrapper(*args)[%r{^Depth:\s+(\w+)}m, 1] != 'empty' return directory[2..-1].gsub(File::SEPARATOR, '/') end Dir.entries(directory).map { |entry| next if ['.', '..', '.svn'].include?(entry) entry = File.join(directory, entry) if File.directory?(entry) get_includes(entry) elsif File.file?(entry) entry[2..-1].gsub(File::SEPARATOR, '/') end }.flatten.compact! end end def delete_include(path) at_path do # svn version 1.6 has an incorrect implementation of the `exclude` # parameter to `--set-depth`; it doesn't handle files, only # directories. I know, I rolled my eyes, too. svn_ver = return_svn_client_version if Gem::Version.new(svn_ver) < Gem::Version.new('1.7.0') && !File.directory?(path) # In the non-happy case, we delete the file, and check if the only # thing left in that directory is the .svn folder. If that's the case, # the loop below will take care of excluding the parent directory, and # we're back to a happy case. But, if that's not the case, we need to # fire off a warning telling the user the path can't be excluded. Puppet.debug "Vcsrepo[#{@resource.name}]: Need to handle #{path} removal specially" File.delete(path) if Dir.entries(File.dirname(path)).sort != ['.', '..', '.svn'] Puppet.warning "Unable to exclude #{path} from Vcsrepo[#{@resource.name}]; update to subversion >= 1.7" end else Puppet.debug "Vcsrepo[#{@resource.name}]: Can remove #{path} directly using svn" args = buildargs.push('update', '--set-depth', 'exclude', path) svn_wrapper(*args) end # Keep walking up the parent directories of this include until we find # a non-empty folder, excluding as we go. while (path = path.rpartition(File::SEPARATOR)[0]) != '' entries = Dir.entries(path).sort break if entries != ['.', '..'] && entries != ['.', '..', '.svn'] args = buildargs.push('update', '--set-depth', 'exclude', path) svn_wrapper(*args) end end end def checkout_repository(source, path, revision, depth) args = buildargs.push('checkout') if revision args.push('-r', revision) end if @resource.value(:includes) # Make root checked out at empty depth to provide sparse directories args.push('--depth', 'empty') elsif depth args.push('--depth', depth) end args.push(source, path) svn_wrapper(*args) end def create_repository(path) args = ['create'] if @resource.value(:fstype) args.push('--fs-type', @resource.value(:fstype)) end args << path svnadmin(*args) end def update_owner set_ownership if @resource.value(:owner) || @resource.value(:group) end def update_includes(paths) at_path do args = buildargs.push('update') args.push('--depth', 'empty') if @resource.value(:revision) args.push('-r', @resource.value(:revision)) end parents = paths.map { |path| File.dirname(path) } parents = make_include_paths(parents) args.push(*parents) svn_wrapper(*args) args = buildargs.push('update') if @resource.value(:revision) args.push('-r', @resource.value(:revision)) end if @resource.value(:depth) args.push('--depth', @resource.value(:depth)) end args.push(*paths) svn_wrapper(*args) end end def make_include_paths(includes) includes.map { |inc| prefix = nil inc.split('/').map do |path| prefix = [prefix, path].compact.join('/') end }.flatten end def return_svn_client_version Facter.value('vcsrepo_svn_ver').dup end def validate_version svn_ver = return_svn_client_version raise "Includes option is not available for SVN versions < 1.6. Version installed: #{svn_ver}" if Gem::Version.new(svn_ver) < Gem::Version.new('1.6.0') end end diff --git a/lib/puppet/type/vcsrepo.rb b/lib/puppet/type/vcsrepo.rb index 302464c..23d1649 100644 --- a/lib/puppet/type/vcsrepo.rb +++ b/lib/puppet/type/vcsrepo.rb @@ -1,318 +1,318 @@ # frozen_string_literal: true require 'pathname' require 'puppet/parameter/boolean' Puppet::Type.newtype(:vcsrepo) do desc 'A local version control repository' feature :gzip_compression, 'The provider supports explicit GZip compression levels' feature :basic_auth, 'The provider supports HTTP Basic Authentication' feature :bare_repositories, "The provider differentiates between bare repositories and those with working copies", methods: [:bare_exists?, :working_copy_exists?] feature :filesystem_types, 'The provider supports different filesystem types' feature :reference_tracking, "The provider supports tracking revision references that can change over time (eg, some VCS tags and branch names)" feature :ssh_identity, 'The provider supports a configurable SSH identity file' feature :user, 'The provider can run as a different user' feature :modules, 'The repository contains modules that can be chosen of' feature :multiple_remotes, 'The repository tracks multiple remote repositories' feature :configuration, 'The configuration directory to use' feature :cvs_rsh, 'The provider understands the CVS_RSH environment variable' feature :depth, 'The provider can do shallow clones or set scope limit' feature :branch, 'The name of the branch' feature :p4config, 'The provider understands Perforce Configuration' feature :submodules, 'The repository contains submodules which can be optionally initialized' feature :conflict, 'The provider supports automatic conflict resolution' feature :include_paths, 'The provider supports checking out only specific paths' feature :keep_local_changes, 'The provider supports keeping local changes on files tracked by the repository when changing revision' ensurable do desc 'Ensure the version control repository.' attr_accessor :latest def insync?(is) @should ||= [] case should when :present return true unless [:absent, :purged, :held].include?(is) when :latest return true if is == :latest - return false + false when :bare - return is == :bare + is == :bare when :mirror - return is == :mirror + is == :mirror when :absent - return is == :absent + is == :absent end end newvalue :present do if !provider.exists? provider.create elsif provider.class.feature?(:bare_repositories) && provider.bare_exists? provider.convert_bare_to_working_copy end end newvalue :bare, required_features: [:bare_repositories] do if !provider.exists? provider.create elsif provider.working_copy_exists? provider.convert_working_copy_to_bare elsif provider.mirror? provider.set_no_mirror end end newvalue :mirror, required_features: [:bare_repositories] do if !provider.exists? provider.create elsif provider.working_copy_exists? provider.convert_working_copy_to_bare elsif !provider.mirror? provider.set_mirror end end newvalue :absent do provider.destroy end newvalue :latest, required_features: [:reference_tracking] do if provider.exists? && !@resource.value(:force) if provider.class.feature?(:bare_repositories) && provider.bare_exists? provider.convert_bare_to_working_copy end if provider.respond_to?(:update_references) provider.update_references end reference = if provider.respond_to?(:latest?) provider.latest || provider.revision else resource.value(:revision) || provider.revision end notice "Updating to latest '#{reference}' revision" provider.revision = reference else notice 'Creating repository from latest' provider.create end end def retrieve prov = @resource.provider raise Puppet::Error, 'Could not find provider' unless prov if prov.working_copy_exists? (@should.include?(:latest) && prov.latest?) ? :latest : :present elsif prov.class.feature?(:bare_repositories) && prov.bare_exists? if prov.mirror? :mirror else :bare end else :absent end end end newparam :path do desc 'Absolute path to repository' isnamevar validate do |value| path = Pathname.new(value) unless path.absolute? raise ArgumentError, "Path must be absolute: #{path}" end end end newproperty :source do desc 'The source URI for the repository' # Tolerate versions/providers that strip/add trailing slashes def insync?(is) # unwrap @should should = @should[0] return true if is == should begin if should[-1] == '/' return true if is == should[0..-2] elsif is[-1] == '/' return true if is[0..-2] == should end rescue StandardError return end false end end newparam :fstype, required_features: [:filesystem_types] do desc 'Filesystem type' end newproperty :revision do desc 'The revision of the repository' newvalue(%r{^\S+$}) end newparam :owner do desc 'The user/uid that owns the repository files' end newparam :group do desc 'The group/gid that owns the repository files' end newparam :user do desc 'The user to run for repository operations' end newparam :excludes do desc "Local paths which shouldn't be tracked by the repository" end newproperty :includes, required_features: [:include_paths], array_matching: :all do desc 'Paths to be included from the repository' def insync?(is) if is.is_a?(Array) && @should.is_a?(Array) is.sort == @should.sort else is == @should end end validate do |path| raise Puppet::Error, "Include path '#{path}' starts with a '/'; remove it" if path[0..0] == '/' super(path) end end newparam(:force, boolean: true, parent: Puppet::Parameter::Boolean) do desc 'Force repository creation, destroying any files on the path in the process.' defaultto false end newparam :compression, required_features: [:gzip_compression] do desc 'Compression level' validate do |amount| unless Integer(amount).between?(0, 6) raise ArgumentError, "Unsupported compression level: #{amount} (expected 0-6)" end end end newparam :basic_auth_username, required_features: [:basic_auth] do desc 'HTTP Basic Auth username' end newparam :basic_auth_password, required_features: [:basic_auth] do desc 'HTTP Basic Auth password' end newparam :identity, required_features: [:ssh_identity] do desc 'SSH identity file' end newproperty :module, required_features: [:modules] do desc 'The repository module to manage' end newparam :remote, required_features: [:multiple_remotes] do desc 'The remote repository to track' defaultto 'origin' end newparam :configuration, required_features: [:configuration] do desc 'The configuration directory to use' end newparam :cvs_rsh, required_features: [:cvs_rsh] do desc 'The value to be used for the CVS_RSH environment variable.' end newparam :depth, required_features: [:depth] do desc 'The value to be used to do a shallow clone.' end newparam :branch, required_features: [:branch] do desc 'The name of the branch to clone.' end newparam :p4config, required_features: [:p4config] do desc 'The Perforce P4CONFIG environment.' end newparam :submodules, required_features: [:submodules] do desc 'Initialize and update each submodule in the repository.' newvalues(true, false) defaultto true end newparam :conflict do desc 'The action to take if conflicts exist between repository and working copy' end newparam :trust_server_cert do desc 'Trust server certificate' newvalues(true, false) defaultto :false end newparam :keep_local_changes do desc 'Keep local changes on files tracked by the repository when changing revision' newvalues(true, false) defaultto :false end autorequire(:package) do ['git', 'git-core', 'mercurial', 'subversion'] end private def set_sensitive_parameters(sensitive_parameters) # rubocop:disable Naming/AccessorMethodName if sensitive_parameters.include?(:basic_auth_password) sensitive_parameters.delete(:basic_auth_password) parameter(:basic_auth_password).sensitive = true end super(sensitive_parameters) end end diff --git a/spec/acceptance/svn_paths_spec.rb b/spec/acceptance/svn_paths_spec.rb index f0394f6..ec18c71 100644 --- a/spec/acceptance/svn_paths_spec.rb +++ b/spec/acceptance/svn_paths_spec.rb @@ -1,270 +1,268 @@ # frozen_string_literal: true require 'spec_helper_acceptance' tmpdir = '/tmp/vcsrepo' describe 'subversion :includes tests on SVN version >= 1.7', unless: ( # rubocop:disable RSpec/MultipleDescribes : The # test's on this page must be kept seperate as they are for different operating systems. (os[:family] == 'redhat' && os[:release].start_with?('5', '6')) || (os[:family] == 'sles') -) do - + ) do before(:all) do run_shell("mkdir -p #{tmpdir}") # win test end after(:all) do run_shell("rm -rf #{tmpdir}/svnrepo") end context 'with include paths' do pp = <<-MANIFEST vcsrepo { "#{tmpdir}/svnrepo": ensure => present, provider => svn, includes => ['difftools/README', 'obsolete-notes',], source => "http://svn.apache.org/repos/asf/subversion/developer-resources", revision => 1000000, } MANIFEST it 'can checkout specific paths from svn' do # Run it twice and test for idempotency idempotent_apply(pp) end describe file("#{tmpdir}/svnrepo/difftools") do it { is_expected.to be_directory } end describe file("#{tmpdir}/svnrepo/difftools/README") do its(:md5sum) { is_expected.to eq '540241e9d5d4740d0ef3d27c3074cf93' } end describe file("#{tmpdir}/svnrepo/difftools/pics") do it { is_expected.not_to exist } end describe file("#{tmpdir}/svnrepo/obsolete-notes") do it { is_expected.to be_directory } end describe file("#{tmpdir}/svnrepo/obsolete-notes/draft-korn-vcdiff-01.txt") do its(:md5sum) { is_expected.to eq '37019f808e1af64864853a67526cfe19' } end describe file("#{tmpdir}/svnrepo/obsolete-notes/vcdiff-karlnotes") do its(:md5sum) { is_expected.to eq '26e23ff6a156de14aebd1099e23ac2d8' } end describe file("#{tmpdir}/svnrepo/guis") do it { is_expected.not_to exist } end end context 'with add include paths' do pp = <<-MANIFEST vcsrepo { "#{tmpdir}/svnrepo": ensure => present, provider => svn, includes => ['difftools/README', 'obsolete-notes', 'guis/pics', 'difftools/pics/README'], source => "http://svn.apache.org/repos/asf/subversion/developer-resources", revision => 1000000, } MANIFEST it 'can add paths to includes' do # Run it twice and test for idempotency idempotent_apply(pp) end describe file("#{tmpdir}/svnrepo/guis/pics/README") do its(:md5sum) { is_expected.to eq '62bdc9180684042fe764d89c9beda40f' } end describe file("#{tmpdir}/svnrepo/difftools/pics/README") do its(:md5sum) { is_expected.to eq 'bad02dfc3cb96bf5cadd59bf4fe3e00e' } end end context 'with remove include paths' do pp = <<-MANIFEST vcsrepo { "#{tmpdir}/svnrepo": ensure => present, provider => svn, includes => ['difftools/pics/README', 'obsolete-notes',], source => "http://svn.apache.org/repos/asf/subversion/developer-resources", revision => 1000000, } MANIFEST it 'can remove paths (and empty parent directories) from includes' do # Run it twice and test for idempotency idempotent_apply(pp) end describe file("#{tmpdir}/svnrepo/guis/pics/README") do it { is_expected.not_to exist } end describe file("#{tmpdir}/svnrepo/guis/pics") do it { is_expected.not_to exist } end describe file("#{tmpdir}/svnrepo/guis") do it { is_expected.not_to exist } end describe file("#{tmpdir}/svnrepo/difftools/pics/README") do its(:md5sum) { is_expected.to eq 'bad02dfc3cb96bf5cadd59bf4fe3e00e' } end describe file("#{tmpdir}/svnrepo/difftools/README") do it { is_expected.not_to exist } end end context 'with changing revisions' do pp = <<-MANIFEST vcsrepo { "#{tmpdir}/svnrepo": ensure => present, provider => svn, includes => ['difftools/README', 'obsolete-notes',], source => "http://svn.apache.org/repos/asf/subversion/developer-resources", revision => 1700000, } MANIFEST it 'can change revisions' do # Run it twice and test for idempotency idempotent_apply(pp) end it 'svn info svnrepo' do run_shell("svn info #{tmpdir}/svnrepo") do |r| expect(r.stdout).to match(%r{.*Revision: 1700000.*}) end end it 'svn info svnrepo/difftools/README' do run_shell("svn info #{tmpdir}/svnrepo/difftools/README") do |r| expect(r.stdout).to match(%r{.*Revision: 1700000.*}) end end end end describe 'subversion :includes tests on SVN version == 1.6', if: ( (os[:family] == 'redhat' && os[:release].start_with?('5', '6')) -) do - + ) do after(:all) do run_shell("rm -rf #{tmpdir}/svnrepo") end context 'with include paths' do pp = <<-MANIFEST vcsrepo { "#{tmpdir}/svnrepo": ensure => present, provider => svn, includes => ['difftools/README', 'obsolete-notes',], source => "http://svn.apache.org/repos/asf/subversion/developer-resources", revision => 1000000, } MANIFEST it 'can checkout specific paths from svn' do # Run it twice and test for idempotency idempotent_apply(pp) end describe file("#{tmpdir}/svnrepo/difftools") do it { is_expected.to be_directory } end describe file("#{tmpdir}/svnrepo/difftools/README") do its(:md5sum) { is_expected.to eq '540241e9d5d4740d0ef3d27c3074cf93' } end describe file("#{tmpdir}/svnrepo/difftools/pics") do it { is_expected.not_to exist } end describe file("#{tmpdir}/svnrepo/obsolete-notes") do it { is_expected.to be_directory } end describe file("#{tmpdir}/svnrepo/obsolete-notes/draft-korn-vcdiff-01.txt") do its(:md5sum) { is_expected.to eq '37019f808e1af64864853a67526cfe19' } end describe file("#{tmpdir}/svnrepo/obsolete-notes/vcdiff-karlnotes") do its(:md5sum) { is_expected.to eq '26e23ff6a156de14aebd1099e23ac2d8' } end describe file("#{tmpdir}/svnrepo/guis") do it { is_expected.not_to exist } end end context 'with add include paths' do pp = <<-MANIFEST vcsrepo { "#{tmpdir}/svnrepo": ensure => present, provider => svn, includes => ['difftools/README', 'obsolete-notes', 'guis/pics', 'difftools/pics/README'], source => "http://svn.apache.org/repos/asf/subversion/developer-resources", revision => 1000000, } MANIFEST it 'can add paths to includes' do # Run it twice and test for idempotency idempotent_apply(pp) end describe file("#{tmpdir}/svnrepo/guis/pics/README") do its(:md5sum) { is_expected.to eq '62bdc9180684042fe764d89c9beda40f' } end describe file("#{tmpdir}/svnrepo/difftools/pics/README") do its(:md5sum) { is_expected.to eq 'bad02dfc3cb96bf5cadd59bf4fe3e00e' } end end context 'with remove include paths' do pp = <<-MANIFEST vcsrepo { "#{tmpdir}/svnrepo": ensure => present, provider => svn, includes => ['difftools/pics/README', 'obsolete-notes',], source => "http://svn.apache.org/repos/asf/subversion/developer-resources", revision => 1000000, } MANIFEST it 'can remove directory paths (and empty parent directories) from includes, but not files with siblings' do apply_manifest(pp, catch_failures: true) end describe file("#{tmpdir}/svnrepo/guis/pics/README") do it { is_expected.not_to exist } end describe file("#{tmpdir}/svnrepo/guis/pics") do it { is_expected.not_to exist } end describe file("#{tmpdir}/svnrepo/guis") do it { is_expected.not_to exist } end describe file("#{tmpdir}/svnrepo/difftools/pics/README") do its(:md5sum) { is_expected.to eq 'bad02dfc3cb96bf5cadd59bf4fe3e00e' } end describe file("#{tmpdir}/svnrepo/difftools/README") do its(:md5sum) { is_expected.to eq '540241e9d5d4740d0ef3d27c3074cf93' } end end context 'with changing revisions' do pp = <<-MANIFEST vcsrepo { "#{tmpdir}/svnrepo": ensure => present, provider => svn, includes => ['difftools/README', 'obsolete-notes',], source => "http://svn.apache.org/repos/asf/subversion/developer-resources", revision => 1700000, } MANIFEST it 'can change revisions' do # Run it twice and test for idempotency idempotent_apply(pp) end it 'svn info of svnrepo' do run_shell("svn info #{tmpdir}/svnrepo") do |r| expect(r.stdout).to match(%r{.*Revision: 1700000.*}) end end it 'svn info of svnrepo/difftools/README' do run_shell("svn info #{tmpdir}/svnrepo/difftools/README") do |r| expect(r.stdout).to match(%r{.*Revision: 1700000.*}) end end end end diff --git a/spec/spec_helper_acceptance_local.rb b/spec/spec_helper_acceptance_local.rb index 8641c85..cfd8282 100644 --- a/spec/spec_helper_acceptance_local.rb +++ b/spec/spec_helper_acceptance_local.rb @@ -1,54 +1,54 @@ # frozen_string_literal: true require 'singleton' class LitmusHelper include Singleton include PuppetLitmus end RSpec.configure do |c| # Readable test descriptions c.formatter = :documentation # Configure all nodes in nodeset c.before :suite do case os[:family] when 'redhat' - if os[:release][0] =~ %r{5} + if %r{5}.match?(os[:release][0]) LitmusHelper.instance.run_shell('which git', expect_failures: true) LitmusHelper.instance.run_shell('rpm -ivh http://repository.it4i.cz/mirrors/repoforge/redhat/el5/en/x86_64/rpmforge/RPMS/rpmforge-release-0.5.3-1.el5.rf.x86_64.rpm', expect_failures: true) LitmusHelper.instance.run_shell('yum install -y git') end pp = <<-PP package { 'git': ensure => present, } package { 'subversion': ensure => present, } PP LitmusHelper.instance.apply_manifest(pp) when %r{(ubuntu|[dD]ebian|sles)} pp = <<-PP package { 'git-core': ensure => present, } package { 'subversion': ensure => present, } PP LitmusHelper.instance.apply_manifest(pp) else unless run_bolt_task('package', 'action' => 'status', 'name' => 'git') puts 'Git package is required for this module' exit end unless run_bolt_task('package', 'action' => 'status', 'name' => 'subversion') puts 'Subversion package is required for this module' exit end end LitmusHelper.instance.run_shell('git config --global user.email "root@localhost"') LitmusHelper.instance.run_shell('git config --global user.name "root"') end end # git with 3.18 changes the maximum enabled TLS protocol version, older OSes will fail these tests def only_supports_weak_encryption return_val = (os[:family] == 'redhat' && os[:release].start_with?('5', '6') || (os[:family] == 'sles' && os[:release].start_with?('11'))) return_val end diff --git a/spec/unit/puppet/type/vcsrepo_spec.rb b/spec/unit/puppet/type/vcsrepo_spec.rb index 1d80e45..d6aba6d 100755 --- a/spec/unit/puppet/type/vcsrepo_spec.rb +++ b/spec/unit/puppet/type/vcsrepo_spec.rb @@ -1,153 +1,153 @@ #! /usr/bin/env ruby # frozen_string_literal: true require 'spec_helper' describe Puppet::Type.type(:vcsrepo) do before :each do allow(Puppet::Type.type(:vcsrepo)).to receive(:defaultprovider).and_return(providerclass) end let(:providerclass) do described_class.provide(:fake_vcsrepo_provider) do attr_accessor :property_hash def create; end def destroy; end def exists? get(:ensure) != :absent end mk_resource_methods has_features :include_paths end end let(:provider) do providerclass.new(name: 'fake-vcs') end let(:resource) do described_class.new(name: '/repo', ensure: :present, source: 'http://example.com/repo/', provider: provider) end let(:ensureprop) do resource.property(:ensure) end let(:sourceprop) do resource.property(:source) end properties = [:ensure, :source] properties.each do |property| - it "should have a #{property} property" do + it "has a #{property} property" do expect(described_class.attrclass(property).ancestors).to be_include(Puppet::Property) end end parameters = [:ensure] parameters.each do |parameter| - it "should have a #{parameter} parameter" do + it "has a #{parameter} parameter" do expect(described_class.attrclass(parameter).ancestors).to be_include(Puppet::Parameter) end end describe "with an include path that starts with a '/'" do it 'raises a Puppet::ResourceError error' do expect { resource[:includes] = ['/path1/file1', '/path2/file2'] }.to raise_error(Puppet::ResourceError, %r{Include path '.*' starts with a '/'}) end end describe 'when using a provider that adds/removes a trailing / to the source' do it 'stays in sync when it leaves it as-is' do sourceprop.should = 'http://example.com/repo/' expect(sourceprop.safe_insync?('http://example.com/repo/')).to eq(true) end it 'stays in sync when it adds a slash' do sourceprop.should = 'http://example.com/repo' expect(sourceprop.safe_insync?('http://example.com/repo/')).to eq(true) end it 'stays in sync when it removes a slash' do sourceprop.should = 'http://example.com/repo/' expect(sourceprop.safe_insync?('http://example.com/repo')).to eq(true) end it 'is out of sync with a different source' do sourceprop.should = 'http://example.com/repo/asdf' expect(sourceprop.safe_insync?('http://example.com/repo')).to eq(false) end end describe 'default resource with required params' do it 'has a valid name parameter' do expect(resource[:name]).to eq('/repo') end it 'has ensure set to present' do expect(resource[:ensure]).to eq(:present) end it 'has path set to /repo' do expect(resource[:path]).to eq('/repo') end defaults = { owner: nil, group: nil, user: nil, revision: nil, } defaults.each_pair do |param, value| - it "should have #{param} parameter set to #{value}" do + it "has #{param} parameter set to #{value}" do expect(resource[param]).to eq(value) end end end describe 'when changing the ensure' do it 'is in sync if it is :absent and should be :absent' do ensureprop.should = :absent expect(ensureprop.safe_insync?(:absent)).to eq(true) end it 'is in sync if it is :present and should be :present' do ensureprop.should = :present expect(ensureprop.safe_insync?(:present)).to eq(true) end it 'is out of sync if it is :absent and should be :present' do ensureprop.should = :present expect(ensureprop.safe_insync?(:absent)).not_to eq(true) end it 'is out of sync if it is :present and should be :absent' do ensureprop.should = :absent expect(ensureprop.safe_insync?(:present)).not_to eq(true) end end describe 'when running the type it should autorequire packages' do let(:catalog) { Puppet::Resource::Catalog.new } let(:resource) { described_class.new(name: '/foo', provider: provider) } before :each do ['git', 'git-core', 'mercurial', 'subversion'].each do |pkg| catalog.add_resource(Puppet::Type.type(:package).new(name: pkg)) end end it 'requires package packages' do catalog.add_resource(resource) req = resource.autorequire expect(req.size).to eq(4) end end end