diff options
| author | Micah <micah@leap.se> | 2016-05-24 10:19:38 -0400 | 
|---|---|---|
| committer | Micah <micah@leap.se> | 2016-05-24 10:19:38 -0400 | 
| commit | 92a1ac5b29b497ee659c8097a5c0de94a481181c (patch) | |
| tree | e9c325ad24a732b6bdb13801ef2997f10f99e02e /lib/puppet/provider/vcsrepo | |
Squashed 'puppet/modules/vcsrepo/' content from commit 4e23209
git-subtree-dir: puppet/modules/vcsrepo
git-subtree-split: 4e23209eaccf1ab504d35158f4141b3053327c2f
Diffstat (limited to 'lib/puppet/provider/vcsrepo')
| -rw-r--r-- | lib/puppet/provider/vcsrepo/bzr.rb | 93 | ||||
| -rw-r--r-- | lib/puppet/provider/vcsrepo/cvs.rb | 135 | ||||
| -rw-r--r-- | lib/puppet/provider/vcsrepo/dummy.rb | 12 | ||||
| -rw-r--r-- | lib/puppet/provider/vcsrepo/git.rb | 483 | ||||
| -rw-r--r-- | lib/puppet/provider/vcsrepo/hg.rb | 130 | ||||
| -rw-r--r-- | lib/puppet/provider/vcsrepo/p4.rb | 278 | ||||
| -rw-r--r-- | lib/puppet/provider/vcsrepo/svn.rb | 139 | 
7 files changed, 1270 insertions, 0 deletions
diff --git a/lib/puppet/provider/vcsrepo/bzr.rb b/lib/puppet/provider/vcsrepo/bzr.rb new file mode 100644 index 00000000..797d84d2 --- /dev/null +++ b/lib/puppet/provider/vcsrepo/bzr.rb @@ -0,0 +1,93 @@ +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 +    if !@resource.value(:source) +      create_repository(@resource.value(:path)) +    else +      clone_repository(@resource.value(:revision)) +    end +  end + +  def working_copy_exists? +    File.directory?(File.join(@resource.value(:path), '.bzr')) +  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')[/^revision-id:\s+(\S+)/, 1] +      desired = @resource.value(:revision) +      begin +        desired_revid = bzr('revision-info', desired).strip.split(/\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 latest +    at_path do +      bzr('version-info', ':parent')[/^revision-id:\s+(\S+)/, 1] +    end +  end + +  def latest? +    at_path do +      return self.revision == self.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 +    if @resource.value(:owner) or @resource.value(:group) +      set_ownership +    end +  end +end diff --git a/lib/puppet/provider/vcsrepo/cvs.rb b/lib/puppet/provider/vcsrepo/cvs.rb new file mode 100644 index 00000000..7a8f6ef3 --- /dev/null +++ b/lib/puppet/provider/vcsrepo/cvs.rb @@ -0,0 +1,135 @@ +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 +    if !@resource.value(:source) +      create_repository(@resource.value(:path)) +    else +      checkout_repository +    end +    update_owner +  end + +  def exists? +    if @resource.value(:source) +      directory = File.join(@resource.value(:path), 'CVS') +    else +      directory = File.join(@resource.value(:path), 'CVSROOT') +    end +    File.directory?(directory) +  end + +  def working_copy_exists? +    File.directory?(File.join(@resource.value(:path), 'CVS')) +  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 == "") +      if (!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 = self.revision +    return should != current ? should : current +  end + +  def revision +    if !@rev +      if File.exist?(tag_file) +        contents = File.read(tag_file).strip +        # 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 +    return @rev +  end + +  def revision=(desired) +    at_path do +      runcvs('update', '-dr', desired, '.') +      update_owner +      @rev = desired +    end +  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 + +  # When the source: +  # * Starts with ':' (eg, :pserver:...) +  def module_name +    if (m = @resource.value(:module)) +      m +    elsif (source = @resource.value(:source)) +      source[0, 1] == ':' ? File.basename(source) : '.' +    end +  end + +  def create_repository(path) +    runcvs('-d', path, 'init') +  end + +  def update_owner +    if @resource.value(:owner) or @resource.value(:group) +      set_ownership +    end +  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) and @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) +    else +      Puppet::Util::Execution.execute([:cvs, *args], :custom_environment => e) +    end +  end +end diff --git a/lib/puppet/provider/vcsrepo/dummy.rb b/lib/puppet/provider/vcsrepo/dummy.rb new file mode 100644 index 00000000..27bfbbed --- /dev/null +++ b/lib/puppet/provider/vcsrepo/dummy.rb @@ -0,0 +1,12 @@ +require File.join(File.dirname(__FILE__), '..', 'vcsrepo') + +Puppet::Type.type(:vcsrepo).provide(:dummy, :parent => Puppet::Provider::Vcsrepo) do +  desc "Dummy default provider" + +  defaultfor :feature => :posix + +  def working_copy_exists? +    providers = @resource.class.providers.map{|x| x.to_s}.sort.reject{|x| x == "dummy"}.join(", ") rescue "none" +    raise("vcsrepo resource must have a provider, available: #{providers}") +  end +end diff --git a/lib/puppet/provider/vcsrepo/git.rb b/lib/puppet/provider/vcsrepo/git.rb new file mode 100644 index 00000000..9d18b474 --- /dev/null +++ b/lib/puppet/provider/vcsrepo/git.rb @@ -0,0 +1,483 @@ +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 +    if @resource.value(:revision) and ensure_bare_or_mirror? +      fail("Cannot set a revision (#{@resource.value(:revision)}) on a bare repository") +    end +    if !@resource.value(:source) +      if @resource.value(:ensure) == :mirror +        fail("Cannot init repository with mirror option, try bare instead") +      end + +      init_repository(@resource.value(:path)) +    else +      clone_repository(default_url, @resource.value(:path)) +      update_remotes + +      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? +    return 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 not @resource.value(:revision) and branch = on_branch? +      return branch +    else +      return @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}") } +      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 +    if @resource.value(:source).is_a?(Hash) +      if @resource.value(:source).has_key?(@resource.value(:remote)) +        @resource.value(:source)[@resource.value(:remote)] +      else +        fail("You must specify the URL for #{@resource.value(:remote)} in the :source hash") +      end +    else +      @resource.value(:source) +    end +  end + +  def working_copy_exists? +    if @resource.value(:source) and File.exists?(File.join(@resource.value(:path), '.git', 'config')) +      File.readlines(File.join(@resource.value(:path), '.git', 'config')).grep(/#{Regexp.escape(default_url)}/).any? +    else +      File.directory?(File.join(@resource.value(:path), '.git')) +    end +  end + +  def exists? +    working_copy_exists? || bare_exists? +  end + +  def update_remote_url(remote_name, remote_url) +    do_update = false +    current = git_with_identity('config', '-l') + +    unless remote_url.nil? +      # Check if remote exists at all, regardless of URL. +      # If remote doesn't exist, add it +      if not current.include? "remote.#{remote_name}.url" +        git_with_identity('remote','add', remote_name, remote_url) +        return true + +      # If remote exists, but URL doesn't match, update URL +      elsif not current.include? "remote.#{remote_name}.url=#{remote_url}" +        git_with_identity('remote','set-url', remote_name, remote_url) +        return true +      else +        return false +      end +    end + +  end + +  def update_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 @resource.value(:source).is_a?(Hash) +      @resource.value(:source).keys.sort.each do |remote_name| +        remote_url = @resource.value(:source)[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), @resource.value(:source)) } +    end + +    # If at least one remote was added or updated, then we must +    # call the 'git remote update' command +    if do_update == true +      at_path { git_with_identity('remote','update') } +    end + +  end + +  def update_references +    at_path do +      update_remotes +      git_with_identity('fetch', @resource.value(:remote)) +      git_with_identity('fetch', '--tags', @resource.value(:remote)) +      update_owner_and_excludes +    end +  end + +  private + +  def valid_repo? +    Dir.chdir(@resource.value(:path)){ system('git rev-parse > /dev/null 2>&1')} +  end + +  def bare_git_config_exists? +    File.exist?(File.join(@resource.value(:path), 'config')) && valid_repo? +  end + +  # @!visibility private +  def clone_repository(source, path) +    check_force +    args = ['clone'] +    if @resource.value(:depth) and @resource.value(:depth).to_i > 0 +      args.push('--depth', @resource.value(:depth).to_s) +      if @resource.value(:revision) +        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 check_force +    if path_exists? and not path_empty? +      if @resource.value(:force) && !valid_repo? +        notice "Removing %s to replace with vcsrepo." % @resource.value(:path) +        destroy +      else +        raise Puppet::Error, "Could not create repository (non-repository at path)" +      end +    end +  end + +  # @!visibility private +  def init_repository(path) +    check_force +    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 + +  # Convert working copy to bare +  # +  # Moves: +  #   <path>/.git +  # to: +  #   <path>/ +  # @!visibility private +  def convert_working_copy_to_bare +    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)) +  end + +  # Convert bare to working copy +  # +  # Moves: +  #   <path>/ +  # to: +  #   <path>/.git +  # @!visibility private +  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_in?(File.join(@resource.value(:path), '.git')) +      reset('HEAD') +      git_with_identity('checkout', '--force') +      update_owner_and_excludes +    end +  end + +  # @!visibility private +  def commits_in?(dot_git) +    Dir.glob(File.join(dot_git, 'objects/info/*'), File::FNM_DOTMATCH) do |e| +      return true unless %w(. ..).include?(File::basename(e)) +    end +    false +  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)) +    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 +  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 /(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', '-a') }.gsub('*', ' ').split(/\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 { +      matches = git_with_identity('branch', '-a').match /\*\s+(.*)/ +      matches[1] unless matches[1].match /(\(detached from|\(HEAD detached at|\(no branch)/ +    } +  end + +  # @!visibility private +  def tags +    at_path { git_with_identity('tag', '-l') }.split(/\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 + +  # 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 if !working_copy_exists? + +    if branch = on_branch? +      return get_revision("#{@resource.value(:remote)}/#{branch}") +    else +      return get_revision +    end +  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') +    if @resource.value(:source) +      update_references +    else +      status = at_path { git_with_identity('status')} +      is_it_new = status =~ /Initial commit/ +      if is_it_new +        status =~ /On branch (.*)/ +        branch = $1 +        return branch +      end +    end +    current = at_path { git_with_identity('rev-parse', rev).strip } +    if @resource.value(:revision) +      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. +        canonical = at_path { git_with_identity('rev-parse', "#{@resource.value(:revision)}^0").strip } +      elsif local_branch_revision? +        canonical = at_path { git_with_identity('rev-parse', @resource.value(:revision)).strip } +      elsif remote_branch_revision? +        canonical = at_path { git_with_identity('rev-parse', "#{@resource.value(:remote)}/#{@resource.value(:revision)}").strip } +      else +        #look for a sha (could match invalid shas) +        canonical = at_path { git_with_identity('rev-parse', '--revs-only', @resource.value(:revision)).strip } +      end +      fail("#{@resource.value(:revision)} is not a local or remote ref") if canonical.nil? or canonical.empty? +      current = @resource.value(:revision) if current == canonical +    end +    return current +  end + +  # @!visibility private +  def update_owner_and_excludes +    if @resource.value(:owner) or @resource.value(:group) +      set_ownership +    end +    if @resource.value(:excludes) +      set_excludes +    end +  end + +  # @!visibility private +  def git_with_identity(*args) +    if @resource.value(:identity) +      Tempfile.open('git-helper', Puppet[:statedir]) do |f| +        f.puts '#!/bin/sh' +        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(0755, f.path) +        env_save = ENV['GIT_SSH'] +        ENV['GIT_SSH'] = f.path + +        ret = git(*args) + +        ENV['GIT_SSH'] = env_save + +        return ret +      end +    elsif @resource.value(:user) and @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']}) +    else +      git(*args) +    end +  end +end diff --git a/lib/puppet/provider/vcsrepo/hg.rb b/lib/puppet/provider/vcsrepo/hg.rb new file mode 100644 index 00000000..294c2a97 --- /dev/null +++ b/lib/puppet/provider/vcsrepo/hg.rb @@ -0,0 +1,130 @@ +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 +    if !@resource.value(:source) +      create_repository(@resource.value(:path)) +    else +      clone_repository(@resource.value(:revision)) +    end +    update_owner +  end + +  def working_copy_exists? +    File.directory?(File.join(@resource.value(:path), '.hg')) +  end + +  def exists? +    working_copy_exists? +  end + +  def destroy +    FileUtils.rm_rf(@resource.value(:path)) +  end + +  def latest? +    at_path do +      return self.revision == self.latest +    end +  end + +  def latest +    at_path do +      begin +        hg_wrapper('incoming', '--branch', '.', '--newest-first', '--limit', '1', { :remote => true })[/^changeset:\s+(?:-?\d+):(\S+)/m, 1] +      rescue Puppet::ExecutionFailure +        # If there are no new changesets, return the current nodeid +        self.revision +      end +    end +  end + +  def revision +    at_path do +      current = hg_wrapper('parents')[/^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')[/^#{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 +      end +      begin +        hg_wrapper('merge') +      rescue Puppet::ExecutionFailure +        # If there's nothing to merge, just skip +      end +      hg_wrapper('update', '--clean', '-r', desired) +    end +    update_owner +  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 +    if @resource.value(:owner) or @resource.value(:group) +      set_ownership +    end +  end + +  def hg_wrapper(*args) +    options = { :remote => false } +    if args.length > 0 and 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=#{@resource.value(:basic_auth_password)}\"", +        "--config", "\"auth.x.schemes=http https\"" +      ] +    end + +    if options[:remote] and @resource.value(:identity) +      args += ["--ssh", "ssh -oStrictHostKeyChecking=no -oPasswordAuthentication=no -oKbdInteractiveAuthentication=no -oChallengeResponseAuthentication=no -i #{@resource.value(:identity)}"] +    end +    if @resource.value(:user) and @resource.value(:user) != Facter['id'].value +      args.map! { |a| if a =~ /\s/ then "'#{a}'" else a end }  # Adds quotes to arguments with whitespaces. +      Puppet::Util::Execution.execute("hg #{args.join(' ')}", :uid => @resource.value(:user), :failonfail => true) +    else +      hg(*args) +    end +  end +end diff --git a/lib/puppet/provider/vcsrepo/p4.rb b/lib/puppet/provider/vcsrepo/p4.rb new file mode 100644 index 00000000..b429bcbb --- /dev/null +++ b/lib/puppet/provider/vcsrepo/p4.rb @@ -0,0 +1,278 @@ +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 +    # 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}) + +    return (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 = self.revision +    if rev +      (rev >= self.latest) +    else +      true +    end +  end + +  def latest +    args = ['changes'] +    args.push('-m1', @resource.value(:source)) +    hash = p4(args) + +    return 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 +    return revision +  end + +  def revision=(desired) +    sync_client(@resource.value(:source), desired) +    update_owner +  end + +  private + +  def update_owner +    if @resource.value(:owner) or @resource.value(:group) +      set_ownership +    end +  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(/^P4CLIENT=/).pop +        p = /^P4CLIENT=(.*)$/ +        set_client = p.match(m)[1] if m +      end +    end + +    return 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(/\//) +      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) + +    return hash +  end + + +  # Saves the client workspace spec from the given hash +  # Params: +  # +hash+:: hash map of client spec +  def save_client(hash) +    spec = String.new +    view = "\nView:\n" + +    hash.keys.sort.each do |k| +      v = hash[k] +      next if( k == "code" ) +      if(k.to_s =~ /View/ ) +        view += "\t#{v}\n" +      else +        spec += "#{k.to_s}: #{v.to_s}\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 = Hash.new +    cfg.store 'P4CONFIG', p4config if p4config +    cfg.store 'P4CLIENT', client_name +    return 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 = Hash.new +    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.load(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" +    return hash +  end + +  # helper method as cstat does not Marshal +  def marshal_cstat(hash) +    data = hash['data'] +    code = 'error' + +    list = Array.new +    change = Hash.new +    data.each_line do |l| +      p = /^\.\.\. (.*) (.*)$/ +      m = p.match(l) +      if m +        change[m[1]] = m[2] +        if m[1] == 'status' +          code = 'stat' +          list.push change +          change = Hash.new +        end +      end +    end + +    hash = Hash.new +    hash.store 'code', code +    hash.store 'data', list +    return hash +  end + +end diff --git a/lib/puppet/provider/vcsrepo/svn.rb b/lib/puppet/provider/vcsrepo/svn.rb new file mode 100644 index 00000000..fccfaa5a --- /dev/null +++ b/lib/puppet/provider/vcsrepo/svn.rb @@ -0,0 +1,139 @@ +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 + +  def create +    if !@resource.value(:source) +      create_repository(@resource.value(:path)) +    else +      checkout_repository(@resource.value(:source), +                          @resource.value(:path), +                          @resource.value(:revision), +                          @resource.value(:depth)) +    end +    update_owner +  end + +  def working_copy_exists? +    if File.directory?(@resource.value(:path)) +      # :path is an svn checkout +      return true if File.directory?(File.join(@resource.value(:path), '.svn')) +      if File.file?(File.join(@resource.value(:path), 'format')) +        # :path is an svn server +        return true if svnlook('uuid', @resource.value(:path)) +      end +    end +    false +  end + +  def exists? +    working_copy_exists? +  end + +  def destroy +    FileUtils.rm_rf(@resource.value(:path)) +  end + +  def latest? +    at_path do +      (self.revision >= self.latest) and (@resource.value(:source) == self.sourceurl) +    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', @resource.value(:basic_auth_password)) +      args.push('--no-auth-cache') +    end + +    if @resource.value(:force) +      args.push('--force') +    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(*args)[/^Revision:\s+(\d+)/m, 1] +    end +  end + +  def sourceurl +    args = buildargs.push('info') +    at_path do +      svn(*args)[/^URL:\s+(\S+)/m, 1] +    end +  end + +  def revision +    args = buildargs.push('info') +    at_path do +      svn(*args)[/^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(:conflict) +      args.push('--accept', @resource.value(:conflict)) +    end + +    at_path do +      svn(*args) +    end +    update_owner +  end + +  private + +  def checkout_repository(source, path, revision, depth) +    args = buildargs.push('checkout') +    if revision +      args.push('-r', revision) +    end +    if depth +      args.push('--depth', depth) +    end +    args.push(source, path) +    svn(*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 +    if @resource.value(:owner) or @resource.value(:group) +      set_ownership +    end +  end +end  | 
