summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--lib/puppet/provider/vcsrepo.rb34
-rw-r--r--lib/puppet/provider/vcsrepo/bzr.rb64
-rw-r--r--lib/puppet/provider/vcsrepo/cvs.rb80
-rw-r--r--lib/puppet/provider/vcsrepo/git.rb264
-rw-r--r--lib/puppet/provider/vcsrepo/hg.rb101
-rw-r--r--lib/puppet/provider/vcsrepo/svn.rb82
-rw-r--r--lib/puppet/type/vcsrepo.rb138
7 files changed, 763 insertions, 0 deletions
diff --git a/lib/puppet/provider/vcsrepo.rb b/lib/puppet/provider/vcsrepo.rb
new file mode 100644
index 000000000..2c026ba86
--- /dev/null
+++ b/lib/puppet/provider/vcsrepo.rb
@@ -0,0 +1,34 @@
+require 'tmpdir'
+require 'digest/md5'
+require 'fileutils'
+
+# Abstract
+class Puppet::Provider::Vcsrepo < Puppet::Provider
+
+ private
+
+ def set_ownership
+ owner = @resource.value(:owner) || nil
+ group = @resource.value(:group) || nil
+ FileUtils.chown_R(owner, group, @resource.value(:path))
+ end
+
+ def path_exists?
+ File.directory?(@resource.value(:path))
+ end
+
+ # 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(&block) #: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
new file mode 100644
index 000000000..a0605624b
--- /dev/null
+++ b/lib/puppet/provider/vcsrepo/bzr.rb
@@ -0,0 +1,64 @@
+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'
+ defaultfor :bzr => :exists
+ has_features :reference_tracking
+
+ def create
+ if !@resource.value(:source)
+ create_repository(@resource.value(:path))
+ else
+ clone_repository(@resource.value(:revision))
+ end
+ end
+
+ def exists?
+ File.directory?(File.join(@resource.value(:path), '.bzr'))
+ 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)
+ bzr('update', '-r', desired, @resource.value(:path))
+ end
+
+ private
+
+ def create_repository(path)
+ bzr('init', path)
+ end
+
+ def clone_repository(revision)
+ args = ['branch']
+ if revision
+ args.push('-r', revision)
+ end
+ args.push(@resource.value(:source),
+ @resource.value(:path))
+ bzr(*args)
+ end
+
+end
diff --git a/lib/puppet/provider/vcsrepo/cvs.rb b/lib/puppet/provider/vcsrepo/cvs.rb
new file mode 100644
index 000000000..e82c23afe
--- /dev/null
+++ b/lib/puppet/provider/vcsrepo/cvs.rb
@@ -0,0 +1,80 @@
+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'
+ defaultfor :cvs => :exists
+ has_features :gzip_compression, :reference_tracking
+
+ def create
+ if !@resource.value(:source)
+ create_repository(@resource.value(:path))
+ else
+ checkout_repository
+ end
+ 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 destroy
+ FileUtils.rm_rf(@resource.value(:path))
+ end
+
+ def revision
+ if File.exist?(tag_file)
+ contents = File.read(tag_file)
+ # Note: Doesn't differentiate between N and T entries
+ contents[1..-1]
+ else
+ 'MAIN'
+ end
+ end
+
+ def revision=(desired)
+ at_path do
+ cvs('update', '-r', 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', '-d', basename, module_name)
+ cvs(*args)
+ end
+ if @resource.value(:revision)
+ self.revision = @resource.value(:revision)
+ end
+ end
+
+ # When the source:
+ # * Starts with ':' (eg, :pserver:...)
+ def module_name
+ if (source = @resource.value(:source))
+ source[0, 1] == ':' ? File.basename(source) : '.'
+ end
+ end
+
+ def create_repository(path)
+ cvs('-d', path, 'init')
+ end
+
+end
diff --git a/lib/puppet/provider/vcsrepo/git.rb b/lib/puppet/provider/vcsrepo/git.rb
new file mode 100644
index 000000000..fa7e492cf
--- /dev/null
+++ b/lib/puppet/provider/vcsrepo/git.rb
@@ -0,0 +1,264 @@
+require File.join(File.dirname(__FILE__), '..', 'vcsrepo')
+
+Puppet::Type.type(:vcsrepo).provide(:git, :parent => Puppet::Provider::Vcsrepo) do
+ desc "Supports Git repositories"
+
+ ##TODO modify the commands below so that the su - is included
+ commands :git => 'git'
+ defaultfor :git => :exists
+ has_features :bare_repositories, :reference_tracking
+
+ def create
+ if !@resource.value(:source)
+ init_repository(@resource.value(:path))
+ else
+ clone_repository(@resource.value(:source), @resource.value(:path))
+ if @resource.value(:revision)
+ if @resource.value(:ensure) == :bare
+ notice "Ignoring revision for bare repository"
+ else
+ checkout_or_reset
+ end
+ end
+ if @resource.value(:ensure) != :bare
+ update_submodules
+ end
+ end
+ update_owner_and_excludes
+ end
+
+ def destroy
+ FileUtils.rm_rf(@resource.value(:path))
+ end
+
+ def latest?
+ at_path do
+ return self.revision == self.latest
+ end
+ end
+
+ def latest
+ branch = on_branch?
+ if branch == 'master'
+ return get_revision('origin/HEAD')
+ else
+ return get_revision('origin/%s' % branch)
+ end
+ end
+
+ def revision
+ update_references
+ current = at_path { git('rev-parse', 'HEAD') }
+ canonical = at_path { git('rev-parse', @resource.value(:revision)) }
+ if current == canonical
+ @resource.value(:revision)
+ else
+ current
+ end
+ end
+
+ def revision=(desired)
+ checkout_or_reset(desired)
+ if local_branch_revision?(desired)
+ # reset instead of pull to avoid merge conflicts. assuming remote is
+ # authoritative.
+ # might be worthwhile to have an allow_local_changes param to decide
+ # whether to reset or pull when we're ensuring latest.
+ at_path { git('reset', '--hard', "origin/#{desired}") }
+ end
+ if @resource.value(:ensure) != :bare
+ update_submodules
+ end
+ update_owner_and_excludes
+ end
+
+ def bare_exists?
+ bare_git_config_exists? && !working_copy_exists?
+ end
+
+ def working_copy_exists?
+ File.directory?(File.join(@resource.value(:path), '.git'))
+ end
+
+ def exists?
+ working_copy_exists? || bare_exists?
+ end
+
+ def update_references
+ at_path do
+ git('fetch', '--tags', 'origin')
+ end
+ end
+
+ private
+
+ def bare_git_config_exists?
+ File.exist?(File.join(@resource.value(:path), 'config'))
+ end
+
+ def clone_repository(source, path)
+ check_force
+ args = ['clone']
+ if @resource.value(:ensure) == :bare
+ args << '--bare'
+ end
+ if !File.exist?(File.join(@resource.value(:path), '.git'))
+ args.push(source, path)
+ git(*args)
+ else
+ notice "Repo has already been cloned"
+ end
+ end
+
+ def check_force
+ if path_exists?
+ if @resource.value(:force)
+ 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
+
+ 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))
+ args = ['init']
+ if @resource.value(:ensure) == :bare
+ args << '--bare'
+ end
+ at_path do
+ git(*args)
+ end
+ end
+ end
+
+ # Convert working copy to bare
+ #
+ # Moves:
+ # <path>/.git
+ # to:
+ # <path>/
+ 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
+ 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('checkout', '-f')
+ update_owner_and_excludes
+ end
+ end
+
+ 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
+
+ def checkout_or_reset(revision = @resource.value(:revision))
+ if local_branch_revision?
+ reset(revision)
+ elsif tag_revision?
+ at_path { git('checkout', '-b', revision) }
+ elsif remote_branch_revision?
+ at_path { git('checkout', '-b', revision, '--track', "origin/#{revision}") }
+ end
+ end
+
+ def reset(desired)
+ at_path do
+ git('reset', '--hard', desired)
+ end
+ end
+
+ def update_submodules
+ at_path do
+ git('submodule', 'init')
+ git('submodule', 'update')
+ git('submodule', 'foreach', 'git', 'submodule', 'init')
+ git('submodule', 'foreach', 'git', 'submodule', 'update')
+ end
+ end
+
+ def remote_branch_revision?(revision = @resource.value(:revision))
+ # git < 1.6 returns 'origin/#{revision}'
+ # git 1.6+ returns 'remotes/origin/#{revision}'
+ at_path { branches.grep /(remotes\/)?origin\/#{revision}/ }
+ end
+
+ def local_branch_revision?(revision = @resource.value(:revision))
+ at_path { branches.include?(revision) }
+ end
+
+ def tag_revision?(revision = @resource.value(:revision))
+ at_path { tags.include?(revision) }
+ end
+
+ def branches
+ at_path { git('branch', '-a') }.gsub('*', ' ').split(/\n/).map { |line| line.strip }
+ end
+
+ def on_branch?
+ at_path { git('branch', '-a') }.split(/\n/).grep(/\*/).to_s.gsub('*', '').strip
+ end
+
+ def tags
+ at_path { git('tag', '-l') }.split(/\n/).map { |line| line.strip }
+ end
+
+ def set_excludes
+ at_path { open('.git/info/exclude', 'w') { |f| @resource.value(:excludes).each { |ex| f.write(ex + "\n") }}}
+ end
+
+ def get_revision(rev)
+ if !working_copy_exists?
+ create
+ end
+ at_path do
+ git('fetch', 'origin')
+ git('fetch', '--tags', 'origin')
+ end
+ current = at_path { git('rev-parse', rev).strip }
+ if @resource.value(:revision)
+ if local_branch_revision?
+ canonical = at_path { git('rev-parse', @resource.value(:revision)).strip }
+ elsif remote_branch_revision?
+ canonical = at_path { git('rev-parse', 'origin/' + @resource.value(:revision)).strip }
+ end
+ current = @resource.value(:revision) if current == canonical
+ end
+ return current
+ end
+
+ def update_owner_and_excludes
+ if @resource.value(:owner) or @resource.value(:group)
+ set_ownership
+ end
+ if @resource.value(:excludes)
+ set_excludes
+ end
+ end
+end
diff --git a/lib/puppet/provider/vcsrepo/hg.rb b/lib/puppet/provider/vcsrepo/hg.rb
new file mode 100644
index 000000000..f96758612
--- /dev/null
+++ b/lib/puppet/provider/vcsrepo/hg.rb
@@ -0,0 +1,101 @@
+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'
+ defaultfor :hg => :exists
+ has_features :reference_tracking
+
+ 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('incoming', '--branch', '.', '--newest-first', '--limit', '1')[/^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('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('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
+ hg('pull')
+ begin
+ hg('merge')
+ rescue Puppet::ExecutionFailure
+ # If there's nothing to merge, just skip
+ end
+ hg('update', '--clean', '-r', desired)
+ end
+ update_owner
+ end
+
+ private
+
+ def create_repository(path)
+ hg('init', path)
+ end
+
+ def clone_repository(revision)
+ args = ['clone']
+ if revision
+ args.push('-u', revision)
+ end
+ args.push(@resource.value(:source),
+ @resource.value(:path))
+ hg(*args)
+ end
+
+ def update_owner
+ if @resource.value(:owner) or @resource.value(:group)
+ set_ownership
+ end
+ end
+
+end
diff --git a/lib/puppet/provider/vcsrepo/svn.rb b/lib/puppet/provider/vcsrepo/svn.rb
new file mode 100644
index 000000000..680188c1e
--- /dev/null
+++ b/lib/puppet/provider/vcsrepo/svn.rb
@@ -0,0 +1,82 @@
+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'
+
+ defaultfor :svn => :exists
+ has_features :filesystem_types, :reference_tracking
+
+ def create
+ if !@resource.value(:source)
+ create_repository(@resource.value(:path))
+ else
+ checkout_repository(@resource.value(:source),
+ @resource.value(:path),
+ @resource.value(:revision))
+ end
+ end
+
+ def working_copy_exists?
+ File.directory?(File.join(@resource.value(:path), '.svn'))
+ end
+
+ def exists?
+ working_copy_exists?
+ end
+
+ def destroy
+ FileUtils.rm_rf(@resource.value(:path))
+ end
+
+ def latest?
+ at_path do
+ if self.revision < self.latest then
+ return false
+ else
+ return true
+ end
+ end
+ end
+
+ def latest
+ at_path do
+ svn('info', '-r', 'HEAD')[/^Revision:\s+(\d+)/m, 1]
+ end
+ end
+
+ def revision
+ at_path do
+ svn('info')[/^Revision:\s+(\d+)/m, 1]
+ end
+ end
+
+ def revision=(desired)
+ at_path do
+ svn('update', '-r', desired)
+ end
+ end
+
+ private
+
+ def checkout_repository(source, path, revision = nil)
+ args = ['checkout']
+ if revision
+ args.push('-r', revision)
+ 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
+
+end
diff --git a/lib/puppet/type/vcsrepo.rb b/lib/puppet/type/vcsrepo.rb
new file mode 100644
index 000000000..f0d2613ca
--- /dev/null
+++ b/lib/puppet/type/vcsrepo.rb
@@ -0,0 +1,138 @@
+require 'pathname'
+
+Puppet::Type.newtype(:vcsrepo) do
+ desc "A local version control repository"
+
+ feature :gzip_compression,
+ "The provider supports explicit GZip compression levels"
+
+ 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)"
+
+ ensurable do
+ attr_accessor :latest
+
+ def insync?(is)
+ @should ||= []
+
+ case should
+ when :present
+ return true unless [:absent, :purged, :held].include?(is)
+ when :latest
+ if is == :latest
+ return true
+ else
+ self.debug "%s repo revision is %s, latest is %s" %
+ [@resource.name, provider.revision, provider.latest]
+ return false
+ end
+ end
+ end
+
+ newvalue :present do
+ provider.create
+ end
+
+ newvalue :bare, :required_features => [:bare_repositories] do
+ provider.create
+ end
+
+ newvalue :absent do
+ provider.destroy
+ end
+
+ newvalue :latest, :required_features => [:reference_tracking] do
+ if provider.exists?
+ if provider.respond_to?(:update_references)
+ provider.update_references
+ end
+ if provider.respond_to?(:latest?)
+ reference = provider.latest || provider.revision
+ else
+ reference = resource.value(:revision) || provider.revision
+ end
+ notice "Updating to latest '#{reference}' revision"
+ provider.revision = reference
+ else
+ provider.create
+ end
+ end
+
+ def retrieve
+ prov = @resource.provider
+ if prov
+ if prov.working_copy_exists?
+ prov.latest? ? :latest : :present
+ elsif prov.class.feature?(:bare_repositories) and prov.bare_exists?
+ :bare
+ else
+ :absent
+ end
+ else
+ raise Puppet::Error, "Could not find provider"
+ 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
+
+ newparam(:source) do
+ desc "The source URI for the repository"
+ end
+
+ newparam(:fstype, :required_features => [:filesystem_types]) do
+ desc "Filesystem type"
+ end
+
+ newproperty(:revision) do
+ desc "The revision of the repository"
+ newvalue(/^\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(:excludes) do
+ desc "Files to be excluded from the repository"
+ end
+
+ newparam(:force) do
+ desc "Force repository creation, destroying any files on the path in the process."
+ newvalues(:true, :false)
+ 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
+
+end